Robyn AnyGas

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.

No key required for reads Reads are safe & side-effect free v3.1 · live

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.

1

See the mesh — every chain and rail that is live right now, with chain ids you will use next.

GET /svc/api/route/chains
curl -s https://anygas.xyz/svc/api/route/chains
2

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.

GET /svc/api/route/quote?…
curl -s "https://anygas.xyz/svc/api/route/quote?fromChain=8453&toChain=42161&fromToken=USDC&toToken=USDC&amount=25000000"
3

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.

POST /svc/api/route/execute (sandbox)
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.

mcp.json
{
  "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_status

JavaScript / TypeScript

agent.ts
// 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

agent.py
# 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 · first quote
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"
Sandbox modeBuild and test the full 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.

by problem
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 URLhttps://api.anygas.xyz/svc. The gateway strips /svc and proxies to the service. All paths below are shown as /api/…; prefix them with /svc over 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% (25 bps) from the token in flight; it is never taken in native gas. Every quote returns robynRouteFeeBps.
  • 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"}.
  • Errors4xx/5xx with {"error":"…"}. Reads degrade gracefully rather than throw.

Preflight — check before you spend

Free · read-onlyCall this first

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 · preflight
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

No bridge in the path$0.10–$25

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

No funds · no keySame shape as live

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 · sandbox execute
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"}'
200 · verified response (trimmed)
{
  "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 }).

Related endpoints: 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

Measured uptimeGET /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.
GET/api/status60 / min

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.

200 · verified (trimmed)
{
  "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 }, /* … */ ]
}
GET/api/route/chainsno key

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.

200 · verified (trimmed)
{
  "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 */ }
}
Read the flagsNodes counted in 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.
GET/api/gasless/infono key

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.

200 · verified (trimmed)
{
  "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

POST/api/route/quote40 / min

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 or GETSend the fields as a JSON 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).
FieldTypeNotes
fromChainid / stringrequiredSource node.
toChainid / stringrequiredDestination node.
amountstringrequiredSource-token smallest units.
fromTokenstringoptionalSymbol or address; omit for native.
toTokenstringoptionalSymbol or address; omit for native.
toAddressstringoptionalRecipient; defaults to fromAddress.
fromAddressstringoptionalQuote origin; anonymous default used if omitted.
slippagenumberoptionalFraction, e.g. 0.005.
curl · quote to native Bitcoin
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…"}'
200 · verified response (trimmed)
{
  "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.

POST/api/route/execute8 / min

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.

FieldTypeNotes
fromChain,toChain,amountrequiredAs in quote (smallest units).
fromToken,toToken,toAddressstringoptionalToken symbols/addresses; recipient.
modestringoptional"permit2" (user-signed pull) or "self" (operator moves own funds). Default "self".
permit2objectcond.Required for permit2: {owner, permitted:{token,amount}, nonce, deadline, signature}.
Header · x-anygas-sandbox: 1Set this header to simulate the whole flow with no funds — same response shape, a 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.
200 · live response shape (from router)
{
  "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"
}
GET/api/route/status?id=<id>no key

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.

200 · verified sandbox status
{
  "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

GETPOST/api/route/compare30 / min
Read-only · quote-only

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).

200 · verified — Arbitrum USDC → Solana USDC (2 rails ranked)
{
  "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
}
The guard protects small transfersWhen the best rail's effective fee at the requested size exceeds ~3%, guard.ok is false with a warning and a best-effort recommendedMinInput — fixed bridge costs dominate tiny moves.
POST/api/intent/solve20 / min
Read-only · returns a plan

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.

FieldTypeNotes
toChain,toTokenrequiredDestination node + asset.
fromarrayrequired1–10 sources: [{chain,token,amount}].
goalstringoptionalDefault max-output.
amountstringoptionalShared amount if sources omit their own.
toAddressstringoptionalRecipient, echoed into plan.execute.
curl · pick the best of two balances
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"}]}'
200 · verified response shape
{
  "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."
}
GET/api/routes/recent?limit=N60 / min

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.

200 · verified response
{
  "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

Gated · dry-run by default
These plan & store — they do not move fundsBatch / schedule / conditional only build a plan and write a JSON record. Execution belongs to a separate watcher and fires only under a double gate: the watcher process must run with 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.
POST/api/route/batch30 / min

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.

200 · verified (live:false, per-leg plan)
{
  "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."
}
POST/api/route/schedule30 / min
GETDELETE/api/route/schedule/<id>

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.

200 · verified (hourly ×24, simulated)
{
  "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" }
POST/api/route/conditional30 / min
GETDELETE/api/route/conditional/<id>

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).

200 · verified (armed, simulated)
{
  "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.

POST/api/agent/do60 / min

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.

GET/api/errors

The complete error contract. Every failure response carries errorCode, retryable, retryAfterMs and suggestedAction alongside the legacy error string. Branch on errorCode.

GET/api/route/stream?id=20 / min

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.

GET/api/keys/quota

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.

GET/api/7702/info30 / min

Chains, caps, pricing and the full flow.

GET/api/7702/delegate30 / min

The canonical batch-executor delegate: per-chain deployments, ABI, and the EIP-712 scheme for executeSigned.

POST/api/7702/check30 / min

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.

POST/api/7702/sponsor30 / min

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

POST/api/keys/create5 / hour

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>.

200 · verified
{
  "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."
}
GET/api/keys/usagekey required

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.)

200 · verified
{
  "key": { "label": "docs-example", "created": 1784012809055 },
  "hour": { "total": 0, "errors": 0, "topPaths": [] },
  "day":  { "total": 0, "errors": 0, "topPaths": [] },
  "recent": []
}
POST/api/hooks/register120 / min

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>.

200 · verified
{
  "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."
}
SignatureIf you set secret, each delivery carries x-anygas-signature: sha256=HMAC(secret, rawBody) — verify it before trusting the payload.
POSTGET/api/session/*owner-gated
Owner key required to mint

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/issueowner mints a session + bearer (returns the address to fund).
  • POST /api/session/executebearer (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/listowner 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

No funds · no keyRead-only

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 · plan two hops under a budget
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}}'
200 · infeasible, and it tells you exactly why
{
  "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

No funds · no keyRead-only

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 · assert who was paid and how much
curl -s "https://api.anygas.xyz/svc/api/outcome/verify/fl_reconproof_msaxh1ls\
?recipient=0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2&minAmount=1000000"
200 · each assertion checked on-chain
{
  "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

No keySigned

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 · issue, then verify
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

No funds · no keyRead-only

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

No funds · no keyMeasured

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

Sandbox onlyEvery endpoint

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_LIMITED and DUPLICATE_REQUEST are deliberately not rehearsable — an authorisation or duplicate-detection result must never be fakeable.
  • GET /api/errors lists every code you can rehearse.
curl · rehearse an out-of-inventory failure on execute
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

Opt-inSigned

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 · get a receipt for a refusal
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?

No keyAdvisory

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

No funds · no keyRead-only

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 · the whole picture
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?

No keyRead-only

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 · lint before you ship
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

No keyRead-only

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 · the playbook for paying safely
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

No keyRead-only

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 · only what affects you
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?

No keySigned

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 · the whole decision
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?

No keyRead-only

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 · a $2 transfer to Linea
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

No keyPrincipal-signed

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.

the flow
// 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.

curl · a mandate with a co-sign threshold
// 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.

curl · revoke (principal signs)
// 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

No keySigned

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 · capability snapshot
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

No keySigned

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 · one call, everything
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

No keyPayee-signed

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.

the flow
// 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

Zero install66 tools

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; assert recipient / minAmount. The verdict is three-valued, and undetermined means 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: send accepts 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.

offline verification — no request to 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 / an AWAITING_DEPOSIT address rather than inventing a success. A route reaches DONE only when the destination actually receives.
  • Small transfers are dominated by fixed costs. Bridge ingress/egress + network fees are largely size-independent; the compare guard flags when your amount is uneconomical and suggests a recommendedMinInput.
  • 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 headline nodes/directedRoutes counts on purpose.
  • qualityScore is advisory. It blends output, speed and a static per-rail reliability prior (a heuristic, not a measured success rate). best is always ranked by net estOut, 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.

Rail independence — we quote our own price

Most gasless relays are a thin wrapper over somebody else's bridge aggregator. When that aggregator rate-limits or goes down, the wrapper has nothing to say. Robyn holds its own USDC inventory on the chains it serves, so it can quote and settle a transfer with no third party in the path at all.

The float lane competes on every quote

POST /api/route/compare ranks Robyn's own float lane alongside LI.FI, Bungee, deBridge, Allbridge and Chainflip. It wins on price for the sizes it can serve, and when it cannot serve a size it says so plainly rather than quietly disappearing:

{
  "best": "floatlane",
  "firstParty": {
    "available": true,
    "estOut": "4996000",
    "dependsOnThirdParty": false,
    "bestThirdParty": { "rail": "bungee", "estOut": "4989288" },
    "deltaUnits": "6712",
    "savingsPct": 0.1343
  }
}

The firstParty block is computed per request from the two quotes actually returned. It is a measurement, not a marketing figure — and when the lane cannot serve, it reports available:false with the reason instead of implying a saving that was not on offer.

Multi-endpoint RPC, with automatic failover

Every chain Robyn serves is configured with three to four independent RPC endpoints. One is active at a time and the pool switches when it degrades. Endpoints are never raced against each other: racing can return a stale nonce from a lagging node, and the payout path signs against that nonce. Check live state at GET /api/rpc/health.

What still depends on someone else

Honesty matters more than the claim. Transfers larger than the lane's per-chain capacity, and any token pair Robyn does not stock, still route over third-party bridges — the comparison above shows exactly when that happens. Robyn does not issue USDC and does not produce blocks.

CCTP transfers always finish — we run the mint ourselves

Circle’s CCTP moves native USDC by burning it on one chain and minting it on another. That is two transactions with an attestation in between, and somebody has to submit the second one. Many integrations quietly rely on Circle’s own relayer to do it. When it does not, the funds sit burned and unminted.

Robyn runs a keeper (robyn-cctp-mint, every 2 minutes) that finishes them. Every CCTP transfer gets a tracked route id the moment the burn lands, so you can watch it settle:

GET /api/route/status?id=rt_...
  → BRIDGING     burn confirmed, waiting on Circle
  → MINT_READY   attested; mint not yet submitted (funds safe, replayable)
  → DONE         minted, with destTx

You can also finish one yourself with POST /api/route/resume {"id":"rt_..."}. It is safe to call more than once: CCTP nonces are single-use, so a duplicate mint cannot double-spend — it reverts and is reported as ALREADY_MINTED.

Why this matters for cost. CCTP charges no bridge fee — only gas. Because the mint leg is now handled automatically, Robyn uses CCTP to move its own inventory between chains instead of paying a third-party bridge. On a $25 rebalance that is the difference between gas and $0.05–$0.25 of bridge fees, and that saving is what keeps the instant float lane cheap for you.

Before you call execute: it broadcasts by default

POST /api/route/execute sends real transactions. Leaving live out of the body does not make it a simulation — that is the single easiest way to lose money against this API, and it has happened. The dry paths are explicit:

{"live": false}            → prepared route, nothing broadcast
{"dryRun": true}          → same
header x-anygas-sandbox:1 → fully simulated route (sbx_ ids, no funds)

Guards that protect you

Binding execution to your quote

minOut is enforced by Robyn on the float lane: the transfer is refused with BELOW_AGREED_OUTPUT before custody changes. On third-party bridge rails it is passed through as the bridge's own slippage floor — the guarantee there is the bridge's, not ours. If you need Robyn itself to refuse an under-delivering route, use POST /api/floatlane/execute.

Privacy: unlink who you are, without giving up proof

Robyn is built on provability — signed statements, verifiable outcomes, receipts. Privacy that destroyed those would cost you the thing that makes this relay worth using. So the privacy here removes information from public chains and counterparties, while keeping proof available to the people who need it.

Stealth addresses (ERC-5564)

Publish one meta-address. Every payer derives a fresh address from it, so two payments to you are unlinkable on-chain and the payer never learns where your money lives.

POST /api/stealth/keys        → metaAddress + your two private keys (keep them)
POST /api/route/execute {"toStealth": "st:eth:0x...", ...}
GET  /api/stealth/announcements  → scan with your viewing key, locally

We never accept your viewing key. Scanning happens on your machine. Handing us the viewing key would let us link every payment to you — exactly what this feature exists to prevent.

What the float lane already hid

The instant lane pays out of Robyn's own inventory, so the destination chain sees a payment from Robyn, not from you. Your address never appears there. Stealth addresses remove the other half of the linkage.

Netting: transfers that never touch a chain

A transfer between two enrolled netting accounts is a ledger entry. No transaction, no gas, no chain observer. It is the strongest privacy available here, and it has always been free.

Solvency without disclosure

Custodial balances were previously proven safe by publishing the totals. Now there is a Merkle sum tree: we publish a root and a total, and you can request an inclusion proof that your balance is inside it.

GET  /api/netting/proof/root   → root, totalUnits, backingUnits, solvent
POST /api/netting/proof        → your proof (sign the challenge; it reveals your balance)

What we do NOT claim

PRIVATE · SEALED · POST-QUANTUM — MESSENGER

Sonar — sealed, unlinkable, post-quantum messaging

Broadcast to everyone. Understood by one. Remembered by none.

Wallet-to-wallet messages that the server cannot read, attribute or route-trace. Built from the same primitives as the rest of the privacy stack, so a message is the same envelope as a settlement:

What this does and does not do for anonymity. Rotating handles give unlinkability — no graph, no long-lived pseudonym — and because a message is byte-indistinguishable from any other private-lane operation, messages are honest cover for settlement. They do not raise the anonymity-set size K: one principal under a thousand handles is still one principal, and v2 reports kContribution: 0 rather than pretend otherwise. K rises when distinct real principals share the lane — and then their traffic is already indistinguishable from yours.

Private by default — one wire for everyone (2026-08-20)

Every client now reaches Robyn through the BATMAN relay by default (relay.nightferry.net, Iceland, separate operator): hybrid ML-KEM-768+X25519 envelopes padded to one size (16 KiB request / 8 KiB response), so a quote, a sealed message, a pass redemption and a cover row are identical bytes to the relay. GET /api/ohttp/keys publishes the key and the reachable-path allowlist; execute/submit/purchases stay direct on purpose and every result says which lane it used. A relay failure is an error, never a silent downgrade. MCP: ANYGAS_LANE=private|strict|direct · browser: AnyGas.lane · Node SDK: ANYGAS_LANE.

Anonymous quota. A one-time Plus blind pass is the anonymous API key: x-anygas-pass: base64(JSON {preparedMsg,sig}) (or body.pass over the relay) exempts that request from the limiter and links to nothing — not to a payment, not to another request. Free: POST /api/plus/trial (20/day).

Before you act: GET /api/posture?chainId= — what is private and what is public for that chain and the lane you are on. Receipts: POST /api/receipt/deliver {to, receipt} seals a relayer-signed settlement receipt into the counterparty's v2 mailbox; POST /api/receipt/verify checks one. Code attestation: GET /api/route/attestation signs the hash of every deployed module and the exact SRI of /svc/anygas-web.js — compare before trusting a loaded SDK.

Messaging v1 is retired (writes return 410). One messenger: sealed v2, maximum posture by default — there is no "normal" tier, because a separate private tier would be a smaller crowd and a label.

Node SDK: /svc/msg2-sdk.mjs (npm i ethers @noble/curves @noble/post-quantum). Browser: /svc/anygas-web.js exposes window.AnyGas.{msg2, plus, vault} — pure WebCrypto, byte-compatible with the Node SDK and the server (verified 11/11 cross-tests), with an encrypted localStorage vault for identities and passes. Service page: /svc/messages. MCP: robyn_message_info / directory / mailbox_of / feed / send / publish_mailbox on /mcp — a relay only; sealing and signing stay on your machine. Status and parameters: /api/messages/v2/info.

// npm i ethers @noble/curves @noble/post-quantum
import * as msg2 from './msg2-sdk.mjs';          // https://anygas.xyz/svc/msg2-sdk.mjs
import { ethers } from 'ethers';

const wallet = new ethers.Wallet(process.env.PK);
const me = msg2.createIdentity();               // keep viewPriv / spendPriv / kemSecret secret; persist them
await msg2.publishMailbox(wallet, me);          // once: address -> (stealth meta-address, ML-KEM public key)

await msg2.send('sphynx.robynchain.eth', 'gm', { wallet });   // sealed; one-time handle; server stores no from/to
const mine = await msg2.inbox(me);              // scan the public feed with your viewing key, open what is yours
// mine[0] -> { inner:{text,ts}, sender:'0x…' | null, authenticated, releaseAt }

Measured: a message is indistinguishable from any other operation

Claimed uniformity is worthless unless measured, so it is measured (robyn-uniformity-test.mjs, re-runnable on the box). Ten operation types — sealed send, feed read, directory, info, route compare, status, privacy status, stealth feed, pool tree, and a refused Plus call — were each sent 20× through the real BATMAN relay with the same PQ envelope the SDKs use. What an observer on the wire sees:

This is what makes messaging honest cover for settlement: every sealed message that rides the private lane is one more envelope nobody can tell apart from a payment. The browser bundle (AnyGas.lane, default private) and the Node SDK (ANYGAS_LANE, default private) both use the relay by default and refuse to downgrade silently. The MCP hop is shaped too: every tool result is padded to an exact 4 KiB / 16 KiB / 64 KiB wire size (a trailing _pad key JSON consumers ignore), so robyn_message_* results are byte-equivalent to any other tool’s. Residual: on the direct lane (a raw fetch to /api/…) sizes and paths do differ by operation, and we see your IP — content, sender and recipient stay sealed either way.

Plus — capacity, assurance and funded cover, paid in crypto, redeemed anonymously

Why there is no "more private tier". Privacy is the size of the crowd you are indistinguishable from. A paid lane would be a smaller crowd, and the payment itself the strongest identity link in the system. So the wire is identical for paying and non-paying users; Plus sells what sits around the privacy.

SDK: /svc/plus-sdk.mjs · encrypted pass wallet + CLI: /svc/plus-wallet.mjs · browser: window.AnyGas.plus from /svc/anygas-web.js (trial, invoice, pay with injected wallet, claim, spend) · free trial: POST /api/plus/trial (20 passes/day, no payment) · Service page: /svc/plus · Status: /api/plus/info · MCP: robyn_plus_info / invoice / invoice_status / claim.

// npm i ethers @cloudflare/blindrsa-ts @noble/curves @noble/post-quantum
import * as plus from './plus-sdk.mjs';   // https://anygas.xyz/svc/plus-sdk.mjs
import * as msg2 from './msg2-sdk.mjs';

const inv = await plus.invoice({ asset: 'eth-8453' });                 // or eth-4663 | eth-1 | eth-42161 | usdc-8453 | usdc-42161 | btc
const tx = await wallet.sendTransaction({ to: inv.payTo, value: BigInt(inv.amount) }); await tx.wait();
const passes = await plus.claim(inv.invoiceId, tx.hash);               // 500 unlinkable bearer passes — store them like cash

await msg2.send('sphynx.robynchain.eth', 'gm', { wallet, pass: passes.pop() });   // rate-limit exempt, still unlinkable
await plus.fundCover(passes.pop());                                    // +1 cover row for everyone, some time in the next 24 h
const a = await plus.anchor(passes.pop(), rowId);                      // signed (optionally on-chain) Merkle inclusion
console.log(await plus.verifyAnchorProof(await plus.anchorProof(rowId)));