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.
Install#
$ npm i @paravion/sdk @paravion/miner viemThe 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.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.Get a stamp#
Buy a booklet of 1 to 100 stamps in ETH, from any wallet, even your main one (
buy.tsbelow), 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.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)import { createWalletClient, custom } from 'viem' import { robinhood, buyStamps, createRelayClient } from '@paravion/sdk' const relay = createRelayClient('https://relay-production-fb81.up.railway.app') const wallet = createWalletClient({ chain: robinhood, transport: custom(window.ethereum) }) const [buyer] = await wallet.getAddresses() // any wallet, even your main one const { stamps } = await buyStamps({ relay, count: 10, // 1 to 100 per booklet // PostOffice.buy(commitment, count) with count × price; the excess comes back pay: (call) => wallet.sendTransaction({ account: buyer, ...call }), // called before paying: the pending booklet holds the blinding inverses savePending: (booklet) => db.put('pending', booklet), onProgress: (p) => console.log(p.stage), // issuer, blinding, paying, paid, issuing, done }) await db.put('stamps', stamps) // bearer tokens: guard them like cashimport { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { createRelayClient, freshAccountState, listedTokens, sendPowUserOperation, 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 sent = await sendPowUserOperation({ relay, epochs, owner, state: freshAccountState(owner.address), call: transferCall(NVDA, to, 10n ** 18n), // 8 keccak sub-solutions in Web Workers; free stamps share each epoch's budget onProgress: (p) => p.stage === 'mining' && console.log(`${p.found}/8`), }) console.log(sent.receipt.success, sent.userOpHash)# The issuer, window and price booklets are bought under now (address-free) curl -s https://relay-production-fb81.up.railway.app -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"stamp_getIssuer","params":[]}' # Issue a paid booklet: the purchase tx, the metadata and the blinded messages it committed to curl -s https://relay-production-fb81.up.railway.app -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"stamp_issue","params":[{"orderTx":"0x<tx>","info":"<info>","blinded":["0x<256 bytes>"]}]}'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
statefor 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.
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
})| Stage | Fields | Meaning |
|---|---|---|
| epoch | attempt | Reading the current epoch (from the background poll, refreshed after 60 s). |
| waiting | reason, untilMs, seconds | Not enough time, budget or lane in this epoch: waiting for the next one (window, headroom, cost_cap, keeper, pacing, lane). |
| building | epochId, firstOp | Building the op from the gas profile, the PVG quote and the epoch's fees. |
| mining | found, solutions, hashes, hashrate, eta, etaP95, work, userOpHash | Mined stamp only: the 8 sub-solutions in Web Workers (signing runs in parallel). |
| signing | userOpHash | The account signs the UserOperation. |
| submitting | userOpHash, delaySec | Sending to the relay (eth_sendUserOperation, pow_sendUserOperation with a delay, or stamp_sendUserOperation). |
| held | userOpHash, forwardAt | The relay accepted the op and holds it for the delay you asked for. |
| submitted | userOpHash | Co-signed and forwarded to the bundler. |
| included | receipt, success | In a block. success mirrors UserOperationEvent.success. |
| failed | error, userOpHash, state | Final 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 { 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.
| code | What happened | What to do |
|---|---|---|
| RELAY_RPC | The 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_MISMATCH | The 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_EXPIRED | A stamp does not verify, or its validity has ended. | Drop it from your saved stamps and use another one. |
| STAMP_LANE_CLOSED | The current epoch does not accept prepaid stamps. | Wait for the next epoch, or mine a free stamp. |
| BOOKLET_MISMATCH | The blind signatures do not match the pending booklet. | Keep the pending booklet and ask the relay to issue again. |
| CARNET_LOCKED | An exported stamp file could not be decrypted. | Check the passphrase; the file is useless without it. |
| COST_CAP | Fees are above the paymaster's per-op cap this epoch. | The SDK waits for the next epoch by itself; surface the wait. |
| EPOCH_WAIT_TIMEOUT | No epoch had room for this op on this device within maxWaitMs. | Use more threads, a prepaid stamp, or retry later. |
| REMINE_LIMIT | The op was rejected again after two rebuilds. | Retry later; the epoch or quotes are moving fast. |
| HASH_MISMATCH | The relay computed a different userOpHash. | Check the relay URL and chain; never retry blindly. |
| DEPLOYMENT_MISSING | No paymaster address for this chain. | Wait for the deployment or pass your own addresses. |
| RECEIPT_TIMEOUT | No receipt before the op's validity window closed. | Refresh the account state before sending again. |
| RELAY_HTTP | Network error or timeout talking to the relay. | Retry with backoff. |
| ABORTED | Your AbortSignal fired. | Nothing was sent unless the stage was submitting or later. |