API Reference
Robyn AnyGas is gasless, cross-chain relay infrastructure for AI agents and onchain apps. A relayer fronts the network gas and is repaid from the token being moved, so one signed intent transacts across 22 EVM chains, Stellar, Solana, native Bitcoin and Tron — 26 nodes, 500 directed routes — with no native gas balance held anywhere.
Every endpoint below was verified against the live API before publishing. The base URL is https://api.anygas.xyz/svc (equivalently https://anygas.xyz/svc). Prefer not to hand-roll HTTP? The MCP server, JS SDK and Python package wrap all of this. The raw machine spec is at /openapi.json.
60-second start
Three calls, runnable from this page against production. No key, no funds, no install.
See the mesh — every chain and rail that is live right now, with chain ids you will use next.
curl -s https://anygas.xyz/svc/api/route/chains
Get a real quote — 25 USDC Base → Arbitrum, best rail wins. amount is source-token base units (25000000 = 25 USDC). Same fields as a JSON POST body.
curl -s "https://anygas.xyz/svc/api/route/quote?fromChain=8453&toChain=42161&fromToken=USDC&toToken=USDC&amount=25000000"Rehearse an execute with zero funds — the header x-anygas-sandbox: 1 runs the full execute → status → DONE flow (~40 s) without moving anything. Drop the header when you are ready to go live.
curl -s -X POST https://anygas.xyz/svc/api/route/execute \ -H 'content-type: application/json' -H 'x-anygas-sandbox: 1' \ -d '{"fromChain":8453,"toChain":42161,"fromToken":"USDC","toToken":"USDC","amount":"25000000","fromAddress":"0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2","toAddress":"0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2"}' # then watch it settle (BRIDGING → DONE, no funds moved): curl -s "https://anygas.xyz/svc/api/route/status?id=<id from the response>"
That is the whole loop. Pick a surface below — MCP, JavaScript, Python or plain HTTP — every one talks to this same gateway; /api/route/quote and /api/route/execute document every field.
Quickstart
Pick the surface that matches how your agent runs. All four talk to the same live gateway and default to it with zero config.
MCP server (Claude, Cursor, any MCP client)
Run it locally with npx, or point at the hosted, read-only streamable-HTTP endpoint — no install, no key.
{
"mcpServers": {
"robyn": { "command": "npx", "args": ["anygas-mcp"] }
}
}
# or hosted (zero install, public, read-only):
# https://api.anygas.xyz/mcp — streamable HTTP
# tools: robyn_mesh, robyn_quote, robyn_cross_chain, robyn_route_statusJavaScript / TypeScript
// npm i anygas-agent-kit (MIT · also on JSR as @anygas/agent-kit) import { RobynAgent } from 'anygas-agent-kit'; const robyn = new RobynAgent(); // zero-config → this gateway const quote = await robyn.route({ // gasless cross-chain quote fromChain: 42161, toChain: 8453, fromToken: 'USDC', toToken: 'USDC', amount: '5000000' });
Python
# pip install anygas from anygas import Robyn robyn = Robyn() # zero-config → this gateway mesh = robyn.chains() # the live route graph quote = robyn.route(fromChain=42161, toChain="solana", fromToken="USDC", toToken="USDC", amount="50000000")
First call — a quote with plain curl
No SDK, no key. Ask what a cross-chain move would deliver:
curl -s https://api.anygas.xyz/svc/api/route/compare \ -G --data-urlencode "fromChain=42161" \ --data-urlencode "toChain=solana" \ --data-urlencode "fromToken=USDC" \ --data-urlencode "toToken=USDC" \ --data-urlencode "amount=50000000"
execute → status → DONE flow with zero funds and no funded relayer: add the header x-anygas-sandbox: 1 to POST /api/route/execute. You get a real-shaped response and a sbx_… id that progresses BRIDGING → DONE in ~40s. See Sandbox mode.Start here — what problem are you solving?
This reference has grown a lot. Rather than reading it end to end, jump to the thing you are actually trying to do.
MOVING VALUE I want to move tokens without holding gas ............ /api/route/quote → /execute I want to check it will work before I pay ............ /api/preflight/full I have a multi-step workflow ......................... /api/route/plan I want the price locked to what I was quoted ......... attestation + minOut on execute KNOWING WHAT YOU ARE GETTING Is this actually a good price? ....................... /api/route/why What would make it cheaper? .......................... /api/route/improve How long will it really take? ........................ /api/delivery/stats Can you serve this chain right now? .................. /api/capability/snapshot PROVING WHAT HAPPENED Did it really arrive? (checked on-chain) ............. /api/outcome/verify Where did every unit of the fee go? .................. /api/forensics/fee Give me one signed artefact for all of it ............ /api/evidence/bundle Prove to my principal what I spent ................... /api/statement/spend Prove I was refused, not that I failed ............... /api/refusal/verify SPENDING ON SOMEONE ELSE’S BEHALF Bound what my agent can spend ........................ /api/mandate/prepare Require my approval for large spends ................. cosignAbove on the mandate Withdraw a budget immediately ........................ /api/mandate/revoke Ask another agent for money, verifiably .............. /api/payreq/prepare BUILDING AND MAINTAINING Test my error handling against real errors ........... x-anygas-rehearse What changed since I integrated? ..................... /api/changelog Use it from Claude or Cursor ......................... hosted MCP
Everything above is read-only unless it moves funds. Nothing here needs an API key.
Conventions
- Base URL —
https://api.anygas.xyz/svc. The gateway strips/svcand proxies to the service. All paths below are shown as/api/…; prefix them with/svcover HTTP. - Amounts are always strings in the token's smallest units (e.g. USDC 6dp →
"5000000"= 5 USDC; BTC in sats). - Chain ids — EVM chains are numeric ids; non-EVM nodes are the literal strings
"solana","stellar","bitcoin"(and"sui"for quote-only comparison). - Tokens — a symbol like
USDC/USDT/BTC, or a token contract address. Omit for the chain's native asset. - Routing fee — Robyn withholds 0.25% (
25bps) from the token in flight; it is never taken in native gas. Every quote returnsrobynRouteFeeBps. - Optional API key — the API is keyless. Passing
x-anygas-key: <key>tags requests for usage visibility (see keys). It is never required. - Rate limits are per-IP, per-endpoint (shown on each endpoint). Over the limit →
429 {"error":"rate limited"}. - Errors —
4xx/5xxwith{"error":"…"}. Reads degrade gracefully rather than throw.
Preflight — check before you spend
POST /api/route/preflight takes the same body you would execute and returns decision: "GO" | "NO_GO" with typed blockers (each carrying a fix) and advisory warnings. It answers, for free, the questions that otherwise cost gas to discover: a fixed corridor fee larger than the amount so nothing would arrive, a recipient in the wrong address family (unrecoverable once broadcast), a corridor that can be priced but not settled, a size above the instant lane that would silently pay a dearer bridge, and relayer-side readiness. Branch on blockers[].code — never on the wording. Add ?deep=1 to also read relayer balance and Permit2 allowance.
curl -s https://api.anygas.xyz/svc/api/route/preflight \ -H 'content-type: application/json' \ -d '{"fromChain":42161,"toChain":8453,"fromToken":"USDC","toToken":"USDC", "amount":"5000000","fromAddress":"0xYourAddress","toAddress":"0xRecipient"}'
Rehearsing an execute safely. Send "dryRun": true (or "live": false) to /api/route/execute for a rail-independent dry run — it returns {status:"DRY", broadcast:false, wouldSend} and broadcasts nothing on every corridor. Do not rely on simply omitting live: that means dry on the float lane and the non-EVM legs, but the EVM↔EVM path ignores it and broadcasts.
Free & near-free micro-transfers
Small cross-chain transfers normally pay fees out of all proportion to the amount. On Robyn's own float lane there is no bridge: we pay the recipient from our inventory on the destination chain, one transfer, about one block. Transfers of $0.10–$25 to a low-cost chain are free for holders of a recognised NFT (Bored Ape Yacht Club, DEGEN TOONZ, The Currency by Damien Hirst, or a Robyn collection) and cost a fraction of a cent otherwise — never more than the standard 8 bps. Members also pay 15 bps routing instead of 25 and 10% gas markup instead of 25%.
Always pass fromAddress on a quote: it is how membership is recognised, and without it you are quoted the non-member rate. Check any address with GET /api/membership/status?address=0x…, see live tiers and per-chain cost floors at GET /api/floatlane/info, and every settled payout — checked on-chain against the amount quoted, to the unit — is public at /receipts.
Netting accounts — genuinely free transfers (opt-in, custodial)
Every other rail here is non-custodial. This one is not, which is why it is separate, opt-in, and described bluntly.
A netting account holds a USDC balance with Robyn. A transfer between two netting accounts is a book entry: no chain is touched, no gas is spent, no bridge is crossed, and there is no transaction hash because there is no transaction. That makes transfers free at any size — including amounts far below the per-chain payout cost that bounds even the float lane.
What you are accepting. Your balance is an obligation Robyn owes you, not tokens you control on-chain. You cannot move it with your own key until you withdraw. If Robyn fails, is compromised, or is compelled by law, you could lose it. There is no insurance. Keep only working amounts here; for anything you are not actively spending, the non-custodial rails are the safer choice.
How consent works. GET /api/netting/terms?address=0x… returns the exact text
you must sign with your own key. POST /api/netting/enroll takes {address, signature}
and verifies the signature recovers to that address — nobody can enrol an account they do not control.
Both parties to an internal transfer must be enrolled: we will not take custody of funds for a recipient who
never agreed to it. To pay someone who is not enrolled, use the normal non-custodial
/api/route/execute.
Solvency is published, not asserted. GET /api/netting/reserves reports total
liabilities against the real relayer float backing them, per chain, so anyone can check the books. Deposits
are refused if they would push custodied liabilities above a safe share of that float. Caps are deliberately
small while this is young.
# open an account (you sign the terms with your own key)
curl -s "https://anygas.xyz/svc/api/netting/terms?address=$ADDR"
curl -s -X POST https://anygas.xyz/svc/api/netting/enroll \
-H 'content-type: application/json' \
-d '{"address":"'$ADDR'","signature":"0x…"}'
# fund it: send USDC on any covered chain to the relayer, then present the tx
curl -s -X POST https://anygas.xyz/svc/api/netting/deposit \
-H 'content-type: application/json' \
-d '{"address":"'$ADDR'","chainId":8453,"txHash":"0x…"}'
# free internal transfer (sign: robyn-netting-transfer:v1:from=…:to=…:amount=…:nonce=…)
curl -s -X POST https://anygas.xyz/svc/api/netting/transfer \
-H 'content-type: application/json' \
-d '{"from":"0x…","to":"0x…","amount":"250000","nonce":"1","signature":"0x…"}'
# take it back out on-chain at any time
curl -s -X POST https://anygas.xyz/svc/api/netting/withdraw \
-H 'content-type: application/json' \
-d '{"address":"0x…","amount":"250000","toChain":8453,"toAddress":"0x…","nonce":"2","signature":"0x…"}'
Leaving costs different amounts on different chains. Internal transfers are free at any size, but a withdrawal is a real payout we broadcast, so the destination chain’s payout cost is deducted — and those costs differ by more than 1000x. Check before you sign:
# rank every exit for $1.00, best first
curl -s "https://anygas.xyz/svc/api/netting/preview?amount=1000000"
# -> cheapestExit: 43114 (delivers $0.99994) … Linea delivers $0.927
# price one chain, and learn whether it would succeed at all
curl -s "https://anygas.xyz/svc/api/netting/preview?amount=500&toChain=1"
# -> wouldSucceed: false — below the payout cost, nothing would arrive
Nonces are monotonic per account: every signature at or below the account nonce is permanently spent, so a captured request cannot be replayed.
Sandbox mode
Add x-anygas-sandbox: 1 to POST /api/route/execute and Robyn simulates the entire lifecycle — no relayer spend, no signature, nothing on-chain. The response mirrors a live execute and hands back a sbx_… id. Poll GET /api/route/status?id=sbx_… to watch it walk RECEIVING → SWAPPING → SENDING → COMPLETED over ~40s. Sandbox routes live in memory for 1h and are never persisted or shown in the public explorer.
curl -s https://api.anygas.xyz/svc/api/route/execute \ -H 'content-type: application/json' \ -H 'x-anygas-sandbox: 1' \ -d '{"fromChain":42161,"toChain":8453,"fromToken":"USDC", "toToken":"USDC","amount":"5000000", "toAddress":"0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2"}'
{
"status": "BRIDGING", "sandbox": true, "gasless": true,
"id": "sbx_611d3343c9b8a0edae",
"srcTx": "0xsandbox…",
"to": { "chain": 8453, "token": "USDC", "estimated": "4987500", "recipient": "0x8EdE…DFb2" },
"robynRouteFeeBps": 25,
"note": "SANDBOX — no funds moved. Poll GET /api/route/status?id=sbx_… (~40s)."
}AnyGas Account — yield-native gasless spending (non-custodial)
Hold one yield-bearing position (USDC auto-allocated to the best of Aave v3 + Moonwell, in your own wallet) and spend it as any token or native gas on any of 26 chains, gaslessly, just-in-time — the remainder keeps earning. Non-custodial: your funds stay in your wallet in an audited money market; you grant the relayer a capped, revocable aUSDC/mUSDC allowance, so a compromise reaches at most that cap — never a pool.
GET /api/ncaccount/{agent}— your aUSDC/mUSDC per chain + the allowance you granted the relayer + live APY (best of Aave v3 + Moonwell).POST /api/ncaccount/quote— read-only JIT quote. Body:{agent, srcChain, amount, toChain, toAddress}.POST /api/ncaccount/spend— spend with ONE EIP-712 signature. Body:{intent, signature, live:true}. The relayer verifies the signature, pulls only up to your on-chain allowance, unwinds from your yield venue (Aave v3 or Moonwell), and delivers gaslessly.
EIP-712 domain {name:"RobynNCAccount", version:"1", chainId: srcChain}; intent fields {agent, srcChain, amount, toChain, toAddress, nonce, deadline}. Setup once: supply USDC to Aave v3 or Moonwell in your wallet, then approve the relayer for your aUSDC/mUSDC up to your risk budget. Because your ~3–4% APY typically exceeds the 0.15% (min $0.01) spend fee, gas is effectively net-free. The Agent Kit abstracts all of this: await agent.yieldSpend({ srcChain, amount, toChain, toAddress }).
GET /api/account/{addr} (yield-aware unified balance), GET /api/account/venues (live APYs), GET /api/balance/{addr} (unified balance across all chains), GET /api/receipts/{addr} (relayer-signed settlement history), POST /api/events/subscribe (price/balance webhooks).Health & status
GET /api/uptime returns per-day availability as seen by an independent relay in Iceland probing /api/status every 10 minutes (a probe that never arrived counts against us). Same data drives /status. Delivery success and latency: GET /api/delivery/stats.Sanitized, up/down health for the trust page: per-chain node health, bridge-rail health, and endpoint probes. No addresses, amounts or revenue. Cached ~30s.
{
"status": "operational", "ts": 1784012730722,
"nodes": { "up": 25, "total": 25,
"evm": [ { "chainId": 1, "name": "Ethereum", "up": true }, /* …22 EVM… */ ],
"nonEvm": [ { "name": "Stellar", "up": true }, { "name": "Solana", "up": true }, { "name": "Bitcoin", "up": true } ] },
"rails": [ { "name": "bungee", "up": true }, { "name": "debridge", "up": true }, /* … */ ],
"routes": 500,
"endpoints": [ { "name": "route/chains", "up": true }, /* … */ ]
}The live route graph: node chain ids, node/route counts, the Permit2 spender (relayer) your SDK signs for, the routing engine, and rich per-node metadata for the non-EVM legs. This is the source of truth for what is routable.
{
"nodes": 25,
"chains": [1,10,56,100,130,137,146,480,999,1135,1868,4663,5000,8453,33139,34443,42161,43114,57073,59144,81457,7777777],
"routableNonEvm": ["stellar","solana","bitcoin"],
"directedRoutes": 500,
"relayer": "0x1253D25A0B6a757CfD65CDf441E2d91DBeCeE6c5",
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"engine": "LI.FI aggregation (Across / Gas.zip / Near / Squid / Symbiosis / Relay + ~15 bridges); Stellar + Solana via Allbridge Core; Bitcoin via Chainflip",
"nonEvmNodes": { "stellar": {…}, "solana": {…}, "bitcoin": {…}, "tron": {…}, "sui": { "quoteOnly": true, "executeReady": false } },
"comparatorQuoteOnly": { /* Scroll, Chainflip asset lanes, Sui — quotable, NOT execute-ready */ }
}nodes/directedRoutes are execute-ready. Anything under comparatorQuoteOnly or carrying executeReady:false (Sui, Scroll, and the extra Chainflip asset lanes) is quotable for comparison only — see caveats.Everything an integrator needs to sign a same-chain gasless intent: the deployed router / anyGas / tokenPaymaster addresses, Permit2, EntryPoint, the EIP-712 domain, the per-chain deployment map (gaslessChains) and the accepted fee tokens.
{
"service": "Robyn Gasless Pay",
"router": "0x8f4a466435264A2D56Bc80e6749Ea351d567D141",
"anyGasRouter": "0x298ab4Be2B3a8f1A2dB3650f578407AA1f952487",
"tokenPaymaster": "0x4afc3DD9143bC75fCfFDA52e9E5e50dA4Cb3A635",
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"entryPoint": "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"chainId": 4663, "routes": ["direct","cheapest","fastest"], "recommended": "direct",
"gaslessChains": { "1": { "address": "0x68B5…Ae80", "stable": "0xA0b8…eB48", "version": "3.1" }, /* …per chain… */ },
"acceptedTokens": [ { "symbol": "WETH", … }, { "symbol": "USDG", … } ],
"domain": { "name": "RobynGaslessRouter", "version": "1", "chainId": 4663, "verifyingContract": "0x8f4a…D141" }
}Quote a cross-chain intent
Best gasless route for a single intent. EVM↔EVM runs best-of-N over the bridge aggregators (LI.FI / Bungee / deBridge); a Stellar / Solana / Bitcoin side is dispatched to the dedicated leg (Allbridge Core / Chainflip). One signed intent; Robyn fronts all gas on both sides plus the bridge.
POST body from code. The same fields also work as GET query parameters for a browser or curl — /svc/api/route/quote?fromChain=8453&toChain=42161&fromToken=USDC&toToken=USDC&amount=25000000 — with an identical response and rate limit. amount is always source-token base units (25000000 = 25 USDC).| Field | Type | Notes | |
|---|---|---|---|
fromChain | id / string | required | Source node. |
toChain | id / string | required | Destination node. |
amount | string | required | Source-token smallest units. |
fromToken | string | optional | Symbol or address; omit for native. |
toToken | string | optional | Symbol or address; omit for native. |
toAddress | string | optional | Recipient; defaults to fromAddress. |
fromAddress | string | optional | Quote origin; anonymous default used if omitted. |
slippage | number | optional | Fraction, e.g. 0.005. |
curl -s https://api.anygas.xyz/svc/api/route/quote \ -H 'content-type: application/json' \ -d '{"fromChain":42161,"fromToken":"USDC","toChain":"bitcoin", "toToken":"BTC","amount":"50000000","toAddress":"bc1q…"}'
{
"gasless": true,
"from": { "chain": 42161, "token": "USDC", "amount": "50000000" },
"to": { "chain": "bitcoin", "token": "BTC", "estimated": "78660", "min": "76693", "recipient": "bc1q…" },
"bridge": "chainflip", "via": ["Chainflip (REGULAR)"], "durationSec": 408,
"robynRouteFeeBps": 25, "robynRouteFee": "125000",
"chainflip": { "egressAmount": "78660", "fees": [ {"type":"INGRESS",…}, {"type":"NETWORK",…}, {"type":"EGRESS",…} ] },
"note": "Bitcoin leg via Chainflip (NATIVE BTC — no wrapping). egress is in sats."
}EVM↔EVM lanes return a best-of-N shape instead: bridge, bestOf, and a compared[] array of per-aggregator outputs.
Execute a route, relayer-orchestrated and gasless. The relayer fronts all gas, tries ranked routes in net-output order and fails over on error, then returns a route id you can track. Use mode:"permit2" so the user signs one off-chain Permit2 SignatureTransfer and pays no gas.
| Field | Type | Notes | |
|---|---|---|---|
fromChain,toChain,amount | — | required | As in quote (smallest units). |
fromToken,toToken,toAddress | string | optional | Token symbols/addresses; recipient. |
mode | string | optional | "permit2" (user-signed pull) or "self" (operator moves own funds). Default "self". |
permit2 | object | cond. | Required for permit2: {owner, permitted:{token,amount}, nonce, deadline, signature}. |
sbx_ id, no relayer spend. See Sandbox mode. Non-EVM legs settle only once value lands; when the relayer isn't pre-funded for a lane, execute returns an honest FUNDS_REQUIRED rather than a fake tx.{
"id": "rt_1a2b3c4d5e6f", "status": "BRIDGING", "gasless": true,
"bridge": "across", "aggregator": "lifi",
"robynFee": "12500", "bridged": "4987500",
"from": { "chain": 42161, "token": "0xaf88…5831", "amount": "5000000" },
"to": { "chain": 8453, "token": "0x8335…2913", "recipient": "0x…", "min": "4980000" },
"srcTx": "0x…", "track": "/api/route/status?id=rt_1a2b3c4d5e6f"
}Track an in-flight route. Live ids (rt_…) are re-checked against the underlying bridge on each read; sandbox ids (sbx_…) advance on a timer. Survives a service restart.
{
"id": "sbx_611d3343c9b8a0edae", "sandbox": true,
"status": "BRIDGING", "stage": "RECEIVING",
"srcTx": "0xsandbox…", "destTx": null, "elapsedSec": 0,
"to": { "chain": 8453, "token": "USDC", "estimated": "4987500", "recipient": "0x8EdE…DFb2" }
}Unknown id → 404 {"error":"unknown route id"}. Live routes reach status:"DONE" with a destTx once delivered.
Best-execution comparator
Quotes every rail that can serve a lane in parallel — the bridge aggregators (LI.FI / Bungee / deBridge), native Circle CCTP (USDC lanes, incl. Stellar), Allbridge Core and Chainflip — normalizes them, ranks by net destination output, and flags the single best. Nothing is signed or moved. Adds advisory fields: effectiveFeePct (same-asset lanes), a 0–100 qualityScore, a min-economical-size guard, and a 12s response cached flag. Accepts the same params as quote, via query string (GET) or JSON body (POST).
{
"lane": { "fromChain": "42161", "toChain": "solana", "applicableRails": ["allbridge","chainflip"], "estOutUnit": "USDC smallest-units" },
"quotes": [
{ "rail": "allbridge", "ok": true, "estOut": "49822154", "etaSeconds": 120,
"best": true, "effectiveFeePct": 0.3556, "qualityScore": 97 },
{ "rail": "chainflip", "ok": true, "estOut": "49265533", "etaSeconds": 109,
"effectiveFeePct": 1.4689, "qualityScore": 96 }
],
"best": "allbridge", "comparedRails": 2, "railsAttempted": 2,
"guard": { "ok": true, "effectiveFeePct": 0.3556, "thresholdPct": 3 }, "cached": false
}guard.ok is false with a warning and a best-effort recommendedMinInput — fixed bridge costs dominate tiny moves.Goal-based source picker. State a goal (max-output · cheapest · fastest), a destination, and a set of candidate funding sources; the solver reads /compare once per source and ranks them. It returns a plan with ready-to-POST execute params — it never executes; the caller decides.
| Field | Type | Notes | |
|---|---|---|---|
toChain,toToken | — | required | Destination node + asset. |
from | array | required | 1–10 sources: [{chain,token,amount}]. |
goal | string | optional | Default max-output. |
amount | string | optional | Shared amount if sources omit their own. |
toAddress | string | optional | Recipient, echoed into plan.execute. |
curl -s https://api.anygas.xyz/svc/api/intent/solve \ -H 'content-type: application/json' \ -d '{"goal":"max-output","toChain":8453,"toToken":"USDC", "from":[{"chain":42161,"token":"USDC","amount":"5000000"}, {"chain":10,"token":"USDC","amount":"5000000"}]}'
{
"goal": "max-output", "to": { "chain": 8453, "token": "USDC" },
"plan": { // winner (null if nothing quoted)
"from": { "chain": 42161, "token": "USDC", "amount": "5000000" },
"rail": "allbridge", "estOut": "…", "qualityScore": 97,
"execute": { "fromChain": 42161, "fromToken": "USDC", "toChain": 8453, "toToken": "USDC", "amount": "5000000" }
},
"alternatives": [ … ], "skipped": [ … ], "sourcesConsidered": 2,
"note": "Plan only — NO funds moved. To run it, POST plan.execute to /api/route/execute yourself."
}Anonymized feed of recent real routes settled by the relayer (powers the public Explorer). No addresses, tx hashes or exact amounts — just lane, asset, rail, a coarse status band and size bucket. limit default 25, max 100.
{
"routes": [
{ "lane": "Solana → Arbitrum", "asset": "USDC", "rail": "Allbridge", "status": "pending", "when": "3h ago", "size": "small", "sandbox": false },
{ "lane": "Arbitrum → Bitcoin", "asset": "USDC → BTC", "rail": "Chainflip", "status": "pending", "when": "4h ago", "size": "small", "sandbox": false }
],
"count": 2, "note": "Anonymized recent cross-chain routes settled by the Robyn AnyGas relayer."
}Route primitives
PRIMITIVES_LIVE=1 and the record must carry live:true. The shipped default (live:false) simulates every tick — it logs the exact wouldExecute params and moves nothing. Do not assume a 200 here settles anything.Plan a multi-recipient / multi-leg payout. Body: { routes: [{fromChain,fromToken,toChain,toToken,amount,toAddress}], live? } — up to 20 legs, each planned via /compare. toAddress is required per leg.
{
"id": "batch_086f0679dfac4a905d", "status": "planned", "live": false,
"legs": [ { "index": 0, "route": {…}, "plan": { "ok": true, "rail": "allbridge", "estOut": "49822154", "effectiveFeePct": 0.3556 } } ],
"warnings": [],
"note": "Planned only — NOT executed. The watcher SIMULATES this batch (record live:false). No funds have moved."
}Scheduled / recurring transfer. Body: { route, everySeconds?, at?, count?, live? } — everySeconds ≥ 10, count 1–1000 (omit everySeconds for a one-shot). GET the id to inspect, DELETE to cancel.
{
"id": "sched_cde9a12f1107fa3652", "route": {…},
"everySeconds": 3600, "remaining": 24, "count": 24, "live": false, "status": "active", "runs": [],
"note": "Scheduled — live:false, so it SIMULATES (logs wouldExecute, moves no funds)."
}
// DELETE /api/route/schedule/sched_… → { "id": "sched_…", "status": "cancelled" }Trigger-based transfer. Body: { route, when, live? } where when.type is priceAbove · priceBelow (needs when.asset + numeric when.value) or timeAfter (epoch/ISO when.value). Price conditions arm against the on-chain feeds (/api/pricefeeds: ETH/USD, BTC/USD, USDG/USD).
{
"id": "cond_3728705d2654926bc1", "route": {…},
"when": { "type": "priceBelow", "asset": "ETH/USD", "value": 3000 },
"live": false, "status": "armed", "runs": [],
"note": "Armed — live:false, so it SIMULATES. Price source: on-chain /api/pricefeeds."
}For agents: one call, typed errors, streaming
The fastest path from intent to transaction. These endpoints exist so an autonomous caller does not have to learn the rail model, poll for status, or parse prose to decide whether to retry.
Body {intent} (plain language) or {fromChain, toChain, token, amountHuman|amount, toAddress}. Resolves, quotes, and returns status of sign (with a complete signRequest: chainId, spender, permit2Contract, tokenAddress, amount, and where to POST it), quoted, done, or a typed error. Add x-anygas-sandbox: 1 to dry-run the whole flow with no funds.
The complete error contract. Every failure response carries errorCode, retryable, retryAfterMs and suggestedAction alongside the legacy error string. Branch on errorCode.
Server-sent events for a route: one event immediately, then event: status on every change and event: done on a terminal state before closing. Replaces polling /api/route/status.
Published rate tiers. A free key from POST /api/keys/create scopes limits to your key rather than your IP and raises the ceiling.
Retry safety
Pass x-idempotency-key on any fund-moving call (/api/route/execute, /api/ncaccount/spend, /api/7702/sponsor). A duplicate replays the original response instead of moving funds a second time, and the sandbox honours the identical contract so you can verify your retry logic without risk.
EIP-7702 sponsored transactions
Robyn submits your signed EIP-7702 authorization as a type-4 transaction and pays the gas. No bundler, no paymaster deposit, no native token in your account. Public sponsorship is prepaid in USDC over x402; an unpaid call returns 402 with an accepts[] invoice and the full plan, so pricing is always free to check.
Chains, caps, pricing and the full flow.
The canonical batch-executor delegate: per-chain deployments, ABI, and the EIP-712 scheme for executeSigned.
Verify an authorization tuple {chainId, address, nonce, yParity, r, s} recovers the expected authority, and whether the nonce matches the live account nonce. Free, read-only.
Body {chainId, authorization, to?, data?, dry?}. With dry:true returns the envelope, estGas and priceUsd without broadcasting. Without it, pay the x402 invoice via an X-PAYMENT header and the relayer broadcasts, returning the transaction hash.
Developer keys
Optional self-serve key for a stable identity + usage visibility. The API stays keyless; a key just tags your requests. Body: { label? }. The key is shown once — store it and send it as x-anygas-key: <key>.
{
"key": "ak_9fa383a39ca3b33b74775e7d8f3d04c70b98057c",
"label": "docs-example", "created": 1784012809055,
"note": "Store this key — shown once. Send it as x-anygas-key: ak_… . The API works without a key."
}Usage stats for your key — pass it via x-anygas-key header or ?key=. Returns hour + day aggregates and recent calls. (GET /api/keys/me returns just the key metadata.)
{
"key": { "label": "docs-example", "created": 1784012809055 },
"hour": { "total": 0, "errors": 0, "topPaths": [] },
"day": { "total": 0, "errors": 0, "topPaths": [] },
"recent": []
}Register a completion webhook for a route id — fire-and-forget a cross-chain move and get called on DONE/FAILED instead of polling. Body: { routeId, url, secret? }. Watch state at GET /api/hooks/<id>.
{
"id": "wh_1008e656e4d46f7aec", "routeId": "rt_abc123",
"url": "https://example.com/webhook", "status": "pending",
"note": "We POST {event, routeId, status, destTx, srcTx, ts} on DONE/FAILED."
}secret, each delivery carries x-anygas-signature: sha256=HMAC(secret, rawBody) — verify it before trusting the payload.Custodial capped sub-wallets for autonomous agents. The platform holds an encrypted session key; the agent gets a bearer token and every action is policy-checked (chain / token / recipient allowlists, per-tx and rolling-24h caps, expiry, revoke) before it is signed and forwarded to /api/route/execute. The agent never holds the key, and the funded balance is a hard ceiling.
POST /api/session/issue— owner mints a session + bearer (returns the address to fund).POST /api/session/execute— bearer (Authorization: Bearer <rsk_…>) runs a policy-gated gasless move.GET /api/session/<id>— status, policy, spend (bearer or owner).POST /api/session/revoke·GET /api/session/list— owner only.
Minting requires the platform OWNER_KEY, so this is for first-party / hosted deployments — not anonymous self-serve.
Plan a whole workflow before you pay for step one
Every other quoting API validates one hop. So an agent discovers that step 3 is impossible only after paying for steps 1 and 2 — and money spent reaching a dead end is not refunded by discovering it was a dead end. POST /api/route/plan validates the entire plan up front.
Fees and durations accumulate across steps. Evaluation stops at the first unroutable step rather than reporting speculative results for the steps behind it. When a plan cannot work you get the binding constraint and the value that would work — not a bare “no” that sends your agent into blind retries.
curl -s https://api.anygas.xyz/svc/api/route/plan \ -H 'content-type: application/json' \ -d '{"steps":[ {"fromChain":8453,"toChain":10,"fromToken":"USDC","toToken":"USDC", "amount":"2000000","toAddress":"0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2"}, {"fromChain":10,"toChain":42161,"fromToken":"USDC","toToken":"USDC", "amount":"1900000","toAddress":"0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2"}], "constraints":{"maxTotalSeconds":5}}'
{
"feasible": false,
"bindingConstraint": {
"constraint": "maxTotalSeconds", "limit": 5, "actual": 15,
"why": "The plan needs 15s across 2 steps; your ceiling is 5s."
},
"whatWouldMakeThisWork": "Set maxTotalSeconds to at least 15."
}Verify what actually happened — against the chain, not our database
Every relay answers “did it work?” by reading its own row and saying yes. A relay that has mis-delivered has a database that still says DONE. This endpoint checks the destination chain: it fetches the payout transaction, decodes its ERC-20 Transfer logs, and reports what really happened.
You supply the post-conditions — recipient, minAmount — and we report whether reality matches them. It also cross-checks our own receipt against the chain and says so plainly if our books disagree.
Three-valued on purpose: verified, failed, or undetermined. Undetermined — chain unreachable, transaction not yet mined, unknown id — is not a pass. Collapsing “I could not check” into “fine” is how a loss eventually gets reported as a success.
curl -s "https://api.anygas.xyz/svc/api/outcome/verify/fl_reconproof_msaxh1ls\
?recipient=0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2&minAmount=1000000"{
"verdict": "verified",
"checks": [
{ "assertion": "destination transaction succeeded", "result": "verified" },
{ "assertion": "funds reached the recipient you specified", "result": "verified" },
{ "assertion": "at least the amount you required arrived", "result": "verified" },
{ "assertion": "our receipt matches the chain", "result": "verified" }
],
"howToDistrustUs": "Every figure comes from 0xb732f508… on chain 8453. Fetch that receipt yourself and you can reproduce this without us."
}Prove what your agent spent
An AI agent that spends on someone’s behalf cannot normally prove what it spent. Its own summary is a claim by the party being audited, and a dashboard screenshot proves nothing at all.
GET /api/statement/spend returns every transfer in the window with each fee broken down, signed by Robyn’s relayer key (standard EIP-191 personal_sign over a content digest). Your principal verifies the signature against the published signer address — without trusting the agent, without trusting a web page, and without trusting us.
Alter any number and verification fails: the digest is recomputed from the rows submitted, never taken from the digest field. And every statement lists what it does not cover, so a partial total never reads as a complete one.
curl -s "https://api.anygas.xyz/svc/api/statement/spend?days=30" > stmt.json curl -s https://api.anygas.xyz/svc/api/statement/verify \ -H 'content-type: application/json' -d @stmt.json # { "valid": true, "recoveredSigner": "0x8EdE…DFb2" } # edit any amount in stmt.json and it becomes: "valid": false
Where every unit of the fee went
Everyone quotes a fee. Nobody says where it went. “0.25% routing” is a rate, not an account of the units that actually left.
GET /api/forensics/fee/{id} reconstructs a settled transfer unit by unit: destination payout gas, Robyn margin — or a Robyn subsidy where we absorbed a loss, reported as exactly that. Anything that cannot be attributed comes back as unattributed rather than folded into another line to make the arithmetic look tidy. A breakdown that always sums perfectly is one that is hiding something.
GET /api/forensics/counterfactual/{id} answers “was this actually the better route?” by applying the published standard-bridge model to the same amount and destination. It is a like-for-like model comparison, and the response says so — it is not a claim about what a third party would have quoted that second.
How long it really takes
Quotes used to advertise a constant duration — the same number for an instant float payout and a Stellar burn waiting on a Circle attestation. For an agent planning a workflow, one marketing number is worse than useless, because it is confidently wrong for most corridors.
GET /api/delivery/stats publishes what settlements actually took, per rail and per corridor. Below the sample threshold it returns insufficient-data and hands back the static estimate labelled as an estimate — it never invents a percentile from three observations. Quotes carry the same deliveryBasis, so you always know whether a duration was measured or guessed.
Rehearse failures before you meet them in production
You cannot write good error handling against errors you have never seen. Send x-anygas-rehearse: <ERROR_CODE> together with x-anygas-sandbox: 1 on any endpoint and you get back the byte-identical error the real path would produce — same errorCode, retryable flag and suggestedAction — plus rehearsed: true.
- Refused on live traffic, so production can never be steered into a failure.
UNAUTHORIZED,RATE_LIMITEDandDUPLICATE_REQUESTare deliberately not rehearsable — an authorisation or duplicate-detection result must never be fakeable.GET /api/errorslists every code you can rehearse.
curl -s https://api.anygas.xyz/svc/api/route/execute \ -H 'content-type: application/json' \ -H 'x-anygas-sandbox: 1' \ -H 'x-anygas-rehearse: INSUFFICIENT_FLOAT' -d '{}' # 503 { "errorCode": "INSUFFICIENT_FLOAT", "retryable": true, # "suggestedAction": "Relayer inventory cannot cover…", "rehearsed": true }
Prove you were refused — not that you failed
When a payment provider says no, the agent carries the blame. Its principal sees a task that did not complete and has only the agent’s account of why. “The relay refused me” is indistinguishable from “I got it wrong” or “I never tried” — and an autonomous agent that cannot evidence an external refusal cannot be judged fairly, or believed when it says the fault was elsewhere.
Send x-anygas-receipt: 1 on any request. If we refuse it, the error response carries a signed refusalReceipt: what was asked, when, and the reason we actually returned. Your principal verifies it against our published signer address.
Rewrite the reason or the error code and verification fails — the digest is recomputed from the submitted fields, never taken from the digest field. Blame cannot be shifted in either direction: an agent cannot forge a refusal to excuse itself, and we cannot quietly soften a reason after the fact.
What it does not claim. A receipt proves the refusal happened. It is not an assertion that the refusal was correct. Use it to evidence that you were blocked — or to bring back to us and contest.
Receipts are opt-in and error-only. Successful requests never mint one.
curl -s https://api.anygas.xyz/svc/api/route/quote \ -H 'content-type: application/json' \ -H 'x-anygas-receipt: 1' \ -d '{"fromChain":8453,"toChain":8453,"amount":"1"}' # { "error": "…", "errorCode": "SAME_CHAIN", # "refusalReceipt": { "at": "…", "endpoint": "/api/route/quote", # "errorCode": "SAME_CHAIN", "reason": "…", "digest": "0x…", # "signature": "0x…", "signer": "0x8EdE…DFb2" } }
Have I paid this address before?
Address substitution is the most common theft in this space, and it is silent — one hex string looks exactly like another. Signed payment requests solve it when the payee cooperates. This covers the other half: an agent about to pay an address it has never paid, with nothing telling it so.
You get your own history with that address: how many times, how recently, the typical amount, and whether this one is unusual for that counterparty.
This is not a reputation score. It says nothing about whether the address is honest, and a familiar address is not a safe address — keys get compromised, and a counterparty you have paid fifty times can be under someone else’s control today. Familiarity lowers the odds of a typo or a substitution; it does not remove them.
“First time” is not an accusation. Every legitimate relationship has a first payment. It is simply the last moment a substituted address can still be caught — so confirm it through a second channel.
Read coverage. The float-lane ledger does not retain recipient addresses, so float-lane payouts are excluded from this history and a “first time” result can be wrong. We say so in the payload rather than let a false signal get dismissed — a warning users learn to ignore destroys the value of every true one.
It appears automatically as a warning, never a blocker, in /api/preflight/full.
One call before you act
Everything you need to decide well exists — spread across five endpoints. Can this corridor be served right now? Are the rails actually competing? Is there a cheaper way? Is the delivery time measured or guessed? Does this fit the budget my principal set? An agent that has to make five calls and reconcile them will make one and guess.
curl -s https://api.anygas.xyz/svc/api/preflight/full \ -d '{"fromChain":42161,"toChain":8453,"fromToken":"USDC", "toToken":"USDC","amount":"5000000","toAddress":"0x…", "mandateId":"0x…"}' // -> { verdict: "proceed-with-warnings", proceed: true, // blockers: [], // warnings: [ { what: "not a competitive price", … }, // { what: "cheaper options exist", … } ], // routing: {…}, savings: {…}, mandate: {…}, capability: {…} }
Blockers are not warnings. A blocker will fail — do not attempt it. A warning might cost you or your user something — read it out. They are never merged, because an agent that cannot tell “impossible” from “suboptimal” either aborts on noise or ploughs through real obstacles.
Silence is not assent. Any check that could not run appears in unavailable and downgrades the verdict to caution. Name a mandateId that does not exist and you are blocked — you would otherwise be spending outside any budget while believing you were inside one.
What proceed: true means. Nothing known to us blocks it. It is not a promise the transfer will succeed — chains and third-party rails fail in ways no preflight can foresee.
Is my integration actually safe?
Every provider documents the safe way to integrate and then has no idea whether anyone followed it. You find out you skipped something when it costs you — a price that moved because you never sent minOut, a duplicate transfer because you never sent an idempotencyKey.
Post the request you intend to send and get told what is missing, with the field and the actual consequence.
curl -s https://api.anygas.xyz/svc/api/integration/lint \ -d '{"endpoint":"/api/route/execute", "payload":{"fromChain":8453,"toChain":10,"amount":"2000000"}}' // -> verdict: "unsafe" // minOut — the price can move between quote and settlement // idempotencyKey — any retry can move funds a second time
Severity is honest. unsafe means you can lose money or report something untrue. suboptimal means it will cost you. Inflating everything to a warning is how linters get ignored.
It says what it cannot see. Only the request is linted — not whether you branch on all three verification verdicts, treat undetermined as success, or use serviceableNow rather than policyEligible. A clean lint is not a correct integration, and an endpoint with no rules yet returns “unchecked”, never “clean”.
Don’t guess the call order
This reference documents over a hundred endpoints. It tells you what exists — it cannot tell you what order to call things in, or which checks it would be reckless to skip. That knowledge otherwise lives in prose, so every integrator re-derives it differently and usually badly.
curl -s https://api.anygas.xyz/svc/api/recipes/pay-someone-safely // 1. POST /api/preflight/full — can this be served? is it competitive? // have I paid this address before? // 2. POST /api/route/quote — price + signed attestation // 3. POST /api/route/execute — pass minOut AND the attestation // 4. GET /api/outcome/verify — did it ACTUALLY arrive? (three verdicts)
Five playbooks: pay-someone-safely, prove-what-i-spent, spend-within-a-budget, handle-failures-well, stay-current.
Every step says what skipping it costs. Not “best practice” — the concrete harm. An instruction with no consequence attached is an instruction that gets skipped.
Recipes are checked against the live spec on every request. A recipe that names an endpoint which no longer exists is worse than no recipe, because it will be followed. Any such step is flagged pathNotInSpec, and you should trust the spec over the recipe.
What changed since you integrated
An agent that generated a client last week has no way to learn what changed. Human changelogs are prose on a marketing page — unparseable, usually stale, and silent about the only thing a running integration cares about: whether something it depends on has broken. So integrators either pin forever and miss every improvement, or re-fetch the whole spec on a schedule and diff it themselves. Both are the provider’s failure pushed onto the consumer.
curl -s "https://api.anygas.xyz/svc/api/changelog?since=3.9.0" // -> { currentVersion: "3.19.1", entryCount: 11, breakingCount: 1, // mustAct: true, // entries: [ { version: "3.19.1", breaking: true, // summary: "Recording a mandate spend now requires the agent's signature.", // changed: ["/api/mandate/consume"], // action: "Add `agentSignature` over keccak256(…)" }, … ] }
Check mustAct first. If it is true there are breaking changes since your version and your integration may already be failing. Every breaking entry carries an action — a flag without an instruction is just an alarm.
What “breaking” means here. An existing correct integration could stop working. It is a compatibility claim, not a measure of importance, and nothing that qualifies is quietly downgraded because it would look bad.
It is checked against reality. Every path the changelog claims is cross-referenced with the live spec on each request, and drift is reported rather than hidden — a changelog is believed, so a stale one is worse than none. If the two ever disagree, trust the spec.
What did the “best” route actually beat?
Every router returns a winner. None of them tells you what it beat — or whether it beat anything at all. “Best route” is presented identically when five rails competed and the cheapest won, and when four rails failed to answer and the survivor was declared best by default. Those are completely different facts, and without knowing which one you got, you cannot tell a good price from the only price.
curl -s https://api.anygas.xyz/svc/api/route/why \ -H 'content-type: application/json' \ -d '{"fromChain":42161,"fromToken":"USDC","toChain":8453, "toToken":"USDC","amount":"5000000","toAddress":"0x…"}' // -> { chosen: { rail: "bungee", delivered: "4989288", durationSec: 10 }, // runnerUp: { rail: "debridge", delivered: "4764533" }, // marginOverRunnerUp: "224755", // silentRails: [ { rail: "lifi", reason: "breaker open (rate-limit 429)" }, // { rail: "socket", reason: "no API key (skipped)" } ], // wasCompetitive: true, signature: "0x…" }
Check wasCompetitive before treating a price as good. When it is false, exactly one rail answered — “best” means “only”, and the response says so in plain words rather than letting a sole survivor look like a winner.
A silent rail is not a beaten rail. Rails in silentRails failed to answer — down, rate-limited, or missing credentials — and each carries its reason. They are excluded from the comparison rather than counted as losers, because folding silence into “we compared N rails” is how a router flatters its own coverage.
The rationale is signed, so you can archive why you routed as you did. Inflating the winner’s delivery, renaming the winning rail, erasing the silent rails or flipping wasCompetitive all invalidate it.
What would make this cheaper?
Every router answers “what does this cost?”. None answers “what should I change?” — so an agent handed a price has two options: pay it, or give up. Yet the price is usually a consequence of choices it did not know it was making.
curl -s https://api.anygas.xyz/svc/api/route/improve \ -H 'content-type: application/json' \ -d '{"fromChain":8453,"toChain":59144,"fromToken":"USDC", "toToken":"USDC","amount":"2000000","toAddress":"0x…"}' // -> destination chain: Linea -> Avalanche, saves $0.072940 (changesOutcome: true) // transfer size: fixed payout cost is 365 bps of this transfer; // batching to 29200000 units brings it under 25 bps // retry later: lifi could not quote (rate-limited) — the winner // only had to beat whoever answered
Every suggestion carries a real number, computed from the same cost table the router charges from. Advice without a figure is advice-shaped noise you cannot act on, so we do not return it.
Watch changesOutcome. A suggestion flagged true moves where the money lands. Telling you to send to a different chain is not a cost tip if that is not where the funds were needed — those suggestions always carry a caveat, and an agent should confirm before applying one.
Silent rails are classified. On the routing rationale, a rail that did not quote now reports status: "not-configured" (missing credentials — it will never recover on retry and needs an operator) or status: "degraded" (rate limit or cooldown — it may heal itself), with recoversOnRetry and a remedy. Reporting both as “breaker open” makes a permanent misconfiguration look like weather.
If there is genuinely nothing to improve, the response says so rather than padding the list.
A budget your agent cannot raise
Handing money to an autonomous agent is currently all-or-nothing: you give it a key and hope. Every existing control sits on the wrong side of the trust boundary — a limit the agent enforces on itself is a limit it can drop, and a limit written into its prompt is a suggestion.
A mandate is a budget signed by the principal and enforced by something the agent does not control.
// 1. PRINCIPAL builds the budget and signs the digest with their own wallet curl -s https://api.anygas.xyz/svc/api/mandate/prepare \ -H 'content-type: application/json' \ -d '{"principal":"0xYou","agent":"0xAgent", "maxTotalUnits":"5000000","token":"USDC","expiresInSec":604800}' // 2. register it, then the agent checks BEFORE each spend curl -s https://api.anygas.xyz/svc/api/mandate/check \ -d '{"mandateId":"0x…","amount":"9000000"}' // -> { verdict: "EXCEEDS_MANDATE", remainingUnits: "5000000" } // 3. after spending, record it — verified on-chain before it counts curl -s https://api.anygas.xyz/svc/api/mandate/consume \ -d '{"mandateId":"0x…","transferId":"fl_…"}' // -> { counted: true, countedUnits: "1000000", remainingUnits: "4000000" }
Only the principal can grant a budget. A signature that does not recover to the principal named in the mandate is refused with 401 — otherwise anyone could sign themselves an allowance. Robyn never signs mandates and holds no principal keys.
Consumption is measured, not reported. A spend counts only when the transfer verifies on-chain, and the amount counted is what the destination chain shows arrived — not what the agent claims. An agent cannot inflate its remaining budget with spends that never happened, nor hide ones that did.
Who may record a spend
Recording consumption mutates a spending control, so it is authenticated. The agent named in the mandate must sign keccak256("v1|robyn-mandate-consume|<mandateId>|<transferId>|<amount>") and pass it as agentSignature.
Without that, anyone who learned a mandate id could attribute an unrelated — but genuine — on-chain transfer to your budget and exhaust it. Consumption already required a verifiable transfer, so a spend could never be fabricated; it could be misattributed, which is just as damaging to the agent depending on that budget. A control that any holder of an identifier can drain is not a control.
The authorisation binds the transfer as well as the amount, so it cannot be replayed onto a different payment. Note also that the principal cannot consume on the agent’s behalf: this authenticates the spender, not the grantor.
Approval per spend, not just a total
A total budget is the wrong control for the risk people actually fear. “$500 this month” still permits one catastrophic $500 transfer to the wrong place. What a principal usually wants is: spend freely under some threshold, but anything larger needs me to approve that payment.
// grant: free under 0.50 USDC, approval required above it curl -s https://api.anygas.xyz/svc/api/mandate/prepare \ -d '{"principal":"0xYou","agent":"0xAgent", "maxTotalUnits":"50000000","cosignAbove":"500000"}' // the agent asks whether a spend needs you curl -s https://api.anygas.xyz/svc/api/mandate/cosign-request \ -d '{"mandateId":"0x…","transferId":"fl_…","amount":"1000000"}' // -> { cosignatureRequired: true, digest: "0x…" } // you sign that digest; the agent passes it as `cosignature` to consume
The approval binds the payment, not just the amount. The digest covers the mandate, the transfer and the amount together, so an approval cannot be replayed against a different transfer. Without that, the control would be theatre.
The agent cannot approve itself. A countersignature that does not recover to the principal is refused, and the check happens before any on-chain work — an unapproved large spend is rejected on its face.
Existing grants are untouched. Mandates created before thresholds existed default to cosignAbove: 0 and behave exactly as before. A safety feature must never silently change the meaning of a grant someone already signed.
Watching your budgets
GET /api/mandate/events?principal=0x…&since=<cursor> reports spend, nearly-exhausted, exhausted, approval-required and revoked. Pass the returned cursor back as since to get only what is new. Otherwise a principal has to poll every mandate individually to notice a budget running out or a payment waiting on them.
Why this is a pull feed and not a webhook. A mandate is created by an untrusted caller. Letting it name a callback URL would hand anyone a request primitive originating inside a host that holds signing keys — a textbook SSRF. The convenience of a push is not worth that, so you poll a cursor instead.
Finding what you granted
GET /api/mandate/list?principal=0x… returns every mandate you have granted, live ones first, with the total still drawable without further approval. You cannot revoke what you cannot find, and expecting anyone to have kept every mandate id is how a forgotten grant stays live for months.
Withdrawing a mandate
A budget you cannot withdraw is not a safety control. The moment you most need the limit is the moment you want it gone — not merely capped — and waiting for expiry is not an option.
// the principal signs keccak256("v1|robyn-mandate-revoke|<mandateId>") curl -s https://api.anygas.xyz/svc/api/mandate/revoke \ -d '{"mandateId":"0x…","signature":"0x…"}' // -> { revoked: true, active: false } // afterwards: check -> "REVOKED", consume -> refused
Only the principal can revoke. An agent cannot revoke the budget that constrains it, and a third party cannot revoke someone else’s — both get a 401.
Revocation is irreversible on purpose. A kill switch that can be flipped back is one an attacker who briefly holds the key can undo. To let the agent spend again, issue a fresh mandate — an explicit, auditable act.
What it does not do. It stops Robyn endorsing further spend. It does not claw back money already spent, and it does not stop the agent spending elsewhere with the same key — if the key itself is compromised, rotate the key. The consumption record is kept so the history stays auditable.
Proving the agent behaved
GET /api/mandate/certificate returns a signed document listing every counted spend, the budget, the total, and whether it stayed inside. This is what a principal actually wants at the end of a period — not a dashboard, but something they can verify and keep.
It certifies adherence to a Robyn budget. It does not certify that the agent spent nothing elsewhere, and the payload says so in doesNotCertify rather than in a footnote — a principal reading it as proof of total spending would be badly misled.
What it is not. A mandate covers spending through Robyn. It is not custody and it cannot stop an agent spending elsewhere with the same key. We say so in the payload, because selling this as a hard cap would drain someone who believed they were protected.
What we can actually do — signed, and dated
Deciding whether to depend on a corridor, you normally get a status page — a claim about now that has evaporated by the time anything goes wrong. When a workflow fails later, nobody can reconstruct what the provider said it could do at the moment you committed. You cannot show you chose sensibly; we cannot show we advertised honestly.
A capability snapshot is a signed, timestamped record. Archive it with your decision.
curl -s https://api.anygas.xyz/svc/api/capability/snapshot // -> { serviceableNow: [10, 137, 8453, 42161], // policyEligible: [10, 56, 137, 8453, 42161, 43114], // notServiceable: [56, 43114], // eligible, but no inventory right now // rails: { floatlane: {executeReady:true}, cctp: {…} }, // deliveryBasis: "estimate", // only 2 measured settlements so far // signature: "0x…", signer: "0x8EdE…DFb2" }
Use serviceableNow, not policyEligible. A chain can be eligible by policy while the lane holds no inventory there — the instant lane is then unavailable and the transfer falls through to a bridge rail. This is inventory truth, not a marketing list, and inflating it would invalidate the signature.
It is a record, not a guarantee. It states what was true at that instant. It does not promise the same capability a minute later, and a signature on it does not turn it into a service-level agreement — whatThisIsNot says exactly that in the payload. Verification also reports ageMinutes and warns when a snapshot is stale.
All the evidence, in one signed bundle
Robyn can answer four separate questions about a transfer: did it really happen, where did every unit of the fee go, was this the better route, and is the quoted delivery time measured or guessed. Four calls is work an agent will skip — and four unrelated JSON blobs are not something a principal can meaningfully check.
An evidence bundle is one call and one signature over the assembled whole. It is the artefact you hand to someone who was not there.
curl -s "https://api.anygas.xyz/svc/api/evidence/bundle/fl_reconproof_msaxh1ls\ ?recipient=0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2" // -> { verdict: "verified", // outcome: { verdict: "verified", checks: [ …4 on-chain checks… ] }, // fees: { difference: "0", breakdown: [ …payee + basis per line… ] }, // counterfactual: { difference: "4500", verdict: "…delivered more…" }, // delivery: { basis: "estimate" }, // signature: "0x…", signer: "0x8EdE…DFb2" }
Read verdict before relying on it. The bundle verdict is the weakest of its parts, never the strongest. A complete fee breakdown does not make an unverified transfer settled — if verdict is undetermined, settlement was not confirmed and it must not be described as complete.
Missing parts are declared, not dropped. Anything that could not be produced appears as unavailable with a reason and is listed in incomplete. A bundle missing its on-chain check must not read like one that passed.
Every component is bound by the signature. Rewrite a fee line, flip an on-chain check, swap the transfer id or restamp the issue time and verification fails — the digest is recomputed from the submitted parts. Otherwise a bundle would be a convenient way to launder a false conclusion through a genuine signature.
Ask another agent for money — verifiably
Agents can pay each other. Until now they could not ask each other in any way the payer could check. A message saying “send 5 USDC to 0x…”, sent over a channel with no authentication, is exactly what address-substitution theft preys on — the payer cannot tell whether that address was altered in transit.
A Robyn payment request is a canonical object signed by the payee. Rewrite the payee address, the amount, the chain or the token and the signature no longer recovers to them.
Robyn never signs these and holds no payee keys. A request signed by us would prove only that we said so — which is precisely the trust this is designed to remove.
// 1. PAYEE builds the request curl -s https://api.anygas.xyz/svc/api/payreq/prepare \ -H 'content-type: application/json' \ -d '{"payee":"0xYourAddress","chain":8453,"token":"USDC", "amount":"1000000","memo":"invoice 42","expiresInSec":3600}' // -> { request: {...}, digest: "0x…" } // 2. PAYEE signs the digest with their OWN wallet const signature = await wallet.signMessage(digest); // 3. PAYER verifies BEFORE paying — this is the step that stops the theft curl -s https://api.anygas.xyz/svc/api/payreq/verify \ -H 'content-type: application/json' -d '{"request":{...},"signature":"0x…"}' // -> { valid: true, signedByPayee: true, expired: false } // If signedByPayee is false: DO NOT PAY. That is a redirected address. // 4. PAYER pays it, then proves they did curl -s https://api.anygas.xyz/svc/api/payreq/settlement \ -H 'content-type: application/json' \ -d '{"request":{...},"signature":"0x…","transferId":"fl_…"}' // -> { paid: true, verdict: "verified" } — checked against the destination chain
What a valid signature does not mean. It proves the request is authentic and unaltered. It does not mean you owe the money, and it says nothing about whether the payee is trustworthy. Authenticity is not authorisation.
Over MCP — and verifiable offline
The hosted MCP endpoint is https://api.anygas.xyz/mcp (Streamable HTTP). Everything on this page is exposed as a tool, so an agent in Claude or Cursor does not have to hand-roll HTTP:
robyn_verify_outcome— check a transfer against the destination chain; assertrecipient/minAmount. The verdict is three-valued, andundeterminedmeans unknown, never success.robyn_plan— validate a whole multi-step plan before paying for step one.robyn_spend_statement— signed proof of what was spent.robyn_fee_forensics— where every unit of a fee went.robyn_delivery_stats— measured settlement times.robyn_refusal_verify— verify a signed refusal receipt.
…alongside the tools for quoting, preflight, submission, netting, EIP-7702 and yield. New in 1.3:
robyn_settle— the one-verb settlement primitive (best route + gasless method + privacy posture + what the receipt will prove).robyn_message_info,robyn_message_directory,robyn_message_mailbox_of,robyn_message_feed,robyn_message_send— sealed, unlinkable, post-quantum wallet messaging. The server is a relay:sendaccepts only material you sealed locally (msg2-sdk), so the MCP server never sees plaintext or a recipient.robyn_message_publish_mailbox(pre-signed),robyn_message_anchor_proof,robyn_message_erasure.robyn_plus_trial(20 free passes/day),robyn_plus_info,robyn_plus_invoice,robyn_plus_invoice_status,robyn_plus_claim— blind passes for capacity, funded cover and anchoring; paid in ETH/USDC/BTC, unlinkable to the payment.robyn_posture, receipt delivery/verification, attestation v2 — see the changelog.
Transport: reads and planning ride the BATMAN OHTTP relay by default (every result carries _lane); execute/submit/purchases go direct and say so. Shape: every tool result is padded to an exact 4 KiB / 16 KiB / 64 KiB wire size (ignore the trailing _pad key), so no tool — messaging included — is identifiable by response length. Hosted: https://anygas.xyz/mcp (Streamable HTTP). Local: npx anygas-mcp — the same 66-tool registry.
Verify without trusting anyone
Signed statements and refusal receipts can be checked with no network call at all. The SDK recomputes the signed digest from the data you hold and recovers the signer locally — so a principal can audit an agent’s evidence without trusting the agent, the SDK, or Robyn.
import { verifyStatementOffline, verifyRefusalOffline } from 'https://anygas.xyz/sdk.js'; // or vendor it locally // ROBYN_SIGNER is the published relayer address you already trust. const v = verifyStatementOffline(statement, ROBYN_SIGNER); // -> { valid: true, recoveredSigner: '0x8EdE…DFb2', boundToTrustedSigner: true } // Change any amount in `statement` and valid becomes false. const r = verifyRefusalOffline(receipt, ROBYN_SIGNER); // -> proves the refusal HAPPENED. Not that it was correct.
Pass the expected signer. Without it you only learn the document is self-consistent with some key — which is not the same as knowing Robyn signed it, and the SDK says so in the response.
Honest caveats
- MIT clients, proprietary relay. The SDKs / MCP server / adapters are MIT-licensed. The hosted relayer, routing service and contracts are proprietary — build on Robyn; do not replicate or resell the service. See Terms.
- Settles only if value lands. Execute is honest: for lanes where the relayer isn't pre-funded (most non-EVM sends) it returns
FUNDS_REQUIRED/ anAWAITING_DEPOSITaddress rather than inventing a success. A route reachesDONEonly when the destination actually receives. - Small transfers are dominated by fixed costs. Bridge ingress/egress + network fees are largely size-independent; the compare
guardflags when your amount is uneconomical and suggests arecommendedMinInput. - Quote-only expanded lanes. Sui (EVM/Solana/Stellar↔Sui), ETH→BTC and SOL→BTC, plus Scroll and the extra Chainflip asset lanes, are quotable via /compare but not execute-ready (
executeReady:false) — Robyn holds no signer for those source assets yet. They are excluded from the headlinenodes/directedRoutescounts on purpose. - qualityScore is advisory. It blends output, speed and a static per-rail reliability prior (a heuristic, not a measured success rate).
bestis always ranked by netestOut, never by the score.
What changed, and how to check it yourself
Privacy claims you cannot verify are marketing. Everything below is checkable without asking us, and the failures we found in our own system are listed alongside the fixes.
Deferred withdrawals — POST /api/pool/withdraw with "hold": {}
Timing is the strongest deanonymiser in a small pool: deposit at 14:02, withdraw at 14:09, and no
cryptography saves you. A held withdrawal waits for an exponentially distributed delay and for the
anonymity set to reach the floor you named. It is opt-in per request — omit hold and
behaviour is exactly as before. Poll GET /api/pool/hold?ticket=; the ticket carries no
information about your note.
The subtle part: we do not release the moment your floor is met. The set grows when somebody deposits, and that deposit is a public timestamped event — firing on it would weld your exit to that entry, which is a tighter link than not queueing at all. A second, independent delay is drawn at that moment.
Merkle paths: use /api/pool/tree
Asking for a path with ?commitment= tells us which deposit is yours. That form is no longer
served by default; fetch the whole tree and compute your path locally, which reveals nothing.
Cover traffic is now auditable — GET /api/stealth/coverproof
We publish a Merkle root over real payouts, the real count (kUsers) and the observed count
including cover (kObserver), bound in a signed commitment fixed before you ask. You can prove
your own payout was in a tick. We never publish which entries were cover — the cover set names the
real set by complement.
A defect we shipped, and are telling you about
Our cover announcements derived their viewTag from the ephemeral public key, which is
published in the announcement itself. Anyone could recompute that relation and identify every cover entry
for free. 21 of 31 announcements in the live feed carried that mark, so the anonymity set
was 10, not 31. It is fixed. A second, independent tell then showed up: an announced address that never received value is identifiable by one balanceOf call, no cryptography needed. Measured together, 21 of 31 announcements are identifiable by the viewTag relation and the remaining 10 by never having been funded, with no overlap — so the true anonymity set of the historical feed is zero, not the 10 an earlier version of this page reported. Cover emitted from now on is funded and derived correctly; the historical entries are counted, never deleted. Historical
entries are counted rather than deleted: a genuine announcement matches the same relation about 1 in 256,
and deleting one would strand a recipient whose money is waiting at that address.
Constant-shape replies and constant-rate traffic
Every reply over the private (OHTTP) channel is padded to one fixed size — a refusal, a quote, a tree
fetch and a withdrawal are all the same number of bytes. GET /api/privacy/noop is a stateless,
unlogged no-op so a client can speak on a fixed schedule whether or not it has anything to say.
Retention
Counterparty addresses in settled routes age into a keyed tag after 30 days: anyone holding an address can still confirm a match, nobody can enumerate who we paid.
Policy changes are logged — GET /api/privacy/policy
A hash-chained, signed record of every change to what we promise, each entry marked tightened, loosened or neutral. Entries commit to their predecessor, so nothing can be edited, reordered or removed without breaking every entry after it. Verify it yourself; the method is in the response.
Check our cryptography — stealth-review-bundle.json
The same author wrote our stealth implementation, its tests and the harness that audits it, so our own tests cannot tell you whether we read ERC-5564 correctly. The bundle contains 20 deterministic vectors, reproducible from a seed, with the algorithm in prose. Reimplement it and compare. We would rather you found a mistake than trusted us not to have made one.