STAMP

DocsQuickstart

Send from an address that never held ETH.

Gas for fresh wallets. A fresh EIP-7702 address on Robinhood Chain sends its first operation with a stamp attached, and the Stamp paymaster pays the gas. Buy a booklet of stamps from any wallet (the relay signs them blind, so it cannot tell later which purchase a stamp came from), or mine a free stamp in your browser. No funding transaction ever links the two addresses.

  1. Install#

    shell
    $ npm i @paravion/sdk @paravion/miner viem

    The SDK builds the UserOperation itself (fixed gas profiles, EntryPoint v0.9, nonce key 0) on top of viem, blinds and unblinds stamps with @cloudflare/blindrsa-ts, and mines free stamps in Web Workers (WASM, with a JavaScript fallback) or Node worker threads through @paravion/miner.

  2. Point it at the relay#

    There is no project id: the relay URL is the only setting. This deployment's relay is https://relay-production-fb81.up.railway.app. It issues stamps, checks each operation, co-signs it and forwards it to its own bundler.

  3. Get a stamp#

    Buy a booklet of 1 to 100 stamps in ETH, from any wallet, even your main one (buy.ts below), and keep them safe. Or skip this step and mine a free stamp per operation (mined.ts): free stamps share a small budget per 15-minute epoch, so they can run out. Prepaid stamps explains the purchase, the blind signature and what the relay can and cannot see.

  4. Send the op#

    import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
    import {
      createRelayClient, freshAccountState, listedTokens,
      sendStampedUserOperation, startEpochPolling, transferCall,
    } from '@paravion/sdk'
    
    const relay = createRelayClient('https://relay-production-fb81.up.railway.app')
    const epochs = startEpochPolling(relay)      // fixed 60 s background poll
    const owner = privateKeyToAccount(generatePrivateKey()) // 0 ETH, never funded
    const NVDA = listedTokens(4663).find((t) => t.symbol === 'NVDA')!.address
    const stamp = saved.shift()!                 // bought days ago, not right now
    
    const { receipt, state } = await sendStampedUserOperation({
      relay, epochs, owner, stamp,
      state: freshAccountState(owner.address),   // first op: carries the EIP-7702 delegation
      call: transferCall(NVDA, to, 10n ** 18n),
    })
    console.log(receipt.success)                  // the paymaster cancelled the stamp (StampCancelled)
    Relay URL: https://relay-production-fb81.up.railway.app. The SDK builds every op itself (fixed gas profiles, EntryPoint v0.9, nonce key 0).

    The first op of a fresh key carries an EIP-7702 authorization for Simple7702Account v0.9. The bundler's own type-4 transaction applies it, so the key never needs ETH. Keep the returned state for the next op: the SDK never reads it from the chain before sending.

Show progress and let people stop#

A stamped send goes through the same stages as a mined one, without the mining. Mining is memoryless: every hash has the same odds, so show the found sub-solutions (x/8) and an honest estimate, never a fake percentage. Pass an AbortSignal for a Stop button.

progress.ts
const res = await sendPowUserOperation({
  relay, epochs, owner, state, call,
  delaySec: 120,              // optional: the relay holds the op 2 min
  signal: controller.signal,  // your Stop button
  onProgress: (p) => {
    if (p.stage === 'waiting') show(`Next epoch in ${p.seconds} s`)
    if (p.stage === 'mining') show(`${p.found}/8 · ~${Math.ceil(p.eta)} s`)
    if (p.stage === 'included') show(p.success ? 'Sponsored' : 'Reverted')
  },
  onState: (s) => save(s),    // persist the state as it advances
})
onProgress stages, in order
StageFieldsMeaning
epochattemptReading the current epoch (from the background poll, refreshed after 60 s).
waitingreason, untilMs, secondsNot enough time, budget or lane in this epoch: waiting for the next one (window, headroom, cost_cap, keeper, pacing, lane).
buildingepochId, firstOpBuilding the op from the gas profile, the PVG quote and the epoch's fees.
miningfound, solutions, hashes, hashrate, eta, etaP95, work, userOpHashMined stamp only: the 8 sub-solutions in Web Workers (signing runs in parallel).
signinguserOpHashThe account signs the UserOperation.
submittinguserOpHash, delaySecSending to the relay (eth_sendUserOperation, pow_sendUserOperation with a delay, or stamp_sendUserOperation).
helduserOpHash, forwardAtThe relay accepted the op and holds it for the delay you asked for.
submitteduserOpHashCo-signed and forwarded to the bundler.
includedreceipt, successIn a block. success mirrors UserOperationEvent.success.
failederror, userOpHash, stateFinal failure. The op was not sponsored; see the error codes below.

Imported keys#

A fresh key starts at freshAccountState(address) with no read at all. A key that already has history (an address that received tokens, say) needs its state once:

import.ts
import { loadAccountState } from '@paravion/sdk'

// An imported key may already have history: read its state once, through the relay only.
let state = await loadAccountState(relay, owner.address)

Handle failures#

A failed send never costs ETH. Either the op was not sponsored, and its stamp is not spent; or it was included and only the call reverted, and its stamp is cancelled like any other, since its gas was paid. The SDK rebuilds by itself, at most twice, when the epoch rolls or the bundler's quotes move.

SdkError codes worth handling
codeWhat happenedWhat to do
RELAY_RPCThe relay refused the op; RelayRpcError has reason, failMask and retryAt.Show the reason. PACING and BUDGET_EXHAUSTED carry retryAt; STAMP_SPENT means: use another stamp.
ISSUER_MISMATCHThe relay's stamp key or window is not the published one.Do not buy: stamps under an unpublished key could single you out.
STAMP_INVALID / STAMP_EXPIREDA stamp does not verify, or its validity has ended.Drop it from your saved stamps and use another one.
STAMP_LANE_CLOSEDThe current epoch does not accept prepaid stamps.Wait for the next epoch, or mine a free stamp.
BOOKLET_MISMATCHThe blind signatures do not match the pending booklet.Keep the pending booklet and ask the relay to issue again.
CARNET_LOCKEDAn exported stamp file could not be decrypted.Check the passphrase; the file is useless without it.
COST_CAPFees are above the paymaster's per-op cap this epoch.The SDK waits for the next epoch by itself; surface the wait.
EPOCH_WAIT_TIMEOUTNo epoch had room for this op on this device within maxWaitMs.Use more threads, a prepaid stamp, or retry later.
REMINE_LIMITThe op was rejected again after two rebuilds.Retry later; the epoch or quotes are moving fast.
HASH_MISMATCHThe relay computed a different userOpHash.Check the relay URL and chain; never retry blindly.
DEPLOYMENT_MISSINGNo paymaster address for this chain.Wait for the deployment or pass your own addresses.
RECEIPT_TIMEOUTNo receipt before the op's validity window closed.Refresh the account state before sending again.
RELAY_HTTPNetwork error or timeout talking to the relay.Retry with backoff.
ABORTEDYour AbortSignal fired.Nothing was sent unless the stage was submitting or later.

Next steps#