DocsHow it works
A stamp replaces the funding transaction.
A fresh address can't move until something pays its gas, and funding it from your main wallet links the two on a public ledger. The Stamp paymaster pays instead, for any UserOperation that carries a stamp: a prepaid stamp bought in advance and blind-signed by the relay, or a stamp mined on the spot with proof of work. It asks the fresh address for no ETH and no signature from a known account.
Two postages#
Both postages go through the same relay, the same epochs and the same policy checks. They differ in who pays and what rations them.
| Postage | Prepaid stamp | Mined stamp (free) |
|---|---|---|
| What the client brings | One blind-signed stamp from its saved stamps | 8 keccak sub-solutions bound to the op's hash |
| Who pays the gas | The buyer, in advance: the stamp float in the paymaster's deposit | The free lane: each epoch's budget, refilled from the treasury |
| What rations it | The deposit's headroom only | Difficulty, the epoch budget and the relay's pacing |
| paymasterData version | 2 (no work; nullifier ‖ co-signature) | 1 (nonces ‖ co-signature) |
| Relay method | stamp_sendUserOperation | eth_sendUserOperation |
| Exact postOp gas limit | stampPostOpGasLimit | pmPostOpGasLimit |
| Accounted as | StampCancelled (the nullifier recorded) | OpSponsored (spend and work of the epoch) |
| Cost to you | The stamp's price in ETH, paid once from any wallet | Seconds of CPU; nothing in ETH |
A prepaid stamp is e-cash for gas: bought from any wallet, it carries no trace of that wallet when it is used. Prepaid stamps covers its whole life; this page covers what the two postages share and the proof of work.
- BuyAny wallet pays PostOffice.buy for N blinded serials. The purchase is public and commits to the blinded messages.PostOffice.buy
- IssueThe relay checks the mined purchase and blind-signs exactly the committed messages. It never sees a serial.stamp_issue
- KeepThe client unblinds the signatures into stamps and keeps them, encrypted when exported. A stamp is a bearer token.local only
- UseA fresh address with 0 ETH sends one operation with one stamp. The relay verifies it and co-signs the op with its nullifier.stamp_sendUserOperation
- CancelThe paymaster pays the gas from the stamp float and records the nullifier in postOp. A second use is public.StampCancelled
The relay meets a stamp twice, blinded at issuance and unblinded at use, and cannot match the two. What it can match is your IP address and your timing: see the privacy guide.
The flow#
- Epoch. The SDK polls the relay's
pow_getEpochevery 60 s in the background. An epoch (15 minutes) fixes the fee caps, gas caps, the difficulty and a random seed revealed only when the epoch starts, and whether prepaid stamps are accepted (theSTAMPSflag). - Build. The op's gas limits come from fixed per-template profiles, its fees from the epoch, its pre-verification gas from the relay's quote. The EntryPoint nonce key is always 0. A first op carries an EIP-7702 authorization for Simple7702Account v0.9.
- Hash.
userOpHashis the EntryPoint v0.9 EIP-712 hash, which covers the 7702 delegation. - Attach the stamp. With a prepaid stamp, the op carries the stamp's nullifier, and the account signs at once. With a mined
stamp, the proof lives in the unsigned
paymasterSignaturesuffix, so the account signs while the miner searches for 8 nonces. - Relay. The relay recomputes the hash, verifies the stamp (its signature, and that it was never spent) or the proof, and every on-chain rule, reserves the op's maximum cost against the deposit, co-signs and forwards the op to its own bundler (or holds it for the delay you asked for).
- Bundle. The bundler sends a type-4 transaction that applies the delegation and calls
handleOps. The fresh address holds 0 ETH before and after. - Validate and account.
validatePaymasterUserOpchecks the co-signature, the policy and, for a mined stamp, the proof, against the epoch's write-once parameters.postOprecords the cost:StampCancelled(the nullifier recorded, the cost taken from the stamp float) orOpSponsored(the epoch's spend).
paymasterAndData#
The version byte of the signed paymasterData selects the postage. Everything the relay adds sits in the EntryPoint
v0.9 paymasterSignature suffix, which userOpHash excludes: the signed part only carries the version and the epoch id.
Prepaid stamp · version 2 · 164 bytes
- In
userOpHash, signed by the account - The stamp's nullifier,
keccak256(abi.encode(NULLIFIER_TAG, serial)) - Relay co-signature over userOpHash and the nullifier, and the length
paymasterAndData, drawn to scale (164 bytes). No work: the suffix holds the nullifier the paymaster records in postOp, and the relay's co-signature, which only the relay can produce once it has checked the stamp. The stamp's serial and signature go to the relay alone, never on chain.| Offset | Bytes | Field | Encoding | In userOpHash |
|---|---|---|---|---|
| 0 | 20 | paymaster | address | yes |
| 20 | 16 | paymasterVerificationGasLimit | uint128 BE, must equal the epoch's pmVerificationGasLimit | yes |
| 36 | 16 | paymasterPostOpGasLimit | uint128 BE, must equal the epoch's stampPostOpGasLimit | yes |
| 52 | 1 | version = 0x02 | uint8 | yes (signed paymasterData) |
| 53 | 4 | epochId | uint32 BE; the epoch must have the STAMPS flag | yes (signed paymasterData) |
| 57 | 32 | nullifier | keccak256(abi.encode(NULLIFIER_TAG, serial)), never 0 | no (paymasterSignature) |
| 89 | 65 | cosig | r ‖ s ‖ v over stampDigest(userOpHash, nullifier), low-s | no (paymasterSignature) |
| 154 | 2 | sigLen | uint16 BE: 0x0061 (97) | no |
| 156 | 8 | PAYMASTER_SIG_MAGIC | 0x22e325a297439656 | yes (the magic only) |
Mined stamp · version 1 · 196 bytes
- In
userOpHash, signed by the account - The 8 proof-of-work nonces (8 B each)
- Relay co-signature and length
paymasterAndData in co-signed mode, drawn to scale (196 bytes). The proof lives in the unsigned suffix, so the account can sign while the miner searches.| Offset | Bytes | Field | Encoding | In userOpHash |
|---|---|---|---|---|
| 0 | 20 | paymaster | address | yes |
| 20 | 16 | paymasterVerificationGasLimit | uint128 BE, must equal the epoch's pmVerificationGasLimit | yes |
| 36 | 16 | paymasterPostOpGasLimit | uint128 BE, must equal the epoch's pmPostOpGasLimit | yes |
| 52 | 1 | version = 0x01 | uint8 | yes (signed paymasterData) |
| 53 | 4 | epochId | uint32 BE | yes (signed paymasterData) |
| 57 | 64 | nonces[0..7] | 8 × uint64 BE, strictly increasing | no (paymasterSignature) |
| 121 | 65 | cosig | r ‖ s ‖ v over cosignDigest(userOpHash), v ∈ {27, 28}, low-s | no (paymasterSignature) |
| 186 | 2 | sigLen | uint16 BE: 0x0081 (129) | no |
| 188 | 8 | PAYMASTER_SIG_MAGIC | 0x22e325a297439656 | yes (the magic only) |
In viem terms: paymasterData = version ‖ epochId (5 bytes), and paymasterSignature = nullifier ‖ cosig (97 bytes) for
a prepaid stamp or nonces ‖ cosig (129 bytes) for a mined one. viem appends the length and the magic itself. Before the
relay co-signs, the SDK fills the co-signature with a 65-byte stub (and, mining, the nonces with 64 zero bytes), so the
hashed length is already final.
The challenge (mined stamp)#
challenge = keccak256(abi.encode(
CHALLENGE_TAG, // keccak256("StampPaymaster/v1/challenge")
block.chainid,
address(entryPoint),
address(this), // the paymaster
epochId, // uint32
seed, // uint96, revealed when the epoch starts
userOpHash // EntryPoint v0.9 EIP-712 hash
))
h_i = keccak256(abi.encode(challenge, uint256(nonce_i))) // one 64-byte keccak block
accept_i ⇔ uint256(h_i) ≤ targetPerSolution- Bound to one op. The chain id, EntryPoint and paymaster are in the challenge, and the EntryPoint nonce inside
userOpHashmakes each proof usable once. - No stockpiling. The seed is unknown until the epoch starts, ops expire 5 minutes after the next roll, and the relay refuses an old epoch's ops 2 minutes after a roll.
- 8 independent solutions. The 8 nonces must be strictly increasing and each must hash under the target.
Inputs
- CHALLENGE_TAG
- 0x653a…82cc
- chainid
- 4663
- entryPoint
- 0x4337…D009
- paymaster
- 0x7e57…c0de
- epochId
- 145
- seed
- 0x5eed…5eed
- userOpHash
- 0xfc83…76b7
challenge = keccak256(abi.encode(…))0x65fcac027d28e65abf25c937a24111f91518accdad0f9427ca637c895a5b727f
h = keccak256(challenge ‖ nonce) ≤ target- Sub-solution 1: nonce 2,723, hash 0x0001d98f…330253a9, 15 leading zero bits.
- Sub-solution 2: nonce 5,024, hash 0x000eecd8…ccc3bcbe, 12 leading zero bits.
- Sub-solution 3: nonce 5,566, hash 0x0004e561…557eb662, 13 leading zero bits.
- Sub-solution 4: nonce 9,304, hash 0x0003e89a…2227d416, 14 leading zero bits.
- Sub-solution 5: nonce 12,669, hash 0x00022bd8…72c45183, 14 leading zero bits.
- Sub-solution 6: nonce 13,425, hash 0x0007f014…4ae3f26e, 13 leading zero bits.
- Sub-solution 7: nonce 18,494, hash 0x000ee08f…863c943d, 12 leading zero bits.
- Sub-solution 8: nonce 21,233, hash 0x000889e9…458e8222, 12 leading zero bits.
How much work (mined stamp)#
gasSum = verificationGasLimit + callGasLimit + paymasterVerificationGasLimit
+ paymasterPostOpGasLimit + preVerificationGas
work = max(minWork, gasSum × refFeePerGas × workPerGwei / 1e9) // expected total hashes
perSolution = ceil(work / 8)
target = (2^256 − 1) / perSolution- Priced like the gas the paymaster pays.
refFeePerGasis the base fee snapshotted at the roll, so work follows fee levels, and fee headroom inmaxFeePerGascosts no extra work. - Tight limits pay off. Every client of a template uses the same published gas profile, so the work is the same for everyone.
- K = 8. With 8 sub-solutions the 95th-percentile solve time is about 1.64 × the mean (3 × with one solution), and progress can be shown honestly as x/8.
- Bounded.
work ≤ 2^48for every epoch, far inside the 64-bit nonce space.
gasSum 390,000
verificationGasLimit· VGL- 50,000
callGasLimit· CGL- 80,000
paymasterVerificationGasLimit· pmVGL- 75,000
paymasterPostOpGasLimit· postOp- 25,000
preVerificationGas· PVG- 160,000
work = 390,000 × 0.022 gwei × 5,000 / 1e9
= 42,900,000 hashes ≈ 225.4
per sub-solution = ⌈work / 8⌉ = 5,362,500 → 22 zero bits
8 sub-solutions≈ 222 hashes each
A first op on mainnet (a token transfer with its delegation, gasSum ≈ 390,000 with the L1 data part of PVG) at the 5,000 floor and a 0.022 gwei base fee costs about 2^25.4 hashes: around 3.6 s on a mid laptop (12 MH/s) and 11 s on a mid Android phone (3.75 MH/s). At the 30,000 ceiling (6×), about 21 s and 69 s; the p95 is 1.64 × these means, about 113 s on the phone.
Validation#
Validation reads only the op's epoch (4 storage slots, written once) and, with the delegate check, the sender's 23-byte
delegation designator, on both paths. It never reads a nullifier: the relay refuses a spent stamp, and the chain records
every one, so a double spend is public instead of possible. It reverts only on malformed data, an unknown epoch, or a
prepaid stamp in an epoch without the STAMPS flag. Every other failure is soft: all checks of the op's path run, at
constant gas, and the result is SIG_VALIDATION_FAILED. The same checks are exposed as checkUserOp(), which returns the
failure bits:
| Bit | Name | Set when |
|---|---|---|
| 1 << 0 | F_FEE_LOW | maxFeePerGas is below the epoch's minFeePerGas. |
| 1 << 1 | F_FEE_HIGH | maxFeePerGas is above the epoch's fee cap. |
| 1 << 2 | F_PRIORITY | maxPriorityFeePerGas is above the epoch's priority cap. |
| 1 << 3 | F_COST | maxCost (gasSum × maxFeePerGas) is above maxOpCost. |
| 1 << 4 | F_PM_GAS | A paymaster gas limit differs from the epoch's exact value for the op's postage (pmPostOpGasLimit mined, stampPostOpGasLimit prepaid). |
| 1 << 5 | F_NONCE_ORDER | Mined stamp: the 8 nonces are not strictly increasing. |
| 1 << 6 | F_POW | Mined stamp: a sub-solution hash is above the per-solution target. |
| 1 << 7 | F_COSIG | The relay co-signature is missing, wrong or high-s (for a prepaid stamp it must cover the nullifier too). |
| 1 << 8 | F_GAS_CAP | verificationGasLimit, callGasLimit or preVerificationGas is above the epoch's cap. |
| 1 << 9 | F_DELEGATE | The sender is not delegated to Simple7702Account v0.9. |
| 1 << 10 | F_NULLIFIER | Prepaid stamp: the nullifier is 0 (reserved). A spent stamp is refused by the relay (STAMP_SPENT), not by validation: the paymaster records nullifiers in postOp and never reads them while validating. |
Ops are limited to single calls from an allowlist of templates, enforced by the relay before it co-signs, for both postages:
| Template | Target | Call | SDK helper |
|---|---|---|---|
| transfer | each listed token: WETH, USDG, cbBTC and 195 Robinhood stock tokens | transfer(address,uint256) | transferCall(token, to, amount) |
| approve | each listed token | approve(address,uint256) | approveCall(token, spender, amount) |
| sendEth | a recipient without code, or an EOA delegated to Simple7702Account v0.9 | execute(to, value, 0x), value > 0 | sendEthCall(to, value) |
Epochs, difficulty and budget#
- Write-once epochs. A
roll()(any EOA, once the epoch is over) creates the next epoch. Nothing validation reads ever changes afterwards, so a simulated op can't be invalidated by paymaster state, and the bundler never has a reason to ban it. - Two lanes in one deposit. The EntryPoint deposit holds the stamp float (what sold stamps prepaid and have not spent
yet) and the free lane's share.
roll()tops up onlydeposit − float, and the free lane's budget never counts the float: free stamps can never spend what buyers paid for. - Step controller. At each roll, utilization
uof the free lane's budget sets the next difficulty: ×2 after two saturated epochs, ×1.25 above the 60% target, ×0.5 below half of it, ×1 otherwise, clamped to the configured range (6× on mainnet). - Budget = deposit share. The free lane is topped up to its epoch budget at most once per epoch, by a direct EOA roll,
and an immutable
MAX_DAILY_SPENDbounds the daily outflow. The relay paces co-signing of mined stamps across the epoch; prepaid stamps are admitted against the deposit's headroom only.