ZEAM :: Pass for agents
Overview
A Pass seller serves its tools on its own site:
- MCP at
<base>/mcp.tools/listis free. A paid call carries the x402 PaymentPayload as a JSON object in_meta["x402/payment"]. - HTTP at
POST <base>/v1/<tool>, with the x402PAYMENT-SIGNATUREheader (base64 of the PaymentPayload JSON).<base>/openapi.jsonlists the tools; itsinfo.descriptionstates the mode: paywall, gate or both.
No sign-up, no API key. To pay: hold USDC on Base and sign. Paying needs no ETH. A refund the seller sends needs no
ETH. selfSend, initiateWithdraw and finalizeWithdraw are transactions from your wallet and need ETH on Base for
gas. To pass a gate: sign with your key; nothing is paid and the key needs no funds.
Section 1
Pay with your own wallet
Hold USDC on Base. No approval.
The ZEAM :: x402 Bridge pays from a key in your environment. Set X402_MCP_URL to the seller; without it the Bridge
calls its default upstream.
export X402_PRIVATE_KEY=0x…
X402_MCP_URL=https://seller.example/agents/mcp npx -y @zeam-labs/x402-mcp-bridge --tools
X402_MCP_URL=https://seller.example/agents/mcp npx -y @zeam-labs/x402-mcp-bridge --call add '{"a":1,"b":2}'
Without --call or --tools the Bridge is an MCP stdio server.
- Use the eip3009 row. For USDC, the
accepts[]row whoseextrahasnameandversionand noassetTransferMethodis EIP-3009: signed offline, no approval. The"assetTransferMethod": "permit2"row needs a one-time on-chain Permit2 approval, paid in ETH. - Deposit = price × multiplier. The first call on a channel deposits; later calls spend the deposit. Multiplier:
5 for the stock x402 SDK;
X402_DEPOSIT_MULTIPLIERfor the Bridge (default 40, minimum 3). The deposit must reach the floor in the 402'sdepositline; below it the answer isfunding_requires_open_feewithneededMicroUSD. Raise the multiplier until price × multiplier ≥ floor. - A failed call is not charged. Arguments are checked first; a failed tool releases the hold. A call the tool
completes is charged, even when the connection drops before the answer arrives. Settlement comes
back in the
PAYMENT-RESPONSEheader (MCP:_meta["x402/payment-response"]).
A connector link from a pass: add it as a custom MCP connector. It pays the pass's seller; its free pass_balance
tool states the balance. Section 6 funds one.
Section 2
Read a seller's price
Call a tool without paying:
- HTTP:
402. ThePAYMENT-REQUIREDheader is base64 JSON of the terms; the body carries the same. - MCP:
isError: true;structuredContentis the terms.
accepts[].amount is the price per call in micro-USDC: "20000" is $0.02. pricing states it in dollars
("$0.02 per call. A failed call is not charged."). deposit states the minimum first deposit
("First call on a channel deposits at least $0.03. Later calls spend it. The unspent balance is refundable.").
refund states the refund rule (section 5). A gate's amount is "0".
Each tool has its own price. prices in the 402 tags every tool; tools/list tags each tool in
_meta["zeam-pass/price"]; openapi.json in each operation's x-price:
| Tag | Meaning |
|---|---|
{"usd":"0.02","per":"call"} |
$0.02 per call |
{"usd":"0.0001","per":"unit","upTo":"0.05"} |
usage: sign $0.05; charged $0.0001 × the units the tool reports, at most $0.05 |
{"usd":"0","per":"call","free":true,"perHour":60} |
free: no payment, no admission; 60 calls an hour per address, then 429 free_limit |
{"per":"call","varies":true} |
depends on the arguments; the call's 402 states it |
{"usd":"0.00025","per":"block","blockMs":250,"maxBlocks":14400} |
buy_time: $0.00025 per block of 250 ms of line time, 1 to maxBlocks blocks a call (see Line time) |
{"per":"time","blockUSD":"0.00025","blockMs":250,"callUSD":"0.01","callMs":10000} |
a time tool: on a line it burns line time; without one, $0.01 a call for up to 10,000 ms |
Usage: sign accepts[].amount, the reserve. PAYMENT-RESPONSE extra states chargedAmount (used) and
reservedAmount on every settled call, equal when the whole reserve was charged; the rest stays in your channel. Sign the next voucher on extra.channelState.chargedCumulativeAmount
(stock x402 clients do).
Line time
A seller that sells time lists buy_time and line, tags each time tool
{"per":"time","blockUSD":"0.00025","blockMs":250,"callUSD":"0.01","callMs":10000}, and states the rule in the
402's time line.
- Buy. Call
buy_time{"blocks": N}and pay N ×blockUSD(its tag isper: "block"). The time goes to the paying channel with the payment: time that cannot be recorded is not charged (500tool_failed), and a payment that does not settle credits no time. The answer statesboughtMsandmsRemaining. - Open a line.
POST <base>/line{"op":"open","channelId"}answers{nonce, sign}. Signsign(EIP-191) with the channel's payer key (orpayerAuthorizer), thenPOST{"op":"prove","channelId","nonce","signature"}: the answer carriescredential. A nonce is one attempt, valid 300 s. MCP: the freelinetool, same arguments. - Call. Send the credential as the
x-lineheader (MCP:_meta["zeam-pass/line"], orx-lineon the MCP request). No payment per call. The answer carriesx-pass-ms-remainingandx-pass-ms-elapsed(MCP:_meta["zeam-pass/meter"]). - Burn. Time burns while a call runs on the line; calls at the same time burn one clock. A call stops when the
time runs out: 402
out_of_time, its time spent. - Meter.
{"op":"off"}: no new calls on the line; a running call burns to its end; a call sent meanwhile gets 402meter_offwithmsRemaining, the time the line still holds.{"op":"on"},{"op":"status"},{"op":"close"}, each with the credential. Over HTTP the line isPOST <base>/line, orPOST <base>/v1/linewith the same body; both answer a failed step with its status and{op: "line_failed", code, why}. - Leave. A refund (section 5) returns the unburned time with the balance (
timeReturnedMs) and closes the line.
Without a line a time tool is a paid call at callUSD, for up to callMs; a call past it: 402 out_of_time, not
charged.
Section 3
Pass a gate
A gate admits listed keys. Each call is signed with your key as a zero-value payment: nothing is paid, the key needs no funds or ETH, nothing goes on chain.
Make a key once. Keep the private key; give the seller its address.
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
const key = generatePrivateKey()
privateKeyToAccount(key).address
from zeam_pass import key_address, new_key
key = new_key()
key_address(key)
Sign every call.
- Call without a proof. The 402 has one
exactrequirement:amount: "0",payTothe seller's wallet,realm: "ZEAM Pass", andhow. - Sign it with any x402 client that signs
exact(an EIP-3009TransferWithAuthorizationof value 0, never sent to the chain). Send it asPAYMENT-SIGNATURE(MCP:_meta["x402/payment"]). - Use a new nonce per call. A signature is accepted once (again:
replayed) and is valid 300 seconds.
Node, with the x402 client:
import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { x402Client, wrapFetchWithPayment } from '@x402/fetch'
import { toClientEvmSigner } from '@x402/evm'
import { ExactEvmScheme } from '@x402/evm/exact/client'
const signer = toClientEvmSigner(privateKeyToAccount(process.env.AGENT_KEY), createPublicClient({ chain: base, transport: http() }))
const f = wrapFetchWithPayment(fetch, new x402Client().register('eip155:8453', new ExactEvmScheme(signer)))
const r = await f('https://seller.example/agents/v1/search', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"q":"solar"}' })
Python, with zeam-pass (gate_proof signs the zero-value row of the terms):
import json, os, urllib.error, urllib.request
from zeam_pass import gate_proof
def call(url, body, grant=None):
def post(headers):
req = urllib.request.Request(url, json.dumps(body).encode(), {"content-type": "application/json", **headers}, method="POST")
return urllib.request.urlopen(req)
try:
return post({})
except urllib.error.HTTPError as e:
if e.code != 402:
raise
proof = gate_proof(os.environ["AGENT_KEY"], e.headers["PAYMENT-REQUIRED"])
return post({"payment-signature": proof, **({"x-grant": grant} if grant else {})})
Getting admitted. A key not on the list gets 403 refused with a how. The seller's contact (an https://
page or a mailto: address), when set, is in that answer, in every 402 and in openapi.json info.contact. A
WordPress seller's default is its site address. Send the seller your key's address, or ask for a grant.
Grants. A listed key can admit your key with a grant: your key's address, one tool or all (*), an end time.
Send x-grant: <base64url of JSON {delegate, scope, until, signature}> on every call, and sign the call with your own
key as above. The grant works only for the named key. It ends at until, or when the seller removes the signing key.
signature is the admitting key's EIP-191 signature of:
ZEAM Pass grant
delegate: <your key's address, lowercase>
scope: <tool, or *>
until: <ISO time>
The admitting side signs with signGrant (Node, @zeam-labs/pass) or sign_grant (Python, zeam-pass):
Selling guide. x-grant is an HTTP header, on POST <base>/mcp too, where it covers
every tool call in the request; never in _meta. An answer through a grant carries
X-Pass-Granted-By: <admitting key's address, lowercase> (MCP: _meta["zeam-pass/granted-by"]). With
@x402/fetch, add 'x-grant': grant to the request headers.
Both mode. The key must be admitted (directly or by x-grant) and the call paid from it. The unpaid 402 has an
admission field and batch-settlement rows in accepts; a gate has an exact row. With @x402/fetch, register
BatchSettlementEvmScheme beside ExactEvmScheme; its signer reads your channel on Base and sends no transaction.
import { BatchSettlementEvmScheme } from '@x402/evm/batch-settlement/client'
const client = new x402Client()
.register('eip155:8453', new BatchSettlementEvmScheme(signer))
.register('eip155:8453', new ExactEvmScheme(signer))
const pay = wrapFetchWithPayment(fetch, client)
const r = await pay('https://seller.example/agents/v1/search', { method: 'POST', headers: { 'content-type': 'application/json', 'x-grant': grant }, body: '{"q":"solar"}' })
r.headers.get('x-pass-granted-by')
The Bridge sends X402_GRANT as x-grant on every request, over MCP and HTTP, from 3.0.0.
seller/test/grant-client.test.mjs runs the client above against a reference seller in both and gate mode.
Section 4
Errors
HTTP answers carry error and message, and some carry code, reason, how and contact. An MCP isError
result carries the same fields in structuredContent, merged with the x402 terms when the answer has them. A payment
that does not verify: error is the x402 code (invalid_batch_settlement_evm_…), the same as in the
PAYMENT-REQUIRED terms, and code is payment_invalid; resync the channel from accepts. The refund route answers
op: "refund_failed" with code and why, op: "refund_quote" when there is something to sign, op: "refunded"
when it was sent, or op: "refund_signed" for selfSend (section 5).
| code | status | meaning | next step |
|---|---|---|---|
invalid_arguments |
400 | the arguments do not match the tool's inputSchema, or the body is not JSON; nothing held |
fix the arguments from tools/list |
unknown_tool |
404 | no tool by that name; HTTP lists the tools | use a name from tools/list |
tool_failed |
500 | the tool failed after the hold, its result is not valid JSON, or a usage tool reported no units; hold released, nothing charged | retry later, or with other arguments |
not_found |
404 | (WordPress) no published post with that id; nothing charged | find an id with search_posts |
internal_error |
500 | the seller's server failed | retry later |
price_invalid |
500 | the seller cannot price this call; nothing held | retry later, or with other arguments |
line_unknown |
403 | the x-line credential names no open line (closed, or refunded) |
open a line |
meter_off |
402 | the line's meter is off; msRemaining is the time the line still holds |
POST <base>/line {"op":"on"} |
out_of_time |
402 | the line has no time left, or the call ran past its time; that time is spent. Without a line: past callMs, not charged |
buy_time, then call again |
no_challenge, bad_signature |
400 | (line route) the nonce is used, expired or unknown, or the signature does not recover | {"op":"open"} again and sign sign |
free_limit |
429 | over the free tool's calls an hour per address (the seller's count survives its restarts; callers the seller sees no address for share one count); retry_after_seconds |
retry after retry_after_seconds |
body_too_large |
413 | (Python) the body is over 1 MB | send a smaller body |
not_ready |
503 | (WordPress) the seller's setup is incomplete | retry later |
payment_unavailable |
503 | the payment check or settlement failed; nothing charged | retry |
(none, with accepts) |
402 | payment required; for a gate, a signed proof of key | pay one row of accepts (section 1), or sign the zero-value row (section 3) |
bad_payment_header |
400 | the payment does not decode | send base64 of the PaymentPayload JSON (MCP: the object) |
bad_payment_type |
400 | the payment is neither a voucher nor a deposit | pay with a voucher or a deposit |
no_matching_requirement |
402 | the payment matches no row of accepts |
sign a row as given |
an x402 code, with code: "payment_invalid" |
402 | the payment did not verify (signature, channel, balance or cumulative amount); error and reason name it |
re-sign from the returned accepts, which carry the channel's current state |
price_changed |
402 | the price differs from the one signed | pay the accepts quote |
funding_requires_open_fee |
402 | the deposit is under the floor | deposit at least neededMicroUSD |
channel_leaving |
402 | the channel is withdrawing | open a new channel |
refund_outstanding |
402 | the channel has an unsent signed refund | send it (the transaction in the answer), or open a new channel |
settle_failed |
402 | the payment did not settle; not delivered, nothing charged | call again with a new payment |
no_payment |
402 | (gate) no PAYMENT-SIGNATURE, or not base64 JSON of an x402 payment |
sign the zero-amount requirement with your key |
bad_payment |
402 | (gate) no authorization in the payment | send an exact EIP-3009 payment |
invalid_payment |
402 | (gate) the signature, amount, recipient or time window is wrong; message names it |
re-sign as given: value 0, its payTo, a future validBefore |
replayed |
402 | (gate) this authorization was already used | sign again with a new nonce |
bad_grant |
402 | (gate and both) x-grant is malformed or expired, its signer is not admitted, or it names another delegate than the key that signed the call; message names it; nothing held |
get a new grant from an admitted key, naming the key you sign with |
refused |
403 | your key is not admitted, or your grant's scope is another tool; how says how to get in; contact, when set, says where |
ask the seller to admit your key's address, or ask an admitted key for a grant; do not retry as is |
gate_credit_exhausted |
503 | this gate has no checks left this month | retry later |
invalid_json, invalid_request, bad_request |
400 | (refund route) the body is not a JSON object | send {channelId, issued, signature} |
not_a_paywall |
404 | (refund route) this seller takes no payments | none |
unknown_channel |
404 | (refund route) no channel with that id here; a channel with no calls is found by its channelConfig |
check the id, or send channelConfig with the proof |
channel_config_mismatch |
400 | (refund route) the channelConfig is not this seller's channel (checked whenever sent); mismatch: channelId (hashes to another id; why names both), receiver or receiverAuthorizer (another seller), token (not accepted here), fields (not the 7 channel fields), or balance (the channel holds nothing on chain) |
send the config as deposited, to the seller it names |
proof_invalid |
400 | (refund route) the signed proof does not verify or is stale; sign shows the text |
sign that text within 5 minutes |
not_the_payer |
403 | (refund route) the proof is not signed by the channel's payer | sign with the payer's key |
request_open |
409 | (refund route) a paid call is open on the channel | retry after retry_after_seconds |
nothing_to_return |
409 | (refund route) the balance is 0, or refund gas ≥ balance; returnedMicroUSD 0 |
read leftMicroUSD and gasMicroUSD |
chain_unreadable |
409 | (refund route) the chain read failed | retry after retry_after_seconds |
gas_unpriced |
503 | (refund route) refund gas cannot be priced, or the relay is unreachable | retry after retry_after_seconds, or send "selfSend": true |
refund_unavailable |
503 | (refund route) the seller's refund handler failed | retry later |
refund_error |
409 | (refund route) sending failed, nothing sent; carries leftMicroUSD and the gas figures; the hour is not used |
retry after retry_after_seconds |
refund_too_soon |
409 | (refund route) the channel had a refund in the last hour | retry after retry_after_seconds, or send "selfSend": true |
gas_payment_needed |
409 | (refund route, op: "refund_quote") channel fees (feeMicroUSD) < deposit and refund gas (coverMicroUSD); gasMicroUSD is the quoted refund gas, L1 fee included (l1FeeWei), 15% margin, priced by the relay (quotedBy: "relay") or the seller (quotedBy: "engine"). With requoted: true and sendCostMicroUSD: the send cost rose past your payment; nothing sent; a new quote |
sign sign (EIP-712) and POST again with gasPayment: {authorization, signature} within 5 minutes |
MCP transport errors are JSON-RPC: -32700 the body is not JSON, -32600 not a JSON-RPC request, -32601 no such
method, -32602 bad params, -32603 the server failed.
Section 5
Refunds
POST <base>/refund (the 402's refund line names the URL) with {channelId, issued, signature}. signature is
the payer's (or payerAuthorizer's) EIP-191 signature of:
ZEAM Pass refund
channel: <channel id, lowercase>
issued: <ISO time, within 5 minutes>
A channel with no calls: add channelConfig, the 7 channel fields as deposited. The seller checks that it hashes to
channelId, names this seller as receiver, and holds a balance on chain. A config that fails a check, for any
channel, is 400 channel_config_mismatch with mismatch; nothing is sent.
The rule: ZEAM pays the gas when the channel's fees (its spend times the fee address's share of the seller's split:
9.99% for most sellers, who keep 90.01%; 100% for one whose split pays the fee address everything, like Prism; the
402's refund names it) ≥ its deposit and refund gas × 1.15, at the current gas and ETH price. feeMicroUSD
rounds down, coverMicroUSD rounds up; free when feeMicroUSD >= coverMicroUSD. The seller, the relay and the 402
apply the same comparison. An unsettled call rides the refund as a claim and adds its gas to both.
- Free:
op: "refunded",returnedMicroUSD= the balance,gasMicroUSD: 0,transaction. - Not free: 409
op: "refund_quote",code: "gas_payment_needed", withfeeMicroUSD,coverMicroUSD,gasMicroUSD(the quote),gasUnits(refund + riding claim + your payment),gasUnitsWithMargin,l1FeeWei,gasPriceWei,ethUSD,marginPercent,quotedBy(relay, orenginewhen the relay did not answer),returnedMicroUSD,payTo(the relay's gas wallet, its/healthsender),authorizationandsign(EIP-712 typed data of an EIP-3009TransferWithAuthorizationofgasMicroUSDUSDC from your payer topayTo). Signsignwith the payer (viem:account.signTypedData(answer.sign)) and POST again with a newissuedandsignature, plusgasPayment: {authorization, signature}. One transaction sends the refund, then the gas payment; you need no USDC or ETH beforehand. Answer:op: "refunded",returnedMicroUSD(out of the escrow),gasMicroUSD(your payment); net =returnedMicroUSD - gasMicroUSD. - Refund gas ≥ balance: 409
nothing_to_return;leftMicroUSDstays in the channel,returnedMicroUSD0. "selfSend": true: 200op: "refund_signed",microUSD(the full balance),transaction({chainId, to, data, value}, a signed refund you send from any wallet with ETH on Base for gas),why, andtimeReturnedMswhen line time was returned. Asked again before it is sent: the sametransaction.- No answer from the seller:
initiateWithdrawfrom the payer's wallet, thenfinalizeWithdrawafter the channel'swithdrawDelay(it pays the payer), each with ETH on Base for gas.
1 refund per channel per hour; sooner: refund_too_soon with retry_after_seconds.
The quote. The seller hands its signed refund to the relay. The relay prices it: the gas the refund uses (eth_simulateV1 from its
sender; eth_estimateGas where the RPC lacks it), + the gas a payment adds (median of its paid refunds), + Base's L1 data fee, at the current gas and
ETH price, + 15%. On the relay's paid refunds the quote is 1.14 to 1.18 × the send cost. Relay unreachable: the
seller prices from the same measurements (quotedBy: "engine"). At send time the relay estimates the transaction and
its L1 fee at current prices, and sends when your payment covers that cost. If the cost rose past your payment,
nothing is sent: 409 refund_quote with requoted: true, sendCostMicroUSD, and a new authorization and sign
for that cost + 15%. Sign and POST again. The signature is valid 5 minutes.
Check the price. The relay answers anyone, free, with no key.
GET https://api.zeampass.com/relay/quoteprices by shape.?claim=1: an unsettled call's claim rides the refund.?payment=0: a refund alone (ZEAM pays the gas). Default: refund + your gas payment, no claim.POST https://api.zeampass.com/relay/quotewith{"call": {"to": <escrow>, "data": <the refund>}, "payment": true}prices one refund:refundWithSignature, or the escrowmulticallof claim then refund (the calldata of aselfSendanswer).
{"op": "refund_gas_quote", "shape": {"claim": false, "payment": true}, "gasUnits": 155600, "gasFrom": "measured",
"gasPriceWei": "6000000", "l1FeeWei": "80000000000", "ethUSD": 4000, "costMicroUSD": 4055, "marginPercent": 15,
"quoteMicroUSD": 4663, "payTo": "0xB3ED726D24AF7C3ffbf04219A78506170155641b",
"measured": {"paidRefunds": 12, "paidRefundsWithClaim": 4, "calibrated": "2026-09-29"}}
costMicroUSD: the send cost now. quoteMicroUSD: cost + margin, the figure a seller quotes. gasFrom: simulated (the posted refund run
against the chain), estimated (the posted refund, gas estimate), measured (median of the relay's paid refunds of
that shape) or calibrated (starting units, before the relay's first send of that shape). Unreadable body: 400. Chain unreadable: 503 with
retry_after_seconds.
Ordering. The payment is a TransferWithAuthorization, not a ReceiveWithAuthorization:
receiveWithAuthorization requires the caller to be the payee, and the caller USDC sees is Multicall3. The relay
enforces the order: the payment is sent after the refund, in the same transaction, and both land or neither lands. It
never sends the payment alone or inside an escrow call (seller/test/relay.test.mjs). The seller hands the relay the
refund and payment as one Multicall3 bundle, refund first, neither leg allowed to fail. The relay sends the bundle as
is, or nested in a larger Multicall3 with other buyers' calls, where each bundle can fail alone: another buyer's
failure does not undo yours, and a failing bundle undoes its own refund and payment. Anyone holding your signed
payment can submit it alone to USDC: it pays only the relay's gas wallet, only the quoted amount, within 5 minutes,
and your refund is then quoted again. Sign it only to post it, over HTTPS.
Section 6
Fund a pass for any Pass seller
A pass is an allowance at one seller, for another agent or a person's chatbot: a channel with your wallet as payer, a
pass key as spender, the seller as receiver. It pays that seller up to what you load, or returns the balance to your
wallet. The page https://zeampass.com/pass runs these steps in a browser; a seller links to it with
?seller=<its MCP URL>. The buyer API is https://api.zeampass.com/pass (every path below is under it); connector
links are https://api.zeampass.com/c/<token>/mcp.
POST https://api.zeampass.com/pass/quote {"seller": "<the seller's MCP URL or HTTP base>"}answersseller: the terms the pass is locked to (receiver, its split;receiverAuthorizer;token;withdrawDelay;refund;admission, for a seller with an access list;contact, when set) and its prices:pricing(a sentence: the per-call range, line time apart),priceMicroandpriceTool(its cheapest per-call tool),prices(each tool's tag:{tool, usd, per: "call"},{tool, usd, per: "unit", upTo},{tool, per: "time", ...},{tool, free: true},{tool, varies: true}) andtime({tool, usd, blockMs, maxBlocks}when it sells line time). It creates nothing.- Derive the pass key from your wallet: sign
passMessage(receiver)(buyer/src/wallet.mjs, EIP-191); passnispassAt(keccak256(signature), n), a key and a channel salt. The token is the key's 32 bytes, base64url (tokenOfinbuyer/src/passes.mjs). POST /issue {"seller": …, "token": …}answerspassAddress,connectorUrlandpage.409 pass_in_use: passnis funded; usen + 1. Withouttokenthe service makes a random key and returns it once; it stores no key. A seller withadmissionmust admit the pass address: add"grant", anx-grantvalue with delegate =passAddress, signed by an admitted key (section 3). No grant:400 grant_required("this seller admits only listed keys. Get a grant for this pass first.") withpassAddress; no pass is made. A grant for another key:400 grant_wrong_delegatewithpassAddressanddelegate. Not anx-grantvalue:400 grant_invalidwithpassAddress. The service then asks the seller whether it admits the grant (a call carrying the grant, for the pass, that pays nothing): not admitted,400 grant_not_admittedwith the seller's reason inmessage; the seller could not be asked,502 grant_unchecked; no pass is made. All carry the seller'scontactwhen set./linkanswers the same.POST /c/<token>/prepare {"payer": <your wallet>, "amount": "1.00", "salt": <the pass's salt>}answerstypedData: an EIP-3009ReceiveWithAuthorizationofamountfrom your wallet. Sign it (no gas), thenPOST /c/<token>/depositwith the prepare answer plussignature. The service sends the deposit and pays the gas, only into a channel to this pass's seller, in its asset.POST /c/<token>/link {"payer": <your wallet>, "salt": …}checks the channel on chain. TheconnectorUrlthen pays that seller.GET /c/<token>/status:loaded(all deposits;fundedis the same figure),spent,returned(back to your wallet, net of gas),gas_paid(quoted refund gas you paid in USDC) andremaining, with eachtransactioninreturns(usdnet to your wallet,gas_usdpaid out of it).loaded = spent + returned + gas_paid + remaining. The connector's freepass_balancetool (loaded_usd,spent_usd,returned_usd,gas_paid_usd,remaining_usd) and the pass page show the same five.POST /c/<token>/refundreturns the balance: it calls the seller's refund route with the channel config, so a pass with no calls is refunded too.- Seller quotes gas:
409 needsSignature. Signsignwith your wallet and POST{"gasPayment": {authorization, signature}}. A quote changes nothing; the pass keeps paying until a refund is sent or a withdrawal starts. A requote (requoted: true): nothing sent, no withdrawal; sign the new one. - Seller says retry (any answer with
retry_after_seconds, such asrefund_too_soon):409with the seller'scode,message,retry_after_secondsand aRetry-Afterheader; nothing starts. - Seller refuses (no
retry_after_seconds): the service starts the escrow withdrawal. It pays your wallet afterwithdrawDelay; the service then finalizes it and pays the gas, with a finalize the pass key signed at the start (it pays onlypayer). Asked again meanwhile:already: truewithreadyAt. A gas payment posted meanwhile:409 refund_not_sentwithreadyAt. A withdrawal that cannot start:withdrawal_gas_short,withdrawal_no_gas,nothing_leftorwithdrawal_failed, withmessage, the chain error indetailand the seller's answer inseller.POST /c/<token>/finalizefinalizes a due withdrawal now. - A pass sent home (refund sent, or withdrawal started) pays nothing and takes no top-up:
prepare,depositandlinkanswer409 pass_sent_home. Fund a new pass.
- Seller quotes gas:
The link is the key: whoever holds it can spend the pass at its seller, nothing else. The service stores no key and no secret that derives one.