{
 "openapi": "3.1.0",
 "info": {
  "title": "Robyn AnyGas API",
  "version": "3.28.2",
  "summary": "Gasless, cross-chain relay for AI agents.",
  "description": "Move value across 22 EVM chains, Stellar, Solana and native Bitcoin with one signed intent and no native gas. A relayer fronts the gas and is repaid from the token being moved. Read endpoints need no credentials; execution takes a Permit2 / EIP-712 signature. Add the header `x-anygas-sandbox: 1` to POST /api/route/execute to simulate the whole lifecycle with zero funds. Base URL is https://api.anygas.xyz/svc (the gateway strips the /svc prefix). Also available as an MCP server (npx anygas-mcp) and a hosted MCP endpoint (https://api.anygas.xyz/mcp). Client SDKs are MIT-licensed; the hosted relayer, routing service and contracts are proprietary.",
  "contact": {
   "name": "Robyn AnyGas",
   "url": "https://anygas.xyz"
  },
  "license": {
   "name": "Service terms",
   "url": "https://anygas.xyz/svc/api/terms"
  },
  "x-lastUpdated": "2026-07-29"
 },
 "servers": [
  {
   "url": "https://api.anygas.xyz/svc",
   "description": "Production gateway (also https://anygas.xyz/svc)"
  }
 ],
 "tags": [
  {
   "name": "discovery",
   "description": "Health, mesh and gasless config"
  },
  {
   "name": "routing",
   "description": "Quote and execute gasless cross-chain routes"
  },
  {
   "name": "best-execution",
   "description": "Multi-rail comparator and goal-based intent solver (read-only)"
  },
  {
   "name": "explorer",
   "description": "Anonymized recent-routes feed"
  },
  {
   "name": "primitives",
   "description": "Batch / schedule / conditional route primitives (gated, dry-run by default)"
  },
  {
   "name": "keys-hooks",
   "description": "Optional developer keys and completion webhooks"
  },
  {
   "name": "concierge",
   "description": "AI concierge and terms"
  },
  {
   "name": "account",
   "description": "AnyGas Account — yield-native gasless spending, balances, receipts, and event webhooks for agents."
  },
  {
   "name": "agents",
   "description": "One-call and discovery endpoints built for autonomous agents"
  },
  {
   "name": "sponsored-transactions",
   "description": "EIP-7702 type-4 sponsorship: the relayer pays your gas"
  },
  {
   "name": "keys",
   "description": "Optional self-serve API keys, quotas and usage"
  },
  {
   "name": "robyn-services",
   "description": "User services for Robinhood Chain (token checker, premium monitor, gas top-up, fast exit, limit orders, corporate actions)"
  },
  {
   "name": "membership",
   "description": "NFT-based membership: free micro-transfers and reduced fees"
  },
  {
   "name": "float-lane",
   "description": "Robyn's own inventory lane — no bridge in the path"
  },
  {
   "name": "planning",
   "description": "Validate whole multi-step workflows before executing any of them."
  },
  {
   "name": "verification",
   "description": "Check what actually happened against chain state, with caller-supplied post-conditions."
  },
  {
   "name": "transparency",
   "description": "Where every unit went, what it would have cost elsewhere, how long delivery really takes, and signed proof of spend."
  },
  {
   "name": "payments",
   "description": "Verifiable agent-to-agent payment requests: signed by the payee, checkable by the payer, provable on-chain once paid."
  },
  {
   "name": "mandates",
   "description": "Principal-signed spending budgets for autonomous agents, enforced outside the agent and consumed only against on-chain-verified transfers."
  },
  {
   "name": "privacy",
   "description": "Selectable, per-request privacy. Oblivious HTTP, stealth addresses, and the proofs that let you check the claims rather than trust them."
  },
  {
   "name": "messaging",
   "description": "Wallet-to-wallet messaging (v2 sealed + unlinkable; v1 plaintext)"
  },
  {
   "name": "plus",
   "description": "Paid capacity/assurance/cover via unlinkable blind passes"
  }
 ],
 "components": {
  "parameters": {
   "SandboxHeader": {
    "name": "x-anygas-sandbox",
    "in": "header",
    "required": false,
    "description": "Set to \"1\" to simulate the full execute lifecycle with zero funds — no signature, no relayer spend, nothing on-chain. Returns an sbx_ id that progresses BRIDGING -> DONE in ~40s.",
    "schema": {
     "type": "string",
     "enum": [
      "1"
     ]
    }
   },
   "ApiKeyHeader": {
    "name": "x-anygas-key",
    "in": "header",
    "required": false,
    "description": "Optional API key (ak_…). Never required for reads; only tags requests for usage visibility.",
    "schema": {
     "type": "string"
    }
   },
   "AnygasPass": {
    "name": "x-anygas-pass",
    "in": "header",
    "required": false,
    "schema": {
     "type": "string"
    },
    "description": "Anonymous quota raise: base64(JSON {preparedMsg, sig}) of a one-time Plus blind pass (RFC 9474). Exempts THIS request from the rate limit and, unlike x-anygas-key, is unlinkable to any identity or other request. Free passes: POST /api/plus/trial (20/day). Over the OHTTP relay put the pass in body.pass instead (headers do not cross the envelope)."
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "description": "Every error carries a stable errorCode plus recovery metadata. The legacy `error` string is retained for backward compatibility - branch on errorCode.",
    "properties": {
     "error": {
      "type": "string",
      "description": "human-readable message (legacy field)"
     },
     "errorCode": {
      "type": "string",
      "example": "INSUFFICIENT_FLOAT",
      "description": "stable machine code; see GET /api/errors for the full catalogue"
     },
     "retryable": {
      "type": "boolean"
     },
     "retryAfterMs": {
      "type": "integer",
      "description": "present when the failure is transient"
     },
     "suggestedAction": {
      "type": "string",
      "description": "what the caller should actually do next"
     },
     "docs": {
      "type": "string"
     }
    },
    "required": [
     "error"
    ]
   },
   "Status": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "operational",
       "degraded",
       "down",
       "unknown"
      ],
      "example": "operational"
     },
     "ts": {
      "type": "integer",
      "example": 1784012730722
     },
     "nodes": {
      "type": "object",
      "properties": {
       "up": {
        "type": "integer",
        "example": 25
       },
       "total": {
        "type": "integer",
        "example": 25
       },
       "evm": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "chainId": {
           "type": "integer"
          },
          "name": {
           "type": "string"
          },
          "up": {
           "type": "boolean"
          }
         }
        }
       },
       "nonEvm": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "name": {
           "type": "string"
          },
          "up": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "rails": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string"
        },
        "up": {
         "type": "boolean"
        }
       }
      }
     },
     "routes": {
      "type": "integer",
      "nullable": true,
      "example": 500
     },
     "endpoints": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "name": {
         "type": "string"
        },
        "up": {
         "type": "boolean"
        }
       }
      }
     }
    }
   },
   "Mesh": {
    "type": "object",
    "properties": {
     "nodes": {
      "type": "integer",
      "example": 25
     },
     "chains": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "EVM chain ids",
      "example": [
       1,
       10,
       56,
       100,
       130,
       137,
       146,
       480,
       999,
       1135,
       1868,
       4663,
       5000,
       8453,
       33139,
       34443,
       42161,
       43114,
       57073,
       59144,
       81457,
       7777777
      ]
     },
     "routableNonEvm": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "example": [
       "stellar",
       "solana",
       "bitcoin"
      ]
     },
     "directedRoutes": {
      "type": "integer",
      "example": 500
     },
     "relayer": {
      "type": "string",
      "description": "Permit2 spender the SDK signs for",
      "example": "0x1253D25A0B6a757CfD65CDf441E2d91DBeCeE6c5"
     },
     "permit2": {
      "type": "string",
      "example": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
     },
     "engine": {
      "type": "string",
      "example": "LI.FI aggregation (Across / Gas.zip / Near / Squid / Symbiosis / Relay + ~15 bridges); Stellar + Solana via Allbridge Core; Bitcoin via Chainflip"
     },
     "nonEvmNodes": {
      "type": "object",
      "additionalProperties": true,
      "description": "Per-node metadata; quote-only nodes carry executeReady:false"
     },
     "comparatorQuoteOnly": {
      "type": "object",
      "additionalProperties": true,
      "description": "Scroll, extra Chainflip asset lanes and Sui — quotable but NOT execute-ready"
     }
    },
    "additionalProperties": true
   },
   "GaslessInfo": {
    "type": "object",
    "properties": {
     "service": {
      "type": "string",
      "example": "Robyn Gasless Pay"
     },
     "router": {
      "type": "string",
      "example": "0x8f4a466435264A2D56Bc80e6749Ea351d567D141"
     },
     "anyGasRouter": {
      "type": "string",
      "example": "0x298ab4Be2B3a8f1A2dB3650f578407AA1f952487"
     },
     "tokenPaymaster": {
      "type": "string",
      "example": "0x4afc3DD9143bC75fCfFDA52e9E5e50dA4Cb3A635"
     },
     "permit2": {
      "type": "string",
      "example": "0x000000000022D473030F116dDEE9F6B43aC78BA3"
     },
     "entryPoint": {
      "type": "string",
      "example": "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
     },
     "chainId": {
      "type": "integer",
      "example": 4663
     },
     "routes": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "example": [
       "direct",
       "cheapest",
       "fastest"
      ]
     },
     "recommended": {
      "type": "string",
      "example": "direct"
     },
     "gaslessChains": {
      "type": "object",
      "additionalProperties": true,
      "description": "Per-chain deployment map keyed by chain id"
     },
     "acceptedTokens": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "domain": {
      "type": "object",
      "additionalProperties": true,
      "description": "EIP-712 domain"
     }
    },
    "additionalProperties": true
   },
   "RouteRequest": {
    "type": "object",
    "required": [
     "fromChain",
     "toChain",
     "amount"
    ],
    "properties": {
     "fromChain": {
      "description": "Source node: EVM chain id or one of \"solana\"/\"stellar\"/\"bitcoin\".",
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ],
      "example": 42161
     },
     "toChain": {
      "description": "Destination node.",
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ],
      "example": "bitcoin"
     },
     "amount": {
      "type": "string",
      "description": "Source-token smallest units.",
      "example": "50000000"
     },
     "fromToken": {
      "type": "string",
      "description": "Symbol or address; omit for native.",
      "example": "USDC"
     },
     "toToken": {
      "type": "string",
      "description": "Symbol or address; omit for native.",
      "example": "BTC"
     },
     "toAddress": {
      "type": "string",
      "description": "Recipient; defaults to fromAddress."
     },
     "fromAddress": {
      "type": "string",
      "description": "Quote origin; anonymous default used if omitted."
     },
     "slippage": {
      "type": "number",
      "description": "Fraction, e.g. 0.005."
     },
     "token": {
      "type": "string",
      "description": "Shorthand: sets BOTH fromToken and toToken (e.g. \"USDC\"). Accepted on POST bodies and as a GET query parameter. Explicit fromToken/toToken win when present.",
      "example": "USDC"
     }
    }
   },
   "Quote": {
    "type": "object",
    "description": "Single-intent quote. Non-EVM legs return a dedicated shape (shown); EVM<->EVM lanes return best-of-N fields (bridge, bestOf, compared[]).",
    "properties": {
     "gasless": {
      "type": "boolean",
      "example": true
     },
     "from": {
      "type": "object",
      "properties": {
       "chain": {},
       "token": {
        "type": "string"
       },
       "amount": {
        "type": "string"
       }
      },
      "example": {
       "chain": 42161,
       "token": "USDC",
       "amount": "50000000"
      }
     },
     "to": {
      "type": "object",
      "additionalProperties": true,
      "example": {
       "chain": "bitcoin",
       "token": "BTC",
       "estimated": "78660",
       "min": "76693",
       "recipient": "bc1q…"
      }
     },
     "bridge": {
      "type": "string",
      "example": "chainflip"
     },
     "via": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "example": [
       "Chainflip (REGULAR)"
      ]
     },
     "bestOf": {
      "type": "integer",
      "description": "EVM<->EVM: number of aggregators compared",
      "example": 3
     },
     "compared": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      },
      "description": "EVM<->EVM: per-aggregator outputs"
     },
     "durationSec": {
      "type": "number",
      "example": 408
     },
     "robynRouteFeeBps": {
      "type": "integer",
      "example": 25
     },
     "robynRouteFee": {
      "type": "string",
      "example": "125000"
     },
     "quoteOnly": {
      "type": "boolean",
      "description": "TRUE means this corridor can be PRICED but NOT settled: POST /api/route/execute will refuse it with status UNSUPPORTED_DIRECTION. Absent or false means the route is executable. ALWAYS branch on this before presenting a quote as actionable - a quoteOnly route is for price comparison only. Currently set for Solana (both directions) and stellar->evm.",
      "example": true
     },
     "executeReady": {
      "type": "boolean",
      "description": "False alongside quoteOnly:true. Present so a client can gate on a single positive assertion of executability rather than inferring it from the absence of an error.",
      "example": false
     },
     "quoteOnlyReason": {
      "type": "string",
      "description": "Human-readable explanation of why this corridor cannot settle, and what would lift the restriction.",
      "example": "Solana settlement is not proven in either direction - this quote is for comparison only."
     },
     "floatlane": {
      "type": "object",
      "additionalProperties": true,
      "description": "Present when the Robyn float lane can serve this pair (our own inventory on both chains, no bridge in the path). Key fields: `fee` (base units), `effectiveFeeBps`, `pricing.mode` (free-micro-vip | micro-standard | rebalancing-discount | rebalancing-partial | base | adaptive | undercut), `freeTier` (whether the micro tier applied and, if not, exactly which check failed), `vip` (member status and the recognised collections), `capNow` (largest transfer this lane can serve right now = 25% of destination float) and `destFloat`. MICRO TIER: transfers of $0.10-$25 to a low-cost destination are FREE for holders of a recognised member NFT, and cost a fraction of a cent for everyone else - never more than the standard 8bps. Pass `fromAddress` so membership can be recognised.",
      "properties": {
       "fee": {
        "type": "string",
        "example": "0"
       },
       "effectiveFeeBps": {
        "type": "number",
        "example": 0
       },
       "capNow": {
        "type": "string",
        "description": "Largest transfer this lane can serve right now, in base units",
        "example": "15020000"
       },
       "pricing": {
        "type": "object",
        "additionalProperties": true
       },
       "freeTier": {
        "type": "object",
        "additionalProperties": true
       },
       "vip": {
        "type": "object",
        "additionalProperties": true
       }
      }
     },
     "note": {
      "type": "string"
     }
    },
    "additionalProperties": true
   },
   "ExecuteRequest": {
    "type": "object",
    "required": [
     "fromChain",
     "toChain",
     "amount"
    ],
    "properties": {
     "fromChain": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ],
      "example": 42161
     },
     "toChain": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ],
      "example": 8453
     },
     "amount": {
      "type": "string",
      "description": "Smallest units.",
      "example": "5000000"
     },
     "fromToken": {
      "type": "string",
      "example": "USDC"
     },
     "toToken": {
      "type": "string",
      "example": "USDC"
     },
     "toAddress": {
      "type": "string",
      "example": "0x8EdE0eEb8C03a45886836A1baDec03CdB08cDFb2"
     },
     "mode": {
      "type": "string",
      "enum": [
       "self",
       "permit2"
      ],
      "default": "self",
      "description": "\"permit2\" = user-signed off-chain pull; \"self\" = operator moves own funds."
     },
     "permit2": {
      "allOf": [
       {
        "$ref": "#/components/schemas/Permit2SignatureTransfer"
       }
      ],
      "description": "Required when mode=permit2. Build it from signRequest returned by POST /api/agent/do."
     }
    }
   },
   "RouteReceipt": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "rt_1a2b3c4d5e6f"
     },
     "status": {
      "type": "string",
      "description": "BRIDGING | DONE | FAILED (plus FUNDS_REQUIRED / AWAITING_DEPOSIT for unfunded non-EVM lanes)",
      "example": "BRIDGING"
     },
     "gasless": {
      "type": "boolean",
      "example": true
     },
     "sandbox": {
      "type": "boolean",
      "example": false
     },
     "bridge": {
      "type": "string",
      "example": "across"
     },
     "aggregator": {
      "type": "string",
      "example": "lifi"
     },
     "robynFee": {
      "type": "string",
      "example": "12500"
     },
     "bridged": {
      "type": "string",
      "example": "4987500"
     },
     "from": {
      "type": "object",
      "additionalProperties": true
     },
     "to": {
      "type": "object",
      "additionalProperties": true
     },
     "srcTx": {
      "type": "string",
      "nullable": true
     },
     "destTx": {
      "type": "string",
      "nullable": true
     },
     "stage": {
      "type": "string",
      "example": "RECEIVING"
     },
     "elapsedSec": {
      "type": "number"
     },
     "track": {
      "type": "string",
      "example": "/api/route/status?id=rt_1a2b3c4d5e6f"
     }
    },
    "additionalProperties": true
   },
   "CompareResult": {
    "type": "object",
    "properties": {
     "lane": {
      "type": "object",
      "properties": {
       "fromChain": {
        "type": "string"
       },
       "toChain": {
        "type": "string"
       },
       "applicableRails": {
        "type": "array",
        "items": {
         "type": "string"
        }
       },
       "estOutUnit": {
        "type": "string"
       }
      }
     },
     "quotes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "rail": {
         "type": "string"
        },
        "ok": {
         "type": "boolean"
        },
        "estOut": {
         "type": "string"
        },
        "etaSeconds": {
         "type": "number"
        },
        "best": {
         "type": "boolean"
        },
        "effectiveFeePct": {
         "type": "number"
        },
        "qualityScore": {
         "type": "integer"
        }
       },
       "additionalProperties": true
      }
     },
     "best": {
      "type": "string",
      "example": "allbridge"
     },
     "comparedRails": {
      "type": "integer",
      "example": 2
     },
     "railsAttempted": {
      "type": "integer",
      "example": 2
     },
     "guard": {
      "type": "object",
      "properties": {
       "ok": {
        "type": "boolean"
       },
       "effectiveFeePct": {
        "type": "number"
       },
       "thresholdPct": {
        "type": "number"
       },
       "recommendedMinInput": {
        "type": "string"
       }
      }
     },
     "cached": {
      "type": "boolean",
      "example": false
     }
    },
    "additionalProperties": true
   },
   "IntentRequest": {
    "type": "object",
    "required": [
     "toChain",
     "toToken",
     "from"
    ],
    "properties": {
     "toChain": {
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ],
      "example": 8453
     },
     "toToken": {
      "type": "string",
      "example": "USDC"
     },
     "from": {
      "type": "array",
      "description": "1–10 candidate sources.",
      "items": {
       "type": "object",
       "properties": {
        "chain": {},
        "token": {
         "type": "string"
        },
        "amount": {
         "type": "string"
        }
       }
      },
      "example": [
       {
        "chain": 42161,
        "token": "USDC",
        "amount": "5000000"
       },
       {
        "chain": 10,
        "token": "USDC",
        "amount": "5000000"
       }
      ]
     },
     "goal": {
      "type": "string",
      "enum": [
       "max-output",
       "cheapest",
       "fastest"
      ],
      "default": "max-output"
     },
     "amount": {
      "type": "string",
      "description": "Shared amount if sources omit their own."
     },
     "toAddress": {
      "type": "string",
      "description": "Recipient, echoed into plan.execute."
     }
    }
   },
   "IntentPlan": {
    "type": "object",
    "properties": {
     "goal": {
      "type": "string",
      "example": "max-output"
     },
     "to": {
      "type": "object",
      "properties": {
       "chain": {},
       "token": {
        "type": "string"
       }
      }
     },
     "plan": {
      "type": "object",
      "nullable": true,
      "description": "Winner (null if nothing quoted). plan.execute is ready to POST to /api/route/execute.",
      "properties": {
       "from": {
        "type": "object",
        "additionalProperties": true
       },
       "rail": {
        "type": "string"
       },
       "estOut": {
        "type": "string"
       },
       "qualityScore": {
        "type": "integer"
       },
       "execute": {
        "type": "object",
        "additionalProperties": true
       }
      },
      "additionalProperties": true
     },
     "alternatives": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "skipped": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "sourcesConsidered": {
      "type": "integer",
      "example": 2
     },
     "note": {
      "type": "string"
     }
    },
    "additionalProperties": true
   },
   "RecentRoutes": {
    "type": "object",
    "properties": {
     "routes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "lane": {
         "type": "string",
         "example": "Arbitrum → Bitcoin"
        },
        "asset": {
         "type": "string",
         "example": "USDC → BTC"
        },
        "rail": {
         "type": "string",
         "example": "Chainflip"
        },
        "status": {
         "type": "string",
         "example": "pending"
        },
        "when": {
         "type": "string",
         "example": "4h ago"
        },
        "size": {
         "type": "string",
         "example": "small"
        },
        "sandbox": {
         "type": "boolean",
         "example": false
        }
       }
      }
     },
     "count": {
      "type": "integer",
      "example": 2
     },
     "note": {
      "type": "string"
     }
    }
   },
   "BatchRequest": {
    "type": "object",
    "required": [
     "routes"
    ],
    "properties": {
     "routes": {
      "type": "array",
      "description": "Up to 20 legs; toAddress required per leg.",
      "items": {
       "type": "object",
       "properties": {
        "fromChain": {},
        "fromToken": {
         "type": "string"
        },
        "toChain": {},
        "toToken": {
         "type": "string"
        },
        "amount": {
         "type": "string"
        },
        "toAddress": {
         "type": "string"
        }
       }
      }
     },
     "live": {
      "type": "boolean",
      "default": false,
      "description": "Ignored unless the watcher runs with PRIMITIVES_LIVE=1."
     }
    }
   },
   "BatchResult": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "batch_086f0679dfac4a905d"
     },
     "status": {
      "type": "string",
      "example": "planned"
     },
     "live": {
      "type": "boolean",
      "example": false
     },
     "legs": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "warnings": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "note": {
      "type": "string"
     }
    },
    "additionalProperties": true
   },
   "ScheduleRequest": {
    "type": "object",
    "required": [
     "route"
    ],
    "properties": {
     "route": {
      "type": "object",
      "additionalProperties": true,
      "description": "A single route object {fromChain,fromToken,toChain,toToken,amount,toAddress}."
     },
     "everySeconds": {
      "type": "integer",
      "minimum": 10,
      "description": "Omit for a one-shot."
     },
     "at": {
      "description": "Epoch or ISO time for a one-shot start.",
      "oneOf": [
       {
        "type": "string"
       },
       {
        "type": "integer"
       }
      ]
     },
     "count": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
     },
     "live": {
      "type": "boolean",
      "default": false
     }
    }
   },
   "ScheduleResult": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "sched_cde9a12f1107fa3652"
     },
     "route": {
      "type": "object",
      "additionalProperties": true
     },
     "everySeconds": {
      "type": "integer",
      "example": 3600
     },
     "remaining": {
      "type": "integer",
      "example": 24
     },
     "count": {
      "type": "integer",
      "example": 24
     },
     "live": {
      "type": "boolean",
      "example": false
     },
     "status": {
      "type": "string",
      "example": "active"
     },
     "runs": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "note": {
      "type": "string"
     }
    },
    "additionalProperties": true
   },
   "ConditionalRequest": {
    "type": "object",
    "required": [
     "route",
     "when"
    ],
    "properties": {
     "route": {
      "type": "object",
      "additionalProperties": true
     },
     "when": {
      "type": "object",
      "required": [
       "type"
      ],
      "properties": {
       "type": {
        "type": "string",
        "enum": [
         "priceAbove",
         "priceBelow",
         "timeAfter"
        ]
       },
       "asset": {
        "type": "string",
        "description": "Required for price conditions, e.g. ETH/USD, BTC/USD, USDG/USD.",
        "example": "ETH/USD"
       },
       "value": {
        "description": "Numeric price for price conditions; epoch/ISO for timeAfter.",
        "oneOf": [
         {
          "type": "number"
         },
         {
          "type": "string"
         }
        ],
        "example": 3000
       }
      }
     },
     "live": {
      "type": "boolean",
      "default": false
     }
    }
   },
   "ConditionalResult": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "cond_3728705d2654926bc1"
     },
     "route": {
      "type": "object",
      "additionalProperties": true
     },
     "when": {
      "type": "object",
      "additionalProperties": true
     },
     "live": {
      "type": "boolean",
      "example": false
     },
     "status": {
      "type": "string",
      "example": "armed"
     },
     "runs": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     },
     "note": {
      "type": "string"
     }
    },
    "additionalProperties": true
   },
   "PrimitiveCancelled": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "status": {
      "type": "string",
      "example": "cancelled"
     }
    }
   },
   "KeyCreated": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string",
      "example": "ak_9fa383a39ca3b33b74775e7d8f3d04c70b98057c"
     },
     "label": {
      "type": "string",
      "example": "docs-example"
     },
     "created": {
      "type": "integer",
      "example": 1784012809055
     },
     "note": {
      "type": "string"
     }
    }
   },
   "KeyUsage": {
    "type": "object",
    "properties": {
     "key": {
      "type": "object",
      "properties": {
       "label": {
        "type": "string"
       },
       "created": {
        "type": "integer"
       }
      }
     },
     "hour": {
      "type": "object",
      "properties": {
       "total": {
        "type": "integer"
       },
       "errors": {
        "type": "integer"
       },
       "topPaths": {
        "type": "array",
        "items": {
         "type": "object",
         "additionalProperties": true
        }
       }
      }
     },
     "day": {
      "type": "object",
      "properties": {
       "total": {
        "type": "integer"
       },
       "errors": {
        "type": "integer"
       },
       "topPaths": {
        "type": "array",
        "items": {
         "type": "object",
         "additionalProperties": true
        }
       }
      }
     },
     "recent": {
      "type": "array",
      "items": {
       "type": "object",
       "additionalProperties": true
      }
     }
    }
   },
   "HookRegistered": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "example": "wh_1008e656e4d46f7aec"
     },
     "routeId": {
      "type": "string",
      "example": "rt_abc123"
     },
     "url": {
      "type": "string",
      "example": "https://example.com/webhook"
     },
     "status": {
      "type": "string",
      "example": "pending"
     },
     "note": {
      "type": "string"
     }
    }
   },
   "Permit2SignatureTransfer": {
    "type": "object",
    "description": "A signed Permit2 SignatureTransfer authorising the relayer to pull exactly `amount` of `token` once. Sign the EIP-712 payload returned by POST /api/agent/do as signRequest.eip712 (domain {name:'Permit2', chainId, verifyingContract: the canonical Permit2 contract}; types PermitTransferFrom + TokenPermissions). IMPORTANT: the signed `spender` must be the relayer address from signRequest.spender - permitTransferFrom takes the spender implicitly as msg.sender, so a permit signed for any other address cannot be redeemed.",
    "required": [
     "owner",
     "permitted",
     "nonce",
     "deadline",
     "signature"
    ],
    "properties": {
     "owner": {
      "type": "string",
      "description": "Address that signed the permit (the payer).",
      "example": "0xAbC0000000000000000000000000000000000001"
     },
     "permitted": {
      "type": "object",
      "required": [
       "token",
       "amount"
      ],
      "description": "The single token permission being granted.",
      "properties": {
       "token": {
        "type": "string",
        "description": "ERC-20 contract on the source chain (signRequest.tokenAddress).",
        "example": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
       },
       "amount": {
        "type": "string",
        "description": "Exact amount in the token's smallest units.",
        "example": "4000000"
       }
      }
     },
     "nonce": {
      "type": "string",
      "description": "Unordered Permit2 nonce (bitmap) - any unused value; signRequest suggests one.",
      "example": "0x9f2c…"
     },
     "deadline": {
      "type": "string",
      "description": "Unix seconds after which the permit is void.",
      "example": "1785300000"
     },
     "signature": {
      "type": "string",
      "description": "0x… EIP-712 signature over the payload above.",
      "example": "0x…"
     }
    }
   }
  }
 },
 "paths": {
  "/api/status": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Operational status",
    "description": "Sanitized public health: overall state, per-node up/down (22 EVM + Stellar/Solana/Bitcoin), bridge rails, endpoint probes and route count. Cached ~30s. Rate limit 60/min.",
    "operationId": "getStatus",
    "responses": {
     "200": {
      "description": "Status snapshot",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Status"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/chains": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "The gasless mesh / route graph",
    "description": "Supported chain ids, node/route counts, the Permit2 spender (relayer) your SDK signs for, the routing engine, and per-node metadata for the non-EVM legs. Source of truth for what is routable. No key required.",
    "operationId": "getMesh",
    "responses": {
     "200": {
      "description": "Mesh snapshot",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Mesh"
        }
       }
      }
     }
    }
   }
  },
  "/api/gasless/info": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Same-chain gasless config",
    "description": "Deployed router / anyGas / tokenPaymaster / Permit2 / EntryPoint addresses, the EIP-712 domain, the per-chain deployment map and accepted fee tokens. No key required.",
    "operationId": "gaslessInfo",
    "responses": {
     "200": {
      "description": "Config",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/GaslessInfo"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/quote": {
   "post": {
    "tags": [
     "routing"
    ],
    "summary": "Quote a gasless route",
    "description": "Best gasless route for a single intent. POST a JSON body (preferred from code); the same fields are also accepted as GET query parameters with an identical response. EVM<->EVM runs best-of-N over the bridge aggregators; a Stellar/Solana/Bitcoin/Tron side is dispatched to its dedicated leg. Rate limit 40/min.",
    "operationId": "quoteRoute",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RouteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Best route quote",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Quote"
        }
       }
      }
     },
     "400": {
      "description": "Malformed or incomplete request. Body: {error, errorCode (e.g. MISSING_FIELD, BAD_REQUEST), suggestedAction, example} — fix and resend.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Unroutable lane",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "NO_ROUTE — no rail can settle this exact lane/amount right now (every rail's own reason is in `error`). Not a rate limit; try /api/route/compare, another amount or lane.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "RATE_LIMITED — Retry-After header (seconds) + retryAfterMs in the body. Anonymous callers get 40 quotes/min per IP; a free key (POST /api/keys/create, header x-anygas-key) raises it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "$ref": "#/components/parameters/AnygasPass"
     }
    ]
   },
   "get": {
    "tags": [
     "routing"
    ],
    "summary": "Quote a gasless route (GET query form)",
    "description": "Same quote as POST /api/route/quote with the body fields given as query parameters — for browsers, curl and quick checks. Same rate limit (40/min) and response. Without any parameters it answers 400 with a usage object.",
    "operationId": "quoteRouteGet",
    "parameters": [
     {
      "name": "fromChain",
      "in": "query",
      "required": true,
      "schema": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "integer"
        }
       ]
      },
      "description": "Source node: EVM chain id or solana|stellar|bitcoin|tron.",
      "example": 8453
     },
     {
      "name": "toChain",
      "in": "query",
      "required": true,
      "schema": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "integer"
        }
       ]
      },
      "description": "Destination node.",
      "example": 42161
     },
     {
      "name": "amount",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Source-token smallest units (25000000 = 25 USDC).",
      "example": "25000000"
     },
     {
      "name": "fromToken",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Symbol or address; omit for native.",
      "example": "USDC"
     },
     {
      "name": "toToken",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Symbol or address; omit for native.",
      "example": "USDC"
     },
     {
      "name": "toAddress",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Recipient; defaults to fromAddress."
     },
     {
      "name": "fromAddress",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Quote origin; anonymous default used if omitted."
     },
     {
      "name": "slippage",
      "in": "query",
      "required": false,
      "schema": {
       "type": "number"
      },
      "description": "Fraction, e.g. 0.005."
     },
     {
      "$ref": "#/components/parameters/AnygasPass"
     }
    ],
    "responses": {
     "200": {
      "description": "Best route quote",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Quote"
        }
       }
      }
     },
     "400": {
      "description": "Missing/invalid parameters (body carries usage)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Unroutable lane",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/execute": {
   "post": {
    "tags": [
     "routing"
    ],
    "summary": "Execute a signed gasless route",
    "description": "Submit a route; the relayer fronts all gas, tries ranked routes in net-output order and fails over on error, then returns a route id to track. Use mode=permit2 for a user-signed off-chain pull. Add header x-anygas-sandbox: 1 for a zero-funds simulation. Rate limit 8/min. REHEARSAL: send dryRun:true (or live:false) for a rail-independent dry run that broadcasts nothing, or the x-anygas-sandbox: 1 header for a full simulation with sbx_ ids.\n\n**THIS ENDPOINT BROADCASTS REAL TRANSACTIONS BY DEFAULT.** Omitting `live` does NOT mean dry run. To simulate without moving funds, send `\"live\": false` or `\"dryRun\": true` (or the header `x-anygas-sandbox: 1` for a fully simulated route with `sbx_` ids). Requests to burn/null addresses are refused with `BURN_ADDRESS_REFUSED` because those funds would be unrecoverable; pass `\"allowBurnAddress\": true` only if the burn is intentional. Amounts must be a positive integer string in base units — malformed, zero or negative amounts are rejected with `BAD_AMOUNT` on the dry path as well as the live one.\n\n**`minOut` coverage:** the Robyn float lane enforces it and refuses the transfer with `BELOW_AGREED_OUTPUT` before any custody change. On third-party bridge rails `minOut` is passed through to the underlying bridge as its slippage floor rather than enforced by Robyn, so the guarantee is the bridge's, not ours. Use `POST /api/floatlane/execute` when you need Robyn itself to refuse a route that would deliver less than you agreed to.",
    "operationId": "executeRoute",
    "parameters": [
     {
      "$ref": "#/components/parameters/SandboxHeader"
     },
     {
      "$ref": "#/components/parameters/ApiKeyHeader"
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ExecuteRequest",
        "properties": {
         "dryRun": {
          "type": "boolean",
          "description": "Rehearse WITHOUT broadcasting. Returns {status:'DRY', broadcast:false, wouldSend, rankedRails}. This is RAIL-INDEPENDENT and is the safe way to preview an execution. IMPORTANT: 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. Use dryRun:true (or live:false), or the x-anygas-sandbox: 1 header for a fully simulated route.",
          "example": true
         },
         "live": {
          "type": "boolean",
          "description": "Set false for an explicit dry run (same effect as dryRun:true). Setting it true is required by the float lane and the non-EVM legs to broadcast; the EVM<->EVM path broadcasts by default when neither dryRun nor live:false is present."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Route accepted (or FUNDS_REQUIRED for an unfunded non-EVM lane)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RouteReceipt"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/status": {
   "get": {
    "tags": [
     "routing"
    ],
    "summary": "Track a route",
    "description": "Live ids (rt_…) are re-checked against the underlying bridge on each read; sandbox ids (sbx_…) advance on a timer. No key required.",
    "operationId": "routeStatus",
    "parameters": [
     {
      "name": "id",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Route id from execute",
      "example": "sbx_611d3343c9b8a0edae"
     }
    ],
    "responses": {
     "200": {
      "description": "Route status",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RouteReceipt"
        }
       }
      }
     },
     "404": {
      "description": "Unknown route id",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/compare": {
   "get": {
    "tags": [
     "best-execution"
    ],
    "summary": "Compare every rail for a lane (query form)",
    "description": "Quotes every rail that can serve a lane (aggregators + Allbridge + Chainflip) in parallel, ranks by net destination output and flags the best. Read-only, nothing signed. Adds effectiveFeePct, qualityScore, guard and a 12s cached flag. Rate limit 30/min.\n\nIncludes Robyn's OWN float lane as a ranked candidate, not just third-party bridges. The lane is quoted from inventory Robyn already holds on the destination chain, so it needs no external API and still answers when every aggregator is rate-limited or down. The `firstParty` block states the first-party price against the best third-party price for that exact request, so the independence claim is checkable per call. Pass `excludeRails: [\"floatlane\"]` to compare only third-party rails (used internally when moving Robyn's own inventory, which the lane cannot do).",
    "operationId": "compareRoutesGet",
    "parameters": [
     {
      "name": "fromChain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "42161"
     },
     {
      "name": "toChain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "solana"
     },
     {
      "name": "fromToken",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "example": "USDC"
     },
     {
      "name": "toToken",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "example": "USDC"
     },
     {
      "name": "amount",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "50000000"
     },
     {
      "name": "toAddress",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Ranked rails",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CompareResult"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "Compare every rail for a lane (JSON body)",
    "description": "Same as the GET form; accepts the RouteRequest body. Read-only.\n\nIncludes Robyn's OWN float lane as a ranked candidate, not just third-party bridges. The lane is quoted from inventory Robyn already holds on the destination chain, so it needs no external API and still answers when every aggregator is rate-limited or down. The `firstParty` block states the first-party price against the best third-party price for that exact request, so the independence claim is checkable per call. Pass `excludeRails: [\"floatlane\"]` to compare only third-party rails (used internally when moving Robyn's own inventory, which the lane cannot do).",
    "operationId": "compareRoutesPost",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RouteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Ranked rails",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CompareResult"
        }
       }
      }
     },
     "400": {
      "description": "Malformed request (missing fromChain/toChain/amount). Body carries errorCode + suggestedAction.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "RATE_LIMITED — honour Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/intent/solve": {
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "Goal-based source picker (JSON body)",
    "description": "State a goal, a destination and 1–10 candidate funding sources; the solver reads /compare once per source and ranks them, returning a plan with ready-to-POST execute params. It NEVER executes. Rate limit 20/min.",
    "operationId": "solveIntentPost",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/IntentRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Ranked plan",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/IntentPlan"
        }
       }
      }
     }
    }
   },
   "get": {
    "tags": [
     "best-execution"
    ],
    "summary": "Goal-based source picker (query form)",
    "description": "Query-string form of intent/solve. Encode the sources as JSON in the `from` param.",
    "operationId": "solveIntentGet",
    "parameters": [
     {
      "name": "toChain",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "8453"
     },
     {
      "name": "toToken",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "USDC"
     },
     {
      "name": "from",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "JSON array of sources, url-encoded.",
      "example": "[{\"chain\":42161,\"token\":\"USDC\",\"amount\":\"5000000\"}]"
     },
     {
      "name": "goal",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "max-output",
        "cheapest",
        "fastest"
       ]
      }
     },
     {
      "name": "amount",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "toAddress",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Ranked plan",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/IntentPlan"
        }
       }
      }
     }
    }
   }
  },
  "/api/routes/recent": {
   "get": {
    "tags": [
     "explorer"
    ],
    "summary": "Recent routes feed",
    "description": "Anonymized feed of recent real routes settled by the relayer (powers the public Explorer). No addresses, tx hashes or exact amounts. Rate limit 60/min.",
    "operationId": "recentRoutes",
    "parameters": [
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "default": 25,
       "maximum": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Recent routes",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/RecentRoutes"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/batch": {
   "post": {
    "tags": [
     "primitives"
    ],
    "summary": "Plan a multi-leg payout (gated, dry-run by default)",
    "description": "Plans a multi-recipient/multi-leg payout (up to 20 legs), each via /compare. Plans & stores only — it does NOT move funds. Execution needs the watcher running with PRIMITIVES_LIVE=1 AND the record carrying live:true; the shipped default (live:false) simulates every tick. Rate limit 30/min.",
    "operationId": "routeBatch",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/BatchRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Planned batch record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/BatchResult"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/schedule": {
   "post": {
    "tags": [
     "primitives"
    ],
    "summary": "Create a scheduled / recurring transfer (gated)",
    "description": "Plans & stores a scheduled or recurring transfer. Same double gate as batch (PRIMITIVES_LIVE=1 + live:true); default live:false simulates. Rate limit 30/min.",
    "operationId": "routeSchedule",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ScheduleRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Schedule record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ScheduleResult"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/schedule/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "sched_cde9a12f1107fa3652"
    }
   ],
   "get": {
    "tags": [
     "primitives"
    ],
    "summary": "Inspect a schedule",
    "operationId": "getSchedule",
    "responses": {
     "200": {
      "description": "Schedule record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ScheduleResult"
        }
       }
      }
     },
     "404": {
      "description": "Bad schedule path or id",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "primitives"
    ],
    "summary": "Cancel a schedule",
    "operationId": "cancelSchedule",
    "responses": {
     "200": {
      "description": "Cancelled",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PrimitiveCancelled"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/conditional": {
   "post": {
    "tags": [
     "primitives"
    ],
    "summary": "Create a trigger-based transfer (gated)",
    "description": "Plans & stores a trigger-based transfer (priceAbove/priceBelow against on-chain feeds, or timeAfter). Same double gate as batch; default live:false simulates. Rate limit 30/min.",
    "operationId": "routeConditional",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/ConditionalRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Conditional record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ConditionalResult"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/conditional/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "cond_3728705d2654926bc1"
    }
   ],
   "get": {
    "tags": [
     "primitives"
    ],
    "summary": "Inspect a condition",
    "operationId": "getConditional",
    "responses": {
     "200": {
      "description": "Conditional record",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ConditionalResult"
        }
       }
      }
     },
     "404": {
      "description": "Bad conditional path or id",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "primitives"
    ],
    "summary": "Cancel a condition",
    "operationId": "cancelConditional",
    "responses": {
     "200": {
      "description": "Cancelled",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PrimitiveCancelled"
        }
       }
      }
     }
    }
   }
  },
  "/api/keys/create": {
   "post": {
    "tags": [
     "keys-hooks"
    ],
    "summary": "Create an optional developer key",
    "description": "Self-serve key for a stable identity + usage visibility. The API stays keyless; a key just tags requests. The key is shown once. Rate limit 5/hour.",
    "operationId": "createKey",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "label": {
          "type": "string",
          "example": "docs-example"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Key created (shown once)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/KeyCreated"
        }
       }
      }
     }
    }
   }
  },
  "/api/keys/usage": {
   "get": {
    "tags": [
     "keys-hooks"
    ],
    "summary": "Usage stats for your key",
    "description": "Hour + day aggregates and recent calls. Pass the key via the x-anygas-key header or ?key=.",
    "operationId": "keyUsage",
    "parameters": [
     {
      "$ref": "#/components/parameters/ApiKeyHeader"
     },
     {
      "name": "key",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Usage",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/KeyUsage"
        }
       }
      }
     }
    }
   }
  },
  "/api/hooks/register": {
   "post": {
    "tags": [
     "keys-hooks"
    ],
    "summary": "Register a completion webhook",
    "description": "Get called on DONE/FAILED for a route id instead of polling. We POST {event,routeId,status,destTx,srcTx,ts}. If secret is set, deliveries carry x-anygas-signature: sha256=HMAC(secret, rawBody). Watch state at GET /api/hooks/{id}. Rate limit 120/min.",
    "operationId": "registerHook",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "routeId",
         "url"
        ],
        "properties": {
         "routeId": {
          "type": "string",
          "example": "rt_abc123"
         },
         "url": {
          "type": "string",
          "example": "https://example.com/webhook"
         },
         "secret": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Hook registered",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/HookRegistered"
        }
       }
      }
     }
    }
   }
  },
  "/api/ask": {
   "post": {
    "tags": [
     "concierge"
    ],
    "summary": "Ask the AI concierge",
    "description": "Natural-language questions about Robyn AnyGas; the concierge can call the read endpoints to answer.",
    "operationId": "ask",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "question"
        ],
        "properties": {
         "question": {
          "type": "string",
          "example": "What chains can I move USDC between gaslessly?"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Answer",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "answer": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/terms": {
   "get": {
    "tags": [
     "concierge"
    ],
    "summary": "Terms of Service",
    "operationId": "terms",
    "responses": {
     "200": {
      "description": "HTML terms"
     }
    }
   }
  },
  "/api/ncaccount/{agent}": {
   "get": {
    "tags": [
     "account"
    ],
    "summary": "Non-custodial yield account",
    "description": "The agent's own Aave v3 / Moonwell position (aUSDC/mUSDC) per chain + the capped allowance it granted the relayer + live APY. Best-yield venue auto-selected. NON-CUSTODIAL: funds stay in the agent's wallet; Robyn holds no key and no pool.",
    "responses": {
     "200": {
      "description": "OK"
     }
    },
    "parameters": [
     {
      "name": "agent",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "the agent's own wallet address (0x…)"
     }
    ]
   }
  },
  "/api/ncaccount/quote": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Quote a non-custodial yield spend",
    "description": "Read-only JIT quote: how a spend would be fulfilled from the agent's yield (draw ≤ allowance → Aave/Moonwell withdraw → gasless deliver). Body: {agent, srcChain, amount, toChain, toAddress}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/ncaccount/spend": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Spend from yield (agent-signed)",
    "description": "Execute a spend from the agent's yield with ONE EIP-712 signature. Body: {intent:{agent,srcChain,amount,toChain,toAddress,nonce,deadline}, signature, live:true}. The relayer verifies the signature, pulls ONLY up to the agent's on-chain aUSDC/mUSDC allowance, unwinds from the agent's yield venue (Aave v3 or Moonwell), and delivers gaslessly. EIP-712 domain: {name:'RobynNCAccount', version:'1', chainId:srcChain}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/account/{address}": {
   "get": {
    "tags": [
     "account"
    ],
    "summary": "AnyGas Account (yield-aware balance)",
    "description": "Unified yield-aware balance across chains: idle USDC + Aave v3 earning positions + blended APY + est. annual yield.",
    "responses": {
     "200": {
      "description": "OK"
     }
    },
    "parameters": [
     {
      "name": "address",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "EVM address (0x…)"
     }
    ]
   }
  },
  "/api/account/venues": {
   "get": {
    "tags": [
     "account"
    ],
    "summary": "Yield venues (live APYs)",
    "description": "Live Aave v3 USDC supply APYs across supported chains (non-custodial — you hold the aToken).",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/account/plan": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Plan a JIT payment from yield",
    "description": "Cheapest-source JIT plan to deliver an amount to any chain from a yield position. Body: {from, toChain, amountUsd, toAddress}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/account/gas": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Gas-on-demand from yield",
    "description": "Quote delivering native gas to a chain, drawn just-in-time from the yield position. Body: {from, chain, amountUsd}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/balance/{address}": {
   "get": {
    "tags": [
     "account"
    ],
    "summary": "Unified balance across all chains",
    "description": "USDC balance for an address across every gasless chain — one wallet view.",
    "responses": {
     "200": {
      "description": "OK"
     }
    },
    "parameters": [
     {
      "name": "address",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "EVM address (0x…)"
     }
    ]
   }
  },
  "/api/balance/plan": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Cheapest-source payment plan",
    "description": "Plan the cheapest way to deliver an amount to any chain (same-chain gasless / best cross-chain source / split). Body: {from, toChain, toAddress, amountUsd}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/receipts/{address}": {
   "get": {
    "tags": [
     "account"
    ],
    "summary": "Signed execution receipts",
    "description": "Relayer-signed, verifiable settlement history for an address (EIP-191). Verify: ethers.verifyMessage(receipt.digest, receipt.signature) === relayer.",
    "responses": {
     "200": {
      "description": "OK"
     }
    },
    "parameters": [
     {
      "name": "address",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "EVM address (0x…)"
     }
    ]
   }
  },
  "/api/events/subscribe": {
   "post": {
    "tags": [
     "account"
    ],
    "summary": "Subscribe to price/balance webhooks",
    "description": "HMAC-signed callback when a price or an address balance crosses a threshold. Body: {event:'price'|'balance', url, secret?, ...filter}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/proof": {
   "get": {
    "tags": [
     "adoption"
    ],
    "summary": "Public proof / adoption feed",
    "description": "Live real numbers: routes settled + volume, AnyGas Account agents + AUM + fees, active callers, 26 networks, uptime. Plus /api/proof/badge.svg.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/referral/register": {
   "post": {
    "tags": [
     "adoption"
    ],
    "summary": "Register as a referrer",
    "description": "Get your referral code + link. Earn 10% of your referrals' collected fees for 12 months (self-funding, capped, anti-sybil).",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/referral/link": {
   "post": {
    "tags": [
     "adoption"
    ],
    "summary": "Attribute an agent to a referrer",
    "description": "Set the referrer for an agent at first use (immutable, no self-refer). Body: {agent, code|referrer}.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/referral/{referrer}": {
   "get": {
    "tags": [
     "adoption"
    ],
    "summary": "Referral earnings",
    "description": "Referred agents, accrued/claimable/paid, and the policy for a referrer address.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/gaspolicy/set": {
   "post": {
    "tags": [
     "adoption"
    ],
    "summary": "Standing gas-on-demand policy",
    "description": "Keep workers funded with native gas across chains from your yield. Body: {agent, chains:{id:{minNative,topUpNative}}, perDayUsd?, webhook?}. Fires a signed gas.low alert with the exact top-up.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/gaspolicy/{agent}": {
   "get": {
    "tags": [
     "adoption"
    ],
    "summary": "Gas policy status",
    "description": "Live per-chain gas balances + which chains are below threshold now.",
    "responses": {
     "200": {
      "description": "OK"
     }
    }
   }
  },
  "/api/agent/do": {
   "post": {
    "tags": [
     "agents"
    ],
    "summary": "One call: intent in, plan or signable payload out",
    "description": "The single entry point for an autonomous agent. Accepts plain language (\"send 25 USDC to 0xabc... on arbitrum\") or structured fields, resolves chains/token/amount, quotes the best rail, and returns exactly one of: status \"sign\" (with the precise payload to sign and where to POST it), \"quoted\" (something is still missing), \"done\" (sandbox), or a typed error. Replaces the chains -> quote -> execute -> poll sequence. Send header x-anygas-sandbox: 1 to dry-run the whole flow with no funds. Rate limit 60/min (300/min with a key).",
    "operationId": "agentDo",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "intent": {
          "type": "string",
          "description": "plain-language instruction"
         },
         "fromChain": {
          "oneOf": [
           {
            "type": "integer"
           },
           {
            "type": "string"
           }
          ]
         },
         "toChain": {
          "oneOf": [
           {
            "type": "integer"
           },
           {
            "type": "string"
           }
          ]
         },
         "token": {
          "type": "string",
          "example": "USDC"
         },
         "amountHuman": {
          "type": "number",
          "description": "human units, e.g. 25"
         },
         "amount": {
          "type": "string",
          "description": "base units; overrides amountHuman"
         },
         "toAddress": {
          "type": "string"
         },
         "sandbox": {
          "type": "boolean"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Plan, signing instruction, or sandbox result",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        },
        "example": {
         "status": "sign",
         "rail": "robyn-floatlane",
         "receives": "3990000",
         "understood": {
          "fromChain": 8453,
          "toChain": 42161,
          "token": "USDC",
          "amount": "4000000"
         }
        }
       }
      }
     },
     "400": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "$ref": "#/components/parameters/AnygasPass"
     }
    ]
   }
  },
  "/api/route/stream": {
   "get": {
    "tags": [
     "routing"
    ],
    "summary": "Server-sent events for route lifecycle",
    "description": "Streams the route status object on every change instead of making you poll. Emits one event immediately so a late subscriber is caught up, then event: status on each change, and event: done on a terminal state (DONE/FAILED/REFUNDED) before closing. Heartbeat comments keep idle proxies open. Bounded to 15 minutes and 50 concurrent streams.",
    "operationId": "streamRoute",
    "parameters": [
     {
      "name": "id",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "route id returned by /api/route/execute"
     }
    ],
    "responses": {
     "200": {
      "description": "text/event-stream of status events",
      "content": {
       "text/event-stream": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "400": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/errors": {
   "get": {
    "tags": [
     "agents"
    ],
    "summary": "The complete error contract",
    "description": "Every errorCode the API can return, whether it is retryable, how long to wait, and the suggested recovery action. Fetch once at integration time and branch on errorCode.",
    "operationId": "errorCatalogue",
    "responses": {
     "200": {
      "description": "Error taxonomy",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/keys/quota": {
   "get": {
    "tags": [
     "keys"
    ],
    "summary": "What a free API key is worth",
    "description": "Published rate tiers. The API is keyless by design; a free self-serve key scopes limits to YOU instead of your shared IP and multiplies the ceiling (5x today).",
    "operationId": "keyQuota",
    "responses": {
     "200": {
      "description": "Tier table",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/7702/info": {
   "get": {
    "tags": [
     "sponsored-transactions"
    ],
    "summary": "EIP-7702 sponsored-transaction rail",
    "description": "Chains, caps, pricing and the end-to-end flow. Robyn submits your signed EIP-7702 authorization as a type-4 transaction and pays the gas: no ERC-4337 bundler, no paymaster deposit, and no native token in your account.",
    "operationId": "sponsor7702Info",
    "responses": {
     "200": {
      "description": "Rail info",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/7702/delegate": {
   "get": {
    "tags": [
     "sponsored-transactions"
    ],
    "summary": "Canonical batch-executor delegate",
    "description": "Address of the RobynDelegate7702 batch executor on each of the 22 EVM chains, its ABI, and the EIP-712 scheme for executeSigned. Delegate to this and one signature can approve, swap and transfer atomically.",
    "operationId": "delegate7702",
    "responses": {
     "200": {
      "description": "Per-chain deployments + ABI",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/7702/check": {
   "post": {
    "tags": [
     "sponsored-transactions"
    ],
    "summary": "Verify an EIP-7702 authorization",
    "description": "Confirms a signed authorization tuple recovers the expected authority and whether its nonce matches the live account nonce. Free, read-only, broadcasts nothing.",
    "operationId": "check7702",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "chainId": {
          "type": "integer"
         },
         "authorization": {
          "type": "object",
          "properties": {
           "chainId": {
            "type": "integer"
           },
           "address": {
            "type": "string"
           },
           "nonce": {
            "type": "integer"
           },
           "yParity": {
            "type": "integer"
           },
           "r": {
            "type": "string"
           },
           "s": {
            "type": "string"
           }
          }
         }
        },
        "required": [
         "chainId",
         "authorization"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Recovered authority",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        },
        "example": {
         "ok": true,
         "authority": "0x...",
         "nonceMatches": true
        }
       }
      }
     },
     "400": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/7702/sponsor": {
   "post": {
    "tags": [
     "sponsored-transactions"
    ],
    "summary": "Sponsor a transaction (relayer pays the gas)",
    "description": "Submits your authorization as a type-4 transaction with the relayer paying gas. Public sponsorship is PREPAID in USDC over x402: calling without payment returns HTTP 402 plus an accepts[] invoice and the full plan, so pricing is always free to check. Send {\"dry\":true} for a preview that never broadcasts. Set x-idempotency-key so a timeout retry cannot pay twice.",
    "operationId": "sponsor7702",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "chainId": {
          "type": "integer"
         },
         "authorization": {
          "type": "object"
         },
         "to": {
          "type": "string"
         },
         "data": {
          "type": "string"
         },
         "dry": {
          "type": "boolean",
          "description": "price it without broadcasting"
         }
        },
        "required": [
         "chainId",
         "authorization"
        ]
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Broadcast result or dry plan",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "402": {
      "description": "Payment required - x402 invoice plus the full plan",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "429": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Typed error - branch on errorCode, never on the message text",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/rpc/health": {
   "get": {
    "summary": "RPC endpoint independence — which provider each chain is using",
    "description": "Robyn keeps MULTIPLE RPC endpoints per chain and switches automatically when one degrades. One endpoint is active per chain at a time (never raced), so nonce reads stay self-consistent for the payout path; eth_sendRawTransaction is never retried across endpoints. Use this to tell \"the chain is down\" apart from \"one vendor is down\".",
    "tags": [
     "status"
    ],
    "responses": {
     "200": {
      "description": "Per-chain endpoint health",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "endpoints": {
           "type": "object",
           "description": "chainId -> {total, healthy, active, redundant}"
          },
          "degraded": {
           "type": "array",
           "items": {
            "type": "string"
           },
           "description": "chains with fewer usable endpoints than configured"
          },
          "chains": {
           "type": "object",
           "description": "full per-endpoint detail incl. call counts, latency and demotion cooldowns"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/nonevm/reserves": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "Non-EVM leg readiness",
    "description": "Per-leg settlement readiness for Tron/Solana/Stellar/Bitcoin, labelling each native balance as gas (spent to settle) or treasury (held, not required). Direction matters: EVM->X is signed by the EVM relayer, while X->EVM is signed by a per-chain key.",
    "operationId": "nonEvmReserves",
    "responses": {
     "200": {
      "description": "Per-leg readiness",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/diagnostics": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "One-call system diagnosis",
    "description": "Aggregates module health, chain nodes, RPC providers, non-EVM settlement legs and keeper freshness into a single verdict, and states the NEXT ACTION for anything flagged. Start here when something looks wrong. Public-safe: counts, booleans and ages only - no addresses, balances or key material.",
    "operationId": "diagnostics",
    "responses": {
     "200": {
      "description": "Verdict, subsystem summary, keeper freshness, findings with next actions",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/tokens": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Stock-token registry (Token Checker)",
    "description": "Verified Robinhood Chain stock tokens with official flags, so a caller can scam-check a token before trading it. NOTE: this is the registry service; the path is /api/tokens, not /api/registry.",
    "operationId": "tokenRegistry",
    "responses": {
     "200": {
      "description": "count, official, tokens[]",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/premium": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Stock-token premium monitor",
    "description": "On-chain stock-token price against the real market, exposing rich/cheap dislocations. Free tier is 15-minute delayed; real-time is paid.",
    "operationId": "premiumFeed",
    "responses": {
     "200": {
      "description": "tier, delay, data[]",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/actions": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Corporate actions (dividends, splits, earnings)",
    "description": "Upcoming corporate actions for the tokenised equities and what they mean for holders. Cached 6h and warmed at boot.",
    "operationId": "corporateActions",
    "responses": {
     "200": {
      "description": "actions[]",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/topup/quote": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Gas top-up terms (USDC -> ETH on 4663)",
    "description": "Deposit address, accepted assets, fee and the LIVE per-transaction cap. maxUsdPerTx is dynamic (bounded by relayer float on 4663) and `example` is derived from it - never send more than maxUsdPerTx. `atCapacity` is true when float is below the minimum.",
    "operationId": "topupQuote",
    "responses": {
     "200": {
      "description": "Terms, live cap, worked example",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/exit/quote": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Fast exit terms (4663 -> Arbitrum)",
    "description": "Deposit address, fee and live per-transaction cap for leaving Robinhood Chain in ~1 minute instead of the 7-day canonical bridge. Payout goes to the sending address, so never send from an exchange.",
    "operationId": "exitQuote",
    "responses": {
     "200": {
      "description": "Terms and live cap",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/orders-info": {
   "get": {
    "tags": [
     "robyn-services"
    ],
    "summary": "Limit-order keeper info",
    "description": "Executor address, fee, EIP-712 domain/types for signing an order, plus open and filled counts. NOTE the hyphenated path: /api/orders-info (the order actions themselves are /api/orders).",
    "operationId": "ordersInfo",
    "responses": {
     "200": {
      "description": "executor, feeBps, domain, types, open, filled",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/membership/status": {
   "get": {
    "tags": [
     "membership"
    ],
    "summary": "Check an address's membership tier and benefits",
    "description": "Returns whether the address holds a recognised member NFT, which collection matched, its fee tier, and a plain-language `benefits` block. Members pay ZERO on cross-chain transfers of $0.10-$25 to low-cost destinations, 15bps instead of 25bps routing, and 10% instead of 25% gas markup. Non-members get a `wouldGet` block showing what membership would save. Membership is a plain on-chain balanceOf check across any supported chain and transfers with the NFT.",
    "parameters": [
     {
      "name": "address",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "EVM address to check (0x…)"
     },
     {
      "name": "collection",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Optional collection id, with tokenId+chainId, to also return intro-allowance status"
     },
     {
      "name": "tokenId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "chainId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Membership status",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "additionalProperties": true
        }
       }
      }
     },
     "400": {
      "description": "address missing or not a valid EVM address"
     }
    }
   }
  },
  "/api/membership/info": {
   "get": {
    "tags": [
     "membership"
    ],
    "summary": "Membership programme: standard vs member rates and recognised collections",
    "responses": {
     "200": {
      "description": "Programme configuration and the live collection registry",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "additionalProperties": true
        }
       }
      }
     }
    }
   }
  },
  "/api/floatlane/info": {
   "get": {
    "tags": [
     "float-lane"
    ],
    "summary": "Float-lane pricing, micro tier, per-chain cost floors and VIP registry",
    "description": "Live lane configuration: per-destination-chain real payout cost floors (from $0.00006 on Avalanche to $0.073 on Linea), the free micro tier and its remaining daily count and dollar budget, directional (imbalance-aware) pricing, and the recognised VIP collections.",
    "responses": {
     "200": {
      "description": "Lane pricing and tiers",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "additionalProperties": true
        }
       }
      }
     }
    }
   }
  },
  "/api/route/preflight": {
   "post": {
    "tags": [
     "routing"
    ],
    "summary": "GO / NO-GO check before executing a transfer (free, read-only)",
    "description": "Call this before /api/route/execute. Returns `decision` (GO|NO_GO), typed `blockers` each carrying a `fix`, advisory `warnings`, and the `expected` delivery. It answers for free the questions that otherwise cost gas to discover: BELOW_FEE_THRESHOLD (a fixed corridor fee would consume the entire amount, so nothing arrives), BAD_RECIPIENT (address not valid for the destination chain family — unrecoverable once broadcast), QUOTE_ONLY_CORRIDOR (the rail prices but cannot settle), ABOVE_LANE_CAPACITY (the instant lane is full so a dearer bridge would serve you; the response names the exact ceiling to split at), HIGH_COST (>=20% of the amount withheld by vendor fees). Signs nothing, broadcasts nothing, moves nothing. Pass `fromAddress` so NFT membership — and therefore free micro-transfers — is recognised. A GO reflects conditions now; capacity and vendor fees move, so treat it as fresh for seconds. Also reports OUR OWN readiness — relayer native gas on both chains — because a preflight that says GO while the relayer cannot perform the transfer is worse than none.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "fromChain",
         "toChain",
         "amount"
        ],
        "properties": {
         "fromChain": {
          "oneOf": [
           {
            "type": "integer"
           },
           {
            "type": "string"
           }
          ],
          "example": 42161
         },
         "toChain": {
          "oneOf": [
           {
            "type": "integer"
           },
           {
            "type": "string"
           }
          ],
          "example": 8453
         },
         "amount": {
          "type": "string",
          "example": "5000000"
         },
         "fromToken": {
          "type": "string",
          "example": "USDC"
         },
         "toToken": {
          "type": "string",
          "example": "USDC"
         },
         "fromAddress": {
          "type": "string",
          "description": "Determines membership and therefore fee tier"
         },
         "toAddress": {
          "type": "string",
          "description": "Validated against the destination chain family"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Preflight verdict",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "decision": {
           "type": "string",
           "enum": [
            "GO",
            "NO_GO"
           ]
          },
          "ok": {
           "type": "boolean"
          },
          "blockers": {
           "type": "array",
           "items": {
            "type": "object",
            "properties": {
             "code": {
              "type": "string"
             },
             "what": {
              "type": "string"
             },
             "fix": {
              "type": "string"
             }
            }
           }
          },
          "warnings": {
           "type": "array",
           "items": {
            "type": "object"
           }
          },
          "checks": {
           "type": "object",
           "additionalProperties": true
          },
          "expected": {
           "type": "object",
           "additionalProperties": true
          }
         }
        }
       }
      }
     },
     "400": {
      "description": "Malformed request. Body carries errorCode + suggestedAction + example.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "RATE_LIMITED — honour Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "name": "deep",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      },
      "description": "Set to 1 to additionally run the rail's real DRY build and inspect relayer-side readiness (token balance, allowance the bridge will pull against). This is the check that catches a missing ERC-20 allowance BEFORE it costs gas. Slower (seconds), so it is opt-in."
     },
     {
      "$ref": "#/components/parameters/AnygasPass"
     }
    ]
   }
  },
  "/api/capabilities": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Everything this service can do, as structured data",
    "description": "The machine-readable capability surface: which rails serve which lanes, which corridors can execute versus only quote (with the reason), the real fee model, and the feature list. Read this first to decide what is possible before quoting. Rails include aggregators (EVM<->EVM best-of-N), allbridge-core (Solana/Sui), cctp (Circle CCTP V2 native burn-and-mint, serves EVM->Stellar), chainflip (native BTC) and sponsor7702 (same-chain sponsored execution).",
    "operationId": "getCapabilities",
    "responses": {
     "200": {
      "description": "Capability surface",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/floatlane/pending": {
   "get": {
    "tags": [
     "floatlane"
    ],
    "summary": "Unfinished payouts (should always be empty)",
    "description": "Obligations where a user has paid in but the payout has not completed — normally an empty object. A pending row is written BEFORE each payout so a crash between taking custody and paying out cannot lose the obligation; a keeper reconciles anything left behind every five minutes. Published for transparency: you can verify at any time that nothing is owed.",
    "operationId": "getFloatlanePending",
    "responses": {
     "200": {
      "description": "Map of unfinished obligations, keyed by id",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/floatlane/transfers": {
   "get": {
    "tags": [
     "floatlane"
    ],
    "summary": "Float-lane transfer history, independently verifiable",
    "description": "Recent float-lane settlements. Pass ?verify=1 to re-read each payout on-chain and confirm the delivered amount equals what the quote promised. Destination transaction hashes are published only for our own self-funded payouts, never for user transfers.",
    "operationId": "getFloatlaneTransfers",
    "parameters": [
     {
      "name": "verify",
      "in": "query",
      "required": false,
      "description": "Set to 1 to re-check each payout against the chain.",
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Transfer list, optionally with on-chain verification",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/floatlane/reconcile": {
   "post": {
    "tags": [
     "floatlane"
    ],
    "summary": "Complete unfinished payouts (owner-gated)",
    "description": "Heals the crash-safety net. For each unfinished obligation it decides safely: if the recipient balance already rose by the payout amount the payout landed and the row is cleared; if the balance is provably unchanged the payout never happened and can be completed; anything ambiguous is left pending for manual review, so it can never double-pay. Obligations younger than ten minutes are skipped because their payout may still be broadcasting. Requires the owner key; DRY unless {\"live\":true}. A keeper runs this automatically every five minutes.",
    "operationId": "postFloatlaneReconcile",
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "live": {
          "type": "boolean",
          "description": "false (default) decides and reports without moving funds."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Per-obligation outcomes",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "Owner key required"
     }
    }
   }
  },
  "/api/netting/info": {
   "get": {
    "tags": [
     "netting"
    ],
    "summary": "What netting is, and what you are accepting",
    "description": "Describes the opt-in custodial balance product: free instant transfers between enrolled accounts, the caps, the fee model (zero for internal transfers) and the alternative if you do not want custody. NOTE: netting is the ONE custodial part of this service. Every other rail holds nothing on your behalf.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/terms": {
   "get": {
    "tags": [
     "netting"
    ],
    "summary": "The exact consent text you must sign to enrol",
    "description": "Returns the message an account must sign with its OWN key to open a custodial netting balance. Signing it is the record of informed consent; enrolment without a valid signature is refused. NOTE: netting is the ONE custodial part of this service. Every other rail holds nothing on your behalf.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "name": "address",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ]
   }
  },
  "/api/netting/enroll": {
   "post": {
    "tags": [
     "netting"
    ],
    "summary": "Open a custodial netting account",
    "description": "Body {address, signature}. The signature must recover to the address, so nobody can enrol an account they do not control. Call GET /api/netting/terms first for the text to sign. NOTE: netting is the ONE custodial part of this service. Every other rail holds nothing on your behalf.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "address",
         "signature"
        ],
        "properties": {
         "address": {
          "type": "string"
         },
         "signature": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/account": {
   "get": {
    "tags": [
     "netting"
    ],
    "summary": "Balance, enrolment state and next nonce",
    "description": "Reports whether the address is enrolled (and therefore custodied), its internal balance, the recent journal, and nextNonce for signing the next transfer or withdrawal.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "parameters": [
     {
      "name": "address",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ]
   }
  },
  "/api/netting/transfer": {
   "post": {
    "tags": [
     "netting"
    ],
    "summary": "Free internal transfer (no chain touched)",
    "description": "Moves value between two enrolled accounts as a book entry: no gas, no bridge, no fee, and NO transaction hash because no transaction occurs. Both parties must be enrolled — we will not hold a balance for a recipient who never consented. Sign robyn-netting-transfer:v1:from=…:to=…:amount=…:nonce=… with the sender key. Nonces are monotonic, so a captured signature cannot be replayed.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "from",
         "to",
         "amount",
         "nonce",
         "signature"
        ],
        "properties": {
         "from": {
          "type": "string"
         },
         "to": {
          "type": "string"
         },
         "amount": {
          "type": "string"
         },
         "nonce": {
          "type": "string"
         },
         "signature": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/deposit": {
   "post": {
    "tags": [
     "netting"
    ],
    "summary": "Credit a netting balance from a real on-chain transfer",
    "description": "Send USDC to the relayer on any covered chain, then present {address, chainId, txHash}. The credit is derived from the Transfer events in that receipt, so only what actually arrived is credited, and only to the account that actually sent it. Each transaction can be credited once.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "address",
         "chainId",
         "txHash"
        ],
        "properties": {
         "address": {
          "type": "string"
         },
         "chainId": {
          "type": "integer"
         },
         "txHash": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/withdraw": {
   "post": {
    "tags": [
     "netting"
    ],
    "summary": "Settle a netting balance back on-chain",
    "description": "Debits the internal balance and pays out real USDC on the chosen chain. The debit happens BEFORE the payout so a crash can never pay twice; if the payout fails the balance is restored and journalled. Sign robyn-netting-withdraw:v1:account=…:amount=…:toChain=…:toAddress=…:nonce=…",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "address",
         "amount",
         "toChain",
         "nonce",
         "signature"
        ],
        "properties": {
         "address": {
          "type": "string"
         },
         "amount": {
          "type": "string"
         },
         "toChain": {
          "type": "integer"
         },
         "toAddress": {
          "type": "string"
         },
         "nonce": {
          "type": "string"
         },
         "signature": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/reserves": {
   "get": {
    "tags": [
     "netting"
    ],
    "summary": "Proof of reserves: liabilities vs backing float",
    "description": "Publishes total custodied liabilities against the relayer USDC float backing them, broken down per chain, so solvency can be checked by anyone rather than taken on trust. Deposits are refused if they would push liabilities above a safe share of that float.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/netting/preview": {
   "get": {
    "tags": [
     "netting"
    ],
    "summary": "What a withdrawal would actually deliver, before you sign",
    "description": "Free, read-only. Given an amount, returns the exit cost and the net delivered amount for every chain, ranked best-first — exit costs differ by more than 1000x, so which chain you leave on is a real decision. Pass toChain to price one chain and learn whether it would succeed at all. Internal transfers between netting accounts are free at any size; this cost applies only when leaving for a real on-chain balance.",
    "operationId": "getNettingPreview",
    "parameters": [
     {
      "name": "amount",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "Amount in USDC base units (6dp), e.g. \"1000000\" for $1.00."
     },
     {
      "name": "toChain",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer"
      },
      "description": "Price a single chain instead of ranking them all."
     }
    ],
    "responses": {
     "200": {
      "description": "Exit costs and delivered amounts",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/plan": {
   "post": {
    "tags": [
     "planning"
    ],
    "summary": "Validate a whole multi-step plan before executing step one",
    "operationId": "planRoute",
    "description": "Agents execute WORKFLOWS, not single transfers, but every quoting API validates one hop at a time — 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 recovered by discovering it was a dead end. POST the whole plan and get it validated up front. Fees and durations ACCUMULATE across steps. Evaluation stops at the first unroutable step rather than reporting speculative results for steps behind it. When a plan is infeasible the response names the BINDING CONSTRAINT and the value that would work (`bindingConstraint`, `whatWouldMakeThisWork`) — a bare \"no\" sends an agent into blind retries. Optional `constraints`: maxTotalFeeBps, maxTotalSeconds, maxStepSeconds, requireNonCustodial. READ-ONLY: nothing is signed, reserved or moved. Each feasible step now carries a signed `attestation` of its quoted terms, so a validated plan is also a LOCKABLE one: pass a step's attestation with `minOut` to that step's execute call and execution is bound to the terms you were shown. Without it the plan is advice only, and the price can move between validating and running it.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "steps"
        ],
        "properties": {
         "steps": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/RouteRequest"
          }
         },
         "constraints": {
          "type": "object",
          "properties": {
           "maxTotalFeeBps": {
            "type": "integer",
            "example": 60
           },
           "maxTotalSeconds": {
            "type": "integer",
            "example": 120
           },
           "maxStepSeconds": {
            "type": "integer"
           },
           "requireNonCustodial": {
            "type": "boolean"
           }
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Per-step results, accumulated totals, feasibility and (if infeasible) the binding constraint plus its remedy",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Malformed plan",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/outcome/verify/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "fl_reconproof_msaxh1ls"
    },
    {
     "name": "recipient",
     "in": "query",
     "required": false,
     "schema": {
      "type": "string"
     },
     "description": "Post-condition: assert the address that must have been credited."
    },
    {
     "name": "minAmount",
     "in": "query",
     "required": false,
     "schema": {
      "type": "string"
     },
     "description": "Post-condition: assert the minimum base units that must have arrived."
    }
   ],
   "get": {
    "tags": [
     "verification"
    ],
    "summary": "Verify a settled transfer against the destination chain (not against our database)",
    "operationId": "verifyOutcome",
    "description": "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 checks the DESTINATION CHAIN: it fetches the payout transaction, decodes its ERC-20 Transfer logs, and reports what actually happened. You supply the post-conditions (`recipient`, `minAmount`) and we tell you whether reality matches. It also cross-checks our own receipt against the chain and will say so, in plain terms, if our books disagree. THREE-VALUED: `verified` | `failed` | `undetermined`. `undetermined` (chain unreachable, tx not yet mined, unknown id) is NOT a pass — collapsing \"could not check\" into \"fine\" is how a loss gets reported as a success. The response includes the destination tx hash so you can reproduce the whole result yourself without trusting us. COVERS EVERY RAIL, and is candid about the ones it cannot re-derive: Allbridge, Chainflip, Solana and Tron settlements record completion as an observed BALANCE DELTA rather than a transaction we can fetch and decode, and non-EVM destinations are not reachable via EVM JSON-RPC. Those return `undetermined` with the reason stated — never `verified` on the strength of a database row. For non-EVM destinations it now QUERIES the chain and returns a `balanceObservation` (live balance vs recorded baseline plus delivered amount). That observation is reported alongside the verdict and never replaces it: a balance cannot prove a specific transfer, because tokens are fungible and recipients spend them, so the verdict stays `undetermined`.",
    "responses": {
     "200": {
      "description": "Verdict plus each assertion checked against chain state",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "No settled transfer with that id — verdict undetermined, never verified",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/forensics/fee/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "fl_reconproof_msaxh1ls"
    }
   ],
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "Account for every unit of a fee on a settled transfer",
    "operationId": "explainFee",
    "description": "Everyone quotes a fee; nobody says where it WENT. This reconstructs a completed transfer unit by unit — what was sent, what arrived, and every component of the difference, each labelled with who it was paid to and how the figure was derived (destination payout gas, Robyn margin, or a Robyn SUBSIDY when we absorbed a loss, which is reported as such). Anything that cannot be attributed is returned 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.",
    "responses": {
     "200": {
      "description": "Unit-by-unit fee breakdown with payee and basis per component",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "Unknown transfer id — no breakdown is invented",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/forensics/counterfactual/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "fl_reconproof_msaxh1ls"
    }
   ],
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "What the same transfer would have cost on the alternative rail",
    "operationId": "routeCounterfactual",
    "description": "Answers \"was this actually the better route?\" with evidence rather than marketing: it applies the published standard-bridge fee model to this exact amount and destination and reports the difference against what you really received. Stated plainly in the response: this is a like-for-like MODEL comparison, not a claim about what a specific third-party bridge would have quoted at that second.",
    "responses": {
     "200": {
      "description": "Actual vs modelled-alternative delivery, with the difference and a verdict",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "Unknown transfer id",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/statement/spend": {
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "A SIGNED statement of what an address spent (proof for a principal)",
    "operationId": "spendStatement",
    "description": "An AI agent that spends on someone's behalf cannot currently PROVE what it spent — its own summary is a claim by the party being audited, and a dashboard screenshot proves nothing. This returns every transfer in the window with the fee on each broken down, SIGNED by Robyn's relayer key. The principal verifies the signature against the published signer address and knows the numbers came from us, unmodified — without trusting the agent and without trusting a web page. NOW COVERS EVERY RAIL — the float lane plus routed settlements (CCTP, Stellar, Solana, Tron, Chainflip, aggregators) — with a `byRail` split so a merged total cannot hide which rails were used. It previously covered the float lane only; that was declared as a limitation, but an honest limitation was still a hole. Third-party bridge fees taken inside a routed leg are not itemised and are excluded, which `limitations` states.",
    "parameters": [
     {
      "name": "address",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Restrict to transfers whose record names this sender. Transfers with no sender recorded are EXCLUDED rather than assumed to be yours."
     },
     {
      "name": "days",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "default": 30,
       "maximum": 365
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Signed statement: entries, per-entry fee breakdown, totals, digest, signature, limitations",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/statement/verify": {
   "post": {
    "tags": [
     "transparency"
    ],
    "summary": "Verify a signed spend statement",
    "operationId": "verifyStatement",
    "description": "POST a statement back to check it. The digest is RECOMPUTED from the rows you submit and never taken from the submitted `digest` field — otherwise a genuine signature could be paired with fabricated numbers. Alter any amount and verification fails. You do not need this endpoint to trust the result: the signature is a standard EIP-191 personal_sign over the digest, so you can verify it entirely offline against the published signer address.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid true/false, recomputed digest, recovered signer, and why",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/delivery/stats": {
   "get": {
    "tags": [
     "transparency"
    ],
    "summary": "MEASURED delivery times per rail and corridor",
    "operationId": "deliveryStats",
    "description": "Quotes used to advertise a constant duration — the same number for an instant float payout and a Stellar burn waiting on Circle attestation. An agent planning a workflow needs to know whether value lands in fifteen seconds or twenty-five minutes; one marketing number is worse than useless because it is confidently wrong for most corridors. This publishes what settlements ACTUALLY took, per corridor. HONESTY RULE: below the sample threshold it returns `insufficient-data` and hands back the static estimate LABELLED as an estimate (`basis: \"estimate\"` vs `basis: \"measured\"`). It never invents a percentile from three observations.",
    "responses": {
     "200": {
      "description": "Per-rail and per-corridor p50/p90 where measured, explicit insufficient-data otherwise",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/refusal/info": {
   "get": {
    "tags": [
     "verification"
    ],
    "summary": "What refusal receipts are and how to get one",
    "operationId": "refusalInfo",
    "responses": {
     "200": {
      "description": "Explanation, signer address, and how to verify offline",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/refusal/recent": {
   "get": {
    "tags": [
     "verification"
    ],
    "summary": "Recently issued refusal receipts",
    "operationId": "recentRefusals",
    "parameters": [
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "default": 25,
       "maximum": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Signed refusal receipts, each independently verifiable",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/refusal/verify": {
   "post": {
    "tags": [
     "verification"
    ],
    "summary": "Verify a signed refusal receipt",
    "operationId": "verifyRefusal",
    "description": "When a payment provider says no, the AGENT carries the blame: its principal sees an incomplete task and has only the agent’s account of why. \"The relay refused me\" is indistinguishable from \"I got it wrong\" or \"I never tried\". Send `x-anygas-receipt: 1` on any request and, IF we refuse it, the error response carries a signed `refusalReceipt` recording what was asked, when, and the reason we actually returned. Hand it to your principal; they verify 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, so blame cannot be shifted in either direction. A receipt proves the refusal HAPPENED. It is explicitly not a claim that the refusal was correct — use it to evidence that you were blocked, or to contest the decision with us. Receipts are opt-in and error-only: successful requests never mint one.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid true/false, recomputed digest, recovered signer, and why",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/payreq/info": {
   "get": {
    "tags": [
     "payments"
    ],
    "summary": "What verifiable payment requests are and how the flow works",
    "operationId": "payreqInfo",
    "responses": {
     "200": {
      "description": "Explanation and the five-step flow",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/payreq/prepare": {
   "post": {
    "tags": [
     "payments"
    ],
    "summary": "Prepare a payment request for THE PAYEE to sign",
    "operationId": "payreqPrepare",
    "description": "Build the canonical request (payee, chain, token, amount, memo, expiry) and return the digest for the payee to sign with their own wallet. ROBYN NEVER SIGNS THESE and holds no payee keys — a request signed by Robyn would prove only that Robyn said so, which is exactly the trust this removes.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "request + digest + signing instructions",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "missing/invalid fields",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/payreq/verify": {
   "post": {
    "tags": [
     "payments"
    ],
    "summary": "Verify a payment request before paying it",
    "operationId": "payreqVerify",
    "description": "Agents can pay each other but could not ASK each other for money verifiably. A plaintext \"send 5 USDC to 0x…\" over an unauthenticated channel is exactly the message that address-substitution theft preys on. This confirms the payee address, chain, token and amount were signed by whoever controls the payee address, and that the request has not expired. Rewrite the payee or the amount and the signature stops recovering. If `signedByPayee` is false, DO NOT PAY. A valid signature proves AUTHENTICITY — not that the payment is owed, and not that the payee is trustworthy. Authenticity is not authorisation.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid, signedByPayee, expired, recovered signer and why",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/payreq/settlement": {
   "post": {
    "tags": [
     "payments"
    ],
    "summary": "Prove on-chain that a payment request was satisfied",
    "operationId": "payreqSettlement",
    "description": "Pass the signed request plus the `transferId` that paid it. Re-checks the transfer against the DESTINATION CHAIN and confirms the payee received at least the amount requested — so you end up with evidence both that the request was genuine and that you honoured it. `paid: null` means undetermined, which is NOT evidence of payment.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "paid true/false/null, verdict, and the full on-chain check",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/evidence/bundle/{id}": {
   "parameters": [
    {
     "name": "id",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "fl_reconproof_msaxh1ls"
    },
    {
     "name": "recipient",
     "in": "query",
     "required": false,
     "schema": {
      "type": "string"
     },
     "description": "assert who should have been paid"
    },
    {
     "name": "minAmount",
     "in": "query",
     "required": false,
     "schema": {
      "type": "string"
     },
     "description": "assert the minimum that should have arrived"
    }
   ],
   "get": {
    "tags": [
     "verification"
    ],
    "summary": "One signed bundle of ALL evidence about a transfer",
    "operationId": "evidenceBundle",
    "description": "Robyn can answer four separate questions about a transfer — did it happen (on-chain verification), where did every unit of the fee go, was this the better route, and is the delivery time measured or guessed. Making four calls and stitching the answers together is work an agent will skip, and four unrelated JSON blobs are not something a principal can check. This is one call and ONE SIGNATURE over the assembled whole. 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. Components that could not be produced appear as `unavailable` WITH a reason and are listed in `incomplete`; they are never silently omitted, because a bundle missing a check must not read like one that passed.",
    "responses": {
     "200": {
      "description": "Assembled, signed evidence: outcome, fees, counterfactual, delivery basis",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/evidence/verify": {
   "post": {
    "tags": [
     "verification"
    ],
    "summary": "Verify a signed evidence bundle",
    "operationId": "verifyEvidence",
    "description": "Recomputes the digest from the SUBMITTED components — never the supplied `digest` — so swapping any single part (a fee line, an on-chain check, the counterfactual, the transfer id, even the issue timestamp) invalidates it. Without that, a bundle would be a convenient vehicle for laundering a false conclusion through a genuine signature. A valid signature means the CONTENTS are authentic; it does not mean the transfer succeeded. A genuine bundle can honestly report a failure.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid, recomputed digest, recovered signer",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/capability/snapshot": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Signed record of what Robyn can actually do right now",
    "operationId": "capabilitySnapshot",
    "description": "An agent deciding whether to depend on a corridor has only a status page — a claim about NOW that is gone by the time anything goes wrong. When a workflow later fails, nobody can reconstruct what the provider said it could do at the moment the decision was made. This is a signed, timestamped record: `serviceableNow` (chains that can ACTUALLY be paid out on, from live inventory), `policyEligible` (the marketing list), `notServiceable` (the difference), per-rail `executeReady`, node health, and whether delivery figures are measured or estimated. USE `serviceableNow`, NOT `policyEligible`, when choosing a destination. It is a RECORD, not a guarantee: it states what was true at that instant, does not promise the same capability a minute later, and the signature does not make it an SLA — `whatThisIsNot` says so in the payload.",
    "responses": {
     "200": {
      "description": "Signed capability snapshot",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/capability/verify": {
   "post": {
    "tags": [
     "discovery"
    ],
    "summary": "Verify a capability snapshot",
    "operationId": "verifyCapability",
    "description": "Recomputes the digest from the submitted claims, so inflating `serviceableNow` to the full policy list — the tempting lie — invalidates it. Also reports `ageMinutes` and warns when a snapshot is stale: still authentic, but no longer a description of the present.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid, recovered signer, age and staleness warning",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/why": {
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "Why this route won — and what it actually beat",
    "operationId": "routeWhy",
    "description": "Every router in this space returns a winner. None 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 — so an agent cannot tell a good price from the only price. This returns the whole decision: every rail that quoted, what each would deliver, the margin the winner won by, and which rails stayed SILENT and why. CHECK `wasCompetitive` BEFORE PRESENTING A PRICE AS GOOD. When false, exactly one rail answered and \"best\" means \"only\". Rails listed in `silentRails` did NOT lose — they failed to answer (down, rate-limited, missing credentials) and each carries its reason. Counting silence as defeat is how a router flatters its own coverage. Signed, so the rationale can be archived. Silent rails are CLASSIFIED: `status:\"not-configured\"` (missing credentials — will never recover on retry, needs an operator) versus `status:\"degraded\"` (rate limit or cooldown — may self-heal). Each carries `recoversOnRetry` and a `remedy`.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RouteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Signed rationale: chosen, runnerUp, margin, candidates, silentRails, wasCompetitive",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "502": {
      "description": "Comparison unavailable",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/why/verify": {
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "Verify a signed routing rationale",
    "operationId": "verifyWhy",
    "description": "Recomputes the digest from the submitted decision, so inflating the winner's delivery, renaming the winning rail, erasing the silent rails or flipping `wasCompetitive` all invalidate it.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid, recomputed digest, recovered signer",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/improve": {
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "What would make this transfer cheaper?",
    "operationId": "improveCost",
    "description": "Every router answers \"what does this cost?\". None answers \"what should I change?\". An agent handed a price can only pay it or give up — yet the price is usually a consequence of choices it did not know it was making: the destination chain (payout gas varies by more than 1000x), the size (a fixed cost is ruinous on a small transfer and trivial on a large one), whether the instant float lane applied, and whether the rails that would have competed were even available. Every suggestion carries the REAL saving in base units, computed from the same cost table the router charges from. CRITICAL: suggestions flagged `changesOutcome: true` move WHERE THE MONEY LANDS. Do not apply them as pure cost savings — telling someone to send to a different chain is not a cost tip if that is not where the funds were needed. If nothing can be improved, it says so instead of inventing filler.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RouteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Quantified suggestions, each declaring changesOutcome",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "amount is required",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/prepare": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Build a spending mandate for the PRINCIPAL to sign",
    "operationId": "mandatePrepare",
    "description": "Handing money to an autonomous agent is all-or-nothing today, and 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 in its prompt is a suggestion. This builds a budget the PRINCIPAL signs with their own wallet. Robyn never signs mandates and holds no principal keys — a budget signed by Robyn would be a budget Robyn granted itself. SCOPE: covers spending THROUGH ROBYN only. It is not custody and cannot stop the agent spending elsewhere with the same key; the payload says so. Treat it as an auditable budget, not a hard cap. Optional `cosignAbove` (base units) adds a PER-SPEND approval threshold: spends above it require the principal to countersign that specific payment. Omit it, or pass 0, for no threshold — mandates granted before this existed behave exactly as before. Pass `sandbox: true` (and on register) to create a throwaway mandate in a separate store — used by our own tests so test grants never appear in an owner's exposure figures.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "mandate + digest to sign",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "missing/invalid fields",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/register": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Register a principal-signed mandate",
    "operationId": "mandateRegister",
    "description": "Verifies the signature recovers to the principal NAMED IN THE MANDATE. An impostor signing a budget that names someone else as principal is refused with 401 — otherwise anyone could grant themselves an allowance.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "registered mandate status",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "signature does not recover to the named principal",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/status": {
   "get": {
    "tags": [
     "mandates"
    ],
    "summary": "Remaining budget",
    "operationId": "mandateStatus",
    "parameters": [
     {
      "name": "mandateId",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "description": "Consumption counts ONLY transfers verified on-chain, so the remaining figure cannot be inflated by claiming spends that never happened, nor understated by omitting ones that did.",
    "responses": {
     "200": {
      "description": "consumed / remaining / active",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/check": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Would this spend fit?",
    "operationId": "mandateCheck",
    "description": "Returns WITHIN_MANDATE, EXCEEDS_MANDATE or EXPIRED. If it exceeds, do not split the spend to get under the limit — that defeats the control the principal set. Ask them to raise it.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "verdict + remaining",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/consume": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Record a spend against a mandate",
    "operationId": "mandateConsume",
    "description": "Requires a `transferId`. The transfer is VERIFIED ON-CHAIN before anything is counted, and the amount counted is what the destination chain shows arrived — not what the caller claims. An unverifiable transfer is not counted (and the response says so plainly, since that also means the budget does not yet reflect it). The same transferId cannot be counted twice. If the mandate has a `cosignAbove` threshold and the amount exceeds it, a valid `cosignature` from the principal is required and is checked BEFORE any on-chain work — an unapproved large spend is refused on its face. The agent cannot approve its own. REQUIRES `agentSignature`: the AGENT named in the mandate must sign keccak256(\"v1|robyn-mandate-consume|<mandateId>|<transferId>|<amount>\"). Without it, anyone who learned the mandateId could attribute an unrelated on-chain transfer to your budget and exhaust it — consumption needs a verifiable transfer, so a spend cannot be fabricated, but it could be MISATTRIBUTED. The authorisation binds the transfer as well as the amount, so it cannot be replayed onto a different payment. Note the principal cannot consume on the agent's behalf; this authenticates the spender, not the grantor.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "counted / not counted, with the on-chain verdict",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "missing transferId or unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/revoke": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Withdraw a mandate immediately",
    "operationId": "mandateRevoke",
    "description": "A budget you cannot withdraw is not a safety control: the moment you most need the limit is the moment you want it gone entirely, not merely capped, and waiting for expiry is not an option. Only the PRINCIPAL who granted the mandate can revoke it — an agent cannot revoke the budget that constrains it, and a third party cannot revoke someone else's (401 otherwise). Revocation is DELIBERATELY IRREVERSIBLE. A kill switch that can be flipped back is one an attacker who briefly holds the key can undo; issue a fresh mandate instead. It stops Robyn endorsing further spend. It does NOT claw back money already spent and does not stop the agent spending elsewhere with the same key — if the key is compromised, rotate the key. The consumption record is kept so history stays auditable.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "revoked status",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "not signed by the principal",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/certificate": {
   "get": {
    "tags": [
     "mandates"
    ],
    "summary": "Signed proof the agent stayed within budget",
    "operationId": "mandateCertificate",
    "parameters": [
     {
      "name": "mandateId",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "description": "What a principal actually wants at the end of a period: not a dashboard, but a document they can verify and keep. Lists every counted spend, the budget, the total, and whether it stayed inside. Every spend listed was VERIFIED ON-CHAIN before counting, and the figure is what the destination chain showed arrived — not what the agent reported. It certifies adherence to a ROBYN budget. It does NOT certify the agent spent nothing elsewhere, because Robyn can only account for money that moved through Robyn — `doesNotCertify` says so in the payload rather than in a footnote, since a principal reading it as proof of total spending would be badly misled.",
    "responses": {
     "200": {
      "description": "signed compliance certificate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/list": {
   "get": {
    "tags": [
     "mandates"
    ],
    "summary": "Every budget a principal has granted",
    "operationId": "mandateList",
    "parameters": [
     {
      "name": "principal",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "description": "You cannot revoke what you cannot find, and expecting anyone to have kept every mandateId is how a forgotten grant stays live for months. Lists remaining budget, co-sign threshold, expiry and revocation state, live mandates first, with the total still drawable WITHOUT further approval. Returns only mandates granted by the address you ask about.",
    "responses": {
     "200": {
      "description": "mandates granted by this principal",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "invalid address",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/cosign-request": {
   "post": {
    "tags": [
     "mandates"
    ],
    "summary": "Does this spend need the principal to approve it?",
    "operationId": "mandateCosignRequest",
    "description": "A mandate caps a TOTAL, which is the wrong control for the risk people actually fear: \"$500 this month\" still permits one catastrophic $500 transfer to the wrong place. Set `cosignAbove` when preparing a mandate and any spend over it needs the principal to approve THAT PAYMENT. Returns whether approval is required and the exact digest to sign. The digest binds the mandate, the transfer AND the amount — an approval therefore cannot be replayed against a different payment, which is what would otherwise reduce the control to theatre. If a spend is refused for want of a countersignature, get the principal to sign it — do NOT split the spend to slip under the threshold.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "cosignatureRequired + digest to sign",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "unknown mandate",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/changelog": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "What changed since you integrated",
    "operationId": "changelog",
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "example": "3.9.0",
      "description": "the API version your integration was built against"
     }
    ],
    "description": "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. Pass `since` and you get only what changed after that version. CHECK `mustAct` FIRST — if true, there are breaking changes and your integration may already be failing; each such entry carries an `action`. `breaking:true` means an existing CORRECT integration could stop working. It is a compatibility claim, not a measure of importance, and nothing that qualifies is downgraded because it looks bad. Every path claimed here is cross-checked against the live spec on each request; drift is reported rather than hidden, because a changelog is believed.",
    "responses": {
     "200": {
      "description": "Change history, filtered",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/preflight/full": {
   "post": {
    "tags": [
     "routing"
    ],
    "summary": "One call: should I do this, and what should I know first?",
    "operationId": "fullPreflight",
    "description": "Everything needed to decide well exists, but across five endpoints: can this corridor be served right now, which rails are actually competing, what would make it cheaper, is the duration measured or guessed, and does this spend fit the budget the principal set. An agent that must make five calls and reconcile them will make one and guess. BLOCKERS AND WARNINGS ARE NEVER MERGED. A blocker WILL fail; a warning might cost you. An agent that cannot tell \"impossible\" from \"suboptimal\" either aborts on noise or ploughs through real obstacles. A check that could not run is reported in `unavailable` and DOWNGRADES the verdict to `caution` — silence is never read as assent. Pass an unknown `mandateId` and you are BLOCKED, because you would be spending outside any budget while believing you were inside one. `proceed:true` means nothing KNOWN TO US blocks it. It is not a promise of success.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/RouteRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "verdict, blockers, warnings and the supporting detail",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "fromChain, toChain and amount are required",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/counterparty/check": {
   "post": {
    "tags": [
     "verification"
    ],
    "summary": "Have I paid this address before?",
    "operationId": "counterpartyCheck",
    "description": "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. Returns YOUR OWN history with the address: times paid, how recently, typical amount, and whether this amount is unusual for that counterparty. IT 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. A `first-time-recipient` result is not a warning about the recipient either; every legitimate relationship has a first payment. It is the moment to confirm the address by a second channel. READ `coverage`: the float-lane ledger does not retain recipient addresses, so float-lane payouts are excluded and \"first time\" can be wrong. Surfaced automatically as a WARNING (never a blocker) in /api/preflight/full.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "history, signal and explicit limits",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "invalid address",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/mandate/events": {
   "get": {
    "tags": [
     "mandates"
    ],
    "summary": "What has happened to my budgets",
    "operationId": "mandateEvents",
    "parameters": [
     {
      "name": "principal",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "since",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "the cursor from your last call"
     },
     {
      "name": "limit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "default": 100,
       "maximum": 500
      }
     }
    ],
    "description": "A principal who granted a budget cannot otherwise learn that it is nearly spent, or that a large payment is waiting on their approval, without polling every mandate individually. Emits `spend`, `nearly-exhausted`, `exhausted`, `approval-required` and `revoked`. Pass the returned `cursor` back as `since` for only what is new. DELIBERATELY A PULL FEED, NOT A WEBHOOK: a mandate is created by an untrusted caller, so letting it name a callback URL would hand anyone a request primitive originating inside a host that holds signing keys. The convenience of a push is not worth that, and the response says so.",
    "responses": {
     "200": {
      "description": "events plus a cursor",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/recipes": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Ordered playbooks for real goals",
    "operationId": "recipes",
    "description": "There are 100+ endpoints here. The spec 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 written for humans, so every agent re-derives it badly and differently — quoting without checking serviceability, paying without confirming a recipient, reporting success when verification said `undetermined`. Each recipe is an ordered sequence: which endpoint, what it answers, what to do with the answer, whether it is optional, and what skipping it actually costs. Every referenced endpoint is checked against the live spec on each request — a recipe naming an endpoint that no longer exists is worse than none, because it will be followed.",
    "responses": {
     "200": {
      "description": "All recipes, with drift flags",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/recipes/{name}": {
   "parameters": [
    {
     "name": "name",
     "in": "path",
     "required": true,
     "schema": {
      "type": "string"
     },
     "example": "pay-someone-safely"
    }
   ],
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "One playbook",
    "operationId": "recipe",
    "responses": {
     "200": {
      "description": "The recipe",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "Unknown name, with the available list",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/integration/lint": {
   "post": {
    "tags": [
     "discovery"
    ],
    "summary": "Is the request I am about to send safe?",
    "operationId": "lintIntegration",
    "description": "Every provider documents the safe way to integrate and then has no idea whether anyone followed it. Integrators discover they skipped something when it costs them. Post the request you INTEND to send and get the missing/unsafe fields with the concrete consequence of each. Severity is honest: `unsafe` means you can lose money or report a falsehood; `suboptimal` means it will cost you. IT LINTS THE REQUEST ONLY. It cannot see 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 returns \"unchecked\", never \"clean\".",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "findings with severity, field, consequence and fix",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "endpoint required",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/integration/rules": {
   "get": {
    "tags": [
     "discovery"
    ],
    "summary": "Every lint rule",
    "operationId": "lintRules",
    "responses": {
     "200": {
      "description": "the rule set",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/route/resume": {
   "post": {
    "summary": "Finish a CCTP transfer whose mint never landed",
    "description": "A CCTP transfer is two transactions: a burn on the source chain and a mint on the destination, separated by Circle's attestation (~15-25 min standard tier). This submits the mint. It is SAFE TO RETRY: CCTP nonces are single-use, so a replayed mint reverts and is reported as ALREADY_MINTED (funds delivered) rather than double-spending. robyn-cctp-mint runs this automatically every 2 minutes, so callers rarely need it. Rate limited to 6/min per IP because it spends Robyn gas, and concurrent resumes for the same route id return 409 rather than submitting a second transaction.",
    "tags": [
     "routes"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "id"
        ],
        "properties": {
         "id": {
          "type": "string",
          "description": "route id from execute or /api/route/status"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Settled. code is MINTED, ALREADY_MINTED (Circle auto-relayer delivered first) or ALREADY_DONE."
     },
     "404": {
      "description": "Unknown route id."
     },
     "409": {
      "description": "Not settled yet (ATTESTATION_NOT_READY) or a resume for this id is already in progress. Funds are safe and the mint stays replayable."
     },
     "422": {
      "description": "Route is not on the Circle CCTP rail."
     },
     "429": {
      "description": "Rate limited."
     }
    }
   }
  },
  "/api/audit/drift": {
   "get": {
    "summary": "Has anything about this system changed?",
    "description": "Robyn runs an hourly drift audit that captures its own security and operational posture (guards present, public listeners, file permissions, dependency vulnerabilities, suite results, solvency invariant) and reports only the DIFFERENCES from the previous run. This endpoint publishes the summary. `recent[].changes: 0` across runs means nothing moved — no guard was removed, no port opened, no permission widened, no suite regression. Values are summarised deliberately; the raw baseline contains operational detail that is not published.",
    "tags": [
     "status"
    ],
    "responses": {
     "200": {
      "description": "Drift posture and recent change counts"
     },
     "429": {
      "description": "Rate limited"
     }
    }
   }
  },
  "/api/stealth/info": {
   "get": {
    "summary": "Stealth address support (ERC-5564)",
    "tags": [
     "privacy"
    ],
    "description": "Publish ONE meta-address; every payer derives a FRESH address from it, so two payments to you cannot be linked on-chain and the payer never learns where your funds actually live. Pay to one by sending \"toStealth\": \"st:eth:0x...\" instead of \"toAddress\" on /api/route/execute. Scanning is a CLIENT operation — this service never accepts a viewing key, because handing it over would let us link every payment to you, which is precisely what stealth addresses remove.",
    "responses": {
     "200": {
      "description": "Scheme status, self-check result, and the honest limits"
     }
    }
   }
  },
  "/api/stealth/derive": {
   "post": {
    "summary": "Derive a stealth address from a meta-address",
    "tags": [
     "privacy"
    ],
    "responses": {
     "200": {
      "description": "stealthAddress, ephemeralPubKey, viewTag"
     },
     "400": {
      "description": "BAD_META_ADDRESS"
     }
    }
   }
  },
  "/api/stealth/announcements": {
   "get": {
    "summary": "Announcement feed to scan with your viewing key",
    "tags": [
     "privacy"
    ],
    "responses": {
     "200": {
      "description": "Paginated announcements"
     }
    }
   }
  },
  "/api/netting/proof/root": {
   "get": {
    "summary": "Proof-of-liabilities root (Merkle sum tree)",
    "tags": [
     "privacy"
    ],
    "description": "Solvency without disclosure. We publish a Merkle sum-tree ROOT and the TOTAL of all netting liabilities; each account can request an inclusion proof showing their balance is inside that total, and the total is checkable against on-chain backing. Nobody learns anybody else's balance. This proves inclusion and the total — it cannot alone prove no account was omitted; that assurance comes from many users each checking their own proof.",
    "responses": {
     "200": {
      "description": "root, totalUnits, backingUnits, solvent"
     }
    }
   }
  },
  "/api/netting/proof": {
   "post": {
    "summary": "Your inclusion proof (signature required)",
    "tags": [
     "privacy"
    ],
    "description": "A proof discloses that account's balance, so it is released only to whoever controls the account. Sign the returned challenge with the account key (EIP-191).",
    "responses": {
     "200": {
      "description": "Inclusion proof + self-verification"
     },
     "401": {
      "description": "Signature required; challenge returned"
     }
    }
   }
  },
  "/api/ohttp/keys": {
   "get": {
    "tags": [
     "privacy"
    ],
    "summary": "OHTTP gateway key config",
    "description": "HPKE public key configuration for Oblivious HTTP. Fetch this, encapsulate your request to the returned publicKey, then POST the sealed bytes to the RELAY at https://relay.nightferry.net/relay — not to this host. Suite: DHKEM(X25519,HKDF-SHA256)/HKDF-SHA256/AES-128-GCM. Response encryption uses the HPKE export secret with info \"robyn-ohttp-response-v1\"; the response is 12-byte nonce || ciphertext || 16-byte GCM tag under AES-256-GCM. NOTE: this wire format is the OHTTP architecture, not RFC 9458 byte-compatible — the inner message is JSON, not RFC 9292 BHTTP.",
    "responses": {
     "200": {
      "description": "Key configuration",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "keyId": {
           "type": "string"
          },
          "kem": {
           "type": "string"
          },
          "kdf": {
           "type": "string"
          },
          "aead": {
           "type": "string"
          },
          "publicKey": {
           "type": "string",
           "description": "base64 X25519 public key"
          },
          "wireFormat": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/ohttp/gateway": {
   "post": {
    "tags": [
     "privacy"
    ],
    "summary": "OHTTP gateway (send via the relay, not here)",
    "description": "Accepts an encapsulated request: 32-byte HPKE enc || ciphertext, content-type application/octet-stream. Returns an encapsulated response. DENY BY DEFAULT — only /api/status, /api/route/compare, /api/route/quote, /api/privacy/*, /api/stealth/announcements and /api/netting/reserves are dispatchable; owner and capability surfaces are refused with OHTTP_PATH_DENIED. Posting here DIRECTLY works but gives you NO address protection: we see your IP exactly as on any other call. Send through https://relay.nightferry.net/relay instead, which sees your address and cannot read your request.",
    "requestBody": {
     "required": true,
     "content": {
      "application/octet-stream": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Encapsulated response (opaque bytes)",
      "content": {
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "description": "Could not decapsulate — deliberately uninformative to avoid a decapsulation oracle"
     }
    }
   }
  },
  "/api/privacy/spec": {
   "get": {
    "tags": [
     "privacy"
    ],
    "summary": "Machine-readable privacy capability spec",
    "description": "What privacy features exist, which are selectable per request, and what each one does and does not protect against. Read this before assuming a guarantee.",
    "responses": {
     "200": {
      "description": "Capability spec",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/privacy/receipt/verify": {
   "post": {
    "tags": [
     "privacy"
    ],
    "summary": "Verify a privacy receipt",
    "description": "Check a receipt against what was actually done, so a privacy claim is verifiable rather than asserted.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Verification result",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/pool/info": {
   "get": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "Pool readiness, blocked gates and design notes",
    "description": "BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "responses": {
     "200": {
      "description": "Readiness, blocked arming gates, registered association providers"
     }
    }
   }
  },
  "/api/pool/state": {
   "get": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "Live contract state",
    "description": "Always answers. `source` is one of chain | cache | ledger-only so you can tell a confirmed number from a remembered one. BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "responses": {
     "200": {
      "description": "Deposits, cap, balance, current root, indexer freshness"
     }
    }
   }
  },
  "/api/pool/sets": {
   "get": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "Association sets and Merkle paths",
    "description": "Pass ?commitment=<64 hex> to also receive the Merkle path for that deposit. Every set carries anonymity.{size, weak, threshold, advice} — check `weak` before relying on this for privacy. Members are listed in order so you can rebuild the Poseidon depth-20 tree and verify the root yourself. Rate limited to 30/min. BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "parameters": [
     {
      "name": "commitment",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-f]{64}$"
      },
      "description": "your deposit commitment, to receive its authentication path"
     }
    ],
    "responses": {
     "200": {
      "description": "Full-pool and per-provider sets, roots, paths and anonymity"
     }
    }
   }
  },
  "/api/pool/deposit": {
   "get": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "How to deposit (you send the transaction yourself)",
    "description": "Returns the contract, denomination and ABI. We do not take custody and do not relay deposits: a relayed deposit would record OUR address as the funding address for everybody, collapsing the provenance that association sets filter on. BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "responses": {
     "200": {
      "description": "Instructions, contract address, denomination, current state"
     }
    }
   },
   "post": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "Ask the indexer to re-scan now",
    "description": "Nothing in the request body is read or trusted; deposits are indexed from on-chain Deposit events. The keeper does this automatically every 2 minutes, so this only makes it sooner. Rate limited to 6/min. BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "responses": {
     "200": {
      "description": "What was newly indexed, plus current state"
     }
    }
   }
  },
  "/api/pool/withdraw": {
   "post": {
    "tags": [
     "privacy-pool"
    ],
    "summary": "Relay a withdrawal proof — we pay the gas",
    "description": "Build the PLONK proof client-side; your secret never leaves your machine. The recipient is bound inside the proof, so nobody (including us) can redirect the payout. Simulated before broadcast, so an invalid proof costs you a 400 rather than a failed transaction. Rate limited to 10/min. BETA: the withdrawal circuit is not independently audited. A hard on-chain deposit cap bounds the value at risk. Deposit only what you can afford to lose entirely.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "proof",
         "root",
         "nullifierHash",
         "recipient"
        ],
        "properties": {
         "proof": {
          "type": "array",
          "minItems": 24,
          "maxItems": 24,
          "items": {
           "type": "string"
          },
          "description": "24 field elements in snarkjs plonk.exportSolidityCallData order"
         },
         "root": {
          "type": "string",
          "pattern": "^0x[0-9a-fA-F]{64}$"
         },
         "nullifierHash": {
          "type": "string",
          "pattern": "^0x[0-9a-fA-F]{64}$"
         },
         "recipient": {
          "type": "string",
          "pattern": "^0x[0-9a-fA-F]{40}$"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Broadcast; returns txHash and the amount paid"
     },
     "400": {
      "description": "Malformed, or simulated and would revert (not broadcast)"
     },
     "409": {
      "description": "Note already spent, or a root the contract has never seen"
     },
     "413": {
      "description": "Body too large"
     },
     "429": {
      "description": "Rate limited"
     }
    }
   }
  },
  "/api/uptime": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "Measured uptime history (independent vantage)",
    "operationId": "uptime",
    "description": "Per-UTC-day availability of this origin as measured by an independent OHTTP relay (Iceland) probing GET /api/status every 10 minutes. A probe that never arrived counts against uptime. Derived from the request log; the window starts at the last pseudonym-salt rotation and grows.",
    "responses": {
     "200": {
      "description": "Uptime history",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "overall": {
           "type": "object"
          },
          "days": {
           "type": "array",
           "items": {
            "type": "object"
           }
          },
          "vantage": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/route/settle": {
   "post": {
    "tags": [
     "best-execution"
    ],
    "summary": "Plan a private, gasless, best-route settlement in one call",
    "description": "The one-verb primitive. Returns the best route (all rails compared), the gasless method, the privacy posture that will apply (API-relay + private mempool or sequencer-privacy + uniform shape), and what the settlement receipt will prove. Planning only - it does not broadcast; sign+submit via /api/route/execute."
   }
  },
  "/api/route/rollup": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "Aggregate health of the whole system",
    "description": "One call returning {ok, components[], degraded[]} across the router, the three signing daemons, the MCP endpoint, and the BATMAN privacy relay. 200 when all healthy, 503 when degraded."
   }
  },
  "/api/route/attestation": {
   "get": {
    "tags": [
     "status"
    ],
    "summary": "Signed proof the router is live AND private, right now",
    "description": "A relayer-signed, timestamped attestation that every component is up AND the privacy invariants hold (adversary probes cannot distinguish cover from real; the k-anonymity floor is enforced). Verify by recovering the signer over the digest - no trust in the endpoint required. v2 (anygas-attestation/2) also signs a CODE manifest: sha256 of every deployed module and the exact hash + SRI of each client bundle (code.bundles[\"anygas-web.js\"].sri) — compare what you loaded to what the relayer signed."
   }
  },
  "/api/route/attestation/verify": {
   "post": {
    "tags": [
     "status"
    ],
    "summary": "Verify a health+privacy attestation",
    "description": "Recomputes the digest and checks the relayer signature of an attestation object. Returns {valid, digestMatches, sigValid, healthy, private}."
   }
  },
  "/api/route/nettingset": {
   "get": {
    "tags": [
     "privacy"
    ],
    "summary": "Netting-as-anonymity-set shadow report",
    "description": "What the fused netting/privacy settlement mechanism would do over observed real flow (advisory until armed): internal-net vs batch vs defer, realCrowdRatio, and the guarantee that a sub-K real batch defers rather than settle filterably."
   }
  },
  "/api/messages/v2/info": {
   "get": {
    "summary": "Messaging v2 status + parameters (suite, buckets, release grid, kContribution)",
    "tags": [
     "messaging"
    ],
    "responses": {
     "200": {
      "description": "Info"
     }
    }
   }
  },
  "/api/messages/v2/mailbox/{addressOrName}": {
   "get": {
    "summary": "Recipient mailbox: stealth meta-address + hybrid ML-KEM public key",
    "tags": [
     "messaging"
    ],
    "parameters": [
     {
      "name": "addressOrName",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "0x address or Robyn name (label.robyn.id / .robynchain.eth / .rhood.eth)"
     }
    ],
    "responses": {
     "200": {
      "description": "Mailbox"
     },
     "404": {
      "description": "No v2 mailbox; fall back to v1"
     }
    }
   }
  },
  "/api/messages/v2/mailbox": {
   "post": {
    "summary": "Publish or rotate your mailbox (EIP-191 signed, monotonic by ts)",
    "tags": [
     "messaging"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "address",
         "metaAddress",
         "kemPub",
         "ts",
         "sig"
        ],
        "properties": {
         "address": {
          "type": "string"
         },
         "metaAddress": {
          "type": "string",
          "example": "st:eth:0x02…"
         },
         "kemPub": {
          "type": "string",
          "description": "base64 hybrid ML-KEM-768+X25519 public key (1216 bytes)"
         },
         "ts": {
          "type": "integer",
          "description": "unix seconds, ±10 min"
         },
         "sig": {
          "type": "string",
          "description": "personal_sign of \"robyn-messaging v2\\naction: mailbox\\naddress: …\\nmeta: …\\nkem: <sha256 of kemPub bytes>\\nts: …\""
         },
         "pass": {
          "type": "object",
          "description": "optional Plus pass {preparedMsg, sig} (base64) — rate-limit exempt, unlinkable",
          "properties": {
           "preparedMsg": {
            "type": "string"
           },
           "sig": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{ok, address, since} or {ok, unchanged:true}"
     },
     "403": {
      "description": "signature does not match address"
     }
    }
   }
  },
  "/api/messages/v2/send": {
   "post": {
    "summary": "Deliver a sealed message (server stores only ephemeralPubKey, viewTag, ct). Replay-idempotent.",
    "tags": [
     "messaging"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "ephemeralPubKey",
         "viewTag",
         "ct"
        ],
        "properties": {
         "ephemeralPubKey": {
          "type": "string",
          "description": "33-byte compressed secp256k1 point, one-time"
         },
         "viewTag": {
          "type": "string",
          "example": "0x3a"
         },
         "ct": {
          "type": "string",
          "description": "base64; suite 0x51; must be one of the fixed ciphertext sizes (see /info)"
         },
         "pass": {
          "type": "object",
          "description": "optional Plus pass {preparedMsg, sig} (base64) — rate-limit exempt, unlinkable",
          "properties": {
           "preparedMsg": {
            "type": "string"
           },
           "sig": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{ok, id, releaseAt} or {ok, id, duplicate:true}"
     },
     "400": {
      "description": "not a bucket-sized v2 seal"
     }
    }
   }
  },
  "/api/messages/v2/feed": {
   "get": {
    "summary": "Public feed (identical for every reader); scan with your viewing key",
    "tags": [
     "messaging"
    ],
    "parameters": [
     {
      "name": "since",
      "in": "query",
      "schema": {
       "type": "integer"
      },
      "description": "releaseAt cursor (ms)"
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "maximum": 200
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{total, count, nextSince, truncated, messages:[{id, ephemeralPubKey, viewTag, ct, releaseAt}]}"
     }
    }
   }
  },
  "/api/messages/send": {
   "post": {
    "summary": "[RETIRED 2026-08-20] v1 send — returns 410; use /api/messages/v2/send",
    "tags": [
     "messaging"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "from",
         "to",
         "message",
         "sig"
        ],
        "properties": {
         "from": {
          "type": "string"
         },
         "to": {
          "type": "string",
          "description": "address or Robyn name"
         },
         "message": {
          "type": "string"
         },
         "sig": {
          "type": "string",
          "description": "personal_sign of \"robyn-msg:<toLower>:<message>\""
         }
        }
       }
      }
     }
    },
    "responses": {
     "410": {
      "description": "GONE — messaging v1 writes are retired; use sealed v2 (POST /api/messages/v2/send)"
     }
    },
    "deprecated": true
   }
  },
  "/api/messages/inbox/{address}": {
   "get": {
    "summary": "v1 inbox (plaintext) — prefer v2",
    "tags": [
     "messaging"
    ],
    "parameters": [
     {
      "name": "address",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "messages"
     }
    }
   }
  },
  "/api/messages/resolve/{addressOrName}": {
   "get": {
    "summary": "Resolve a Robyn name or address",
    "tags": [
     "messaging"
    ],
    "parameters": [
     {
      "name": "addressOrName",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{resolved: address|null}"
     }
    }
   }
  },
  "/api/messages/v2/directory": {
   "get": {
    "summary": "All v2 mailboxes, identical for every reader (oblivious lookup)",
    "tags": [
     "messaging"
    ],
    "responses": {
     "200": {
      "description": "{count, mailboxes:[{address, metaAddress, kemPub, since}]}"
     }
    }
   }
  },
  "/api/plus/info": {
   "get": {
    "summary": "Plus status: mint public key (JWK), products, assets, what is and is not learned",
    "tags": [
     "plus"
    ],
    "responses": {
     "200": {
      "description": "Info"
     }
    }
   }
  },
  "/api/plus/invoice": {
   "post": {
    "summary": "Create an invoice for a bundle of blind passes",
    "tags": [
     "plus"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "asset"
        ],
        "properties": {
         "bundle": {
          "type": "string",
          "default": "bundle"
         },
         "asset": {
          "type": "string",
          "enum": [
           "eth-4663",
           "eth-1",
           "eth-42161",
           "eth-8453",
           "usdc-8453",
           "usdc-42161",
           "btc"
          ]
         },
         "refundAddress": {
          "type": "string",
          "description": "BTC only: your refund address"
         },
         "bundles": {
          "type": "integer",
          "minimum": 1,
          "maximum": 50,
          "default": 1
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{invoiceId, payTo, amount, human, tokens, expires}"
     }
    }
   }
  },
  "/api/plus/invoice/{id}": {
   "get": {
    "summary": "Invoice status",
    "tags": [
     "plus"
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Invoice"
     },
     "404": {
      "description": "unknown/expired"
     }
    }
   }
  },
  "/api/plus/claim": {
   "post": {
    "summary": "After paying: submit blinded tokens, receive blind signatures",
    "tags": [
     "plus"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "invoiceId",
         "blinded"
        ],
        "properties": {
         "invoiceId": {
          "type": "string"
         },
         "txHash": {
          "type": "string",
          "description": "EVM payment tx (not needed for btc)"
         },
         "blinded": {
          "type": "array",
          "items": {
           "type": "string"
          },
          "description": "base64 blinded messages (SDK blindTokens)"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{blindSigs, issued, remaining}"
     },
     "402": {
      "description": "payment not verified"
     },
     "409": {
      "description": "tx already used / nothing left"
     }
    }
   }
  },
  "/api/plus/redeem": {
   "post": {
    "summary": "Burn a pass (verification only; normally redeem inside an action)",
    "tags": [
     "plus"
    ],
    "responses": {
     "200": {
      "description": "ok"
     },
     "402": {
      "description": "invalid/spent"
     }
    }
   }
  },
  "/api/messages/v2/plus/cover": {
   "post": {
    "summary": "Spend a pass: one funded cover row within 24 h",
    "tags": [
     "messaging",
     "plus"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "pass"
        ],
        "properties": {
         "pass": {
          "type": "object",
          "description": "optional Plus pass {preparedMsg, sig} (base64) — rate-limit exempt, unlinkable",
          "properties": {
           "preparedMsg": {
            "type": "string"
           },
           "sig": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "scheduled"
     },
     "402": {
      "description": "pass required"
     }
    }
   }
  },
  "/api/messages/v2/plus/anchor": {
   "post": {
    "summary": "Spend a pass: anchor a feed row id in the next signed Merkle batch",
    "tags": [
     "messaging",
     "plus"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "pass",
         "id"
        ],
        "properties": {
         "pass": {
          "type": "object",
          "description": "optional Plus pass {preparedMsg, sig} (base64) — rate-limit exempt, unlinkable",
          "properties": {
           "preparedMsg": {
            "type": "string"
           },
           "sig": {
            "type": "string"
           }
          }
         },
         "id": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "queued"
     },
     "402": {
      "description": "pass required"
     }
    }
   }
  },
  "/api/messages/v2/anchor/{id}": {
   "get": {
    "summary": "Anchoring proof for a row id",
    "tags": [
     "messaging",
     "plus"
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{leaf, path, root, signer, signature, message, onchain}"
     },
     "404": {
      "description": "not anchored"
     }
    }
   }
  },
  "/api/messages/v2/anchors": {
   "get": {
    "summary": "Recent anchor batches",
    "tags": [
     "messaging",
     "plus"
    ],
    "responses": {
     "200": {
      "description": "batches"
     }
    }
   }
  },
  "/api/messages/v2/erasure": {
   "get": {
    "summary": "Signed proof-of-erasure statement (last purge + cumulative)",
    "tags": [
     "messaging"
    ],
    "responses": {
     "200": {
      "description": "statement"
     }
    }
   }
  },
  "/api/plus/trial": {
   "post": {
    "summary": "FREE trial: up to 20 blind passes per day per caller, no payment (identical to paid passes)",
    "tags": [
     "plus"
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "blinded"
        ],
        "properties": {
         "blinded": {
          "type": "array",
          "maxItems": 20,
          "items": {
           "type": "string"
          },
          "description": "base64 256-byte blinded messages (plus-sdk blindTokens())"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{blindSigs, issued, trial:true}"
     },
     "400": {
      "description": "malformed blinded value"
     },
     "429": {
      "description": "one trial per day per caller"
     }
    }
   }
  },
  "/api/posture": {
   "get": {
    "tags": [
     "privacy"
    ],
    "summary": "What is private, what is public — before you act",
    "operationId": "privacyPosture",
    "description": "One read: the lane you reached Robyn on (private via the BATMAN relay vs direct), PQ transport + one-size envelope facts, the chain's mempool class / private-submit default, the live mode of every privacy feature, what remains public, and recommendations. Reachable over OHTTP. Reads live flags — never a brochure.",
    "parameters": [
     {
      "name": "chainId",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "example": "8453"
     }
    ],
    "responses": {
     "200": {
      "description": "posture",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/receipt/info": {
   "get": {
    "tags": [
     "receipts"
    ],
    "summary": "Settlement receipt service info",
    "operationId": "receiptInfo",
    "responses": {
     "200": {
      "description": "info",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/receipt/verify": {
   "post": {
    "tags": [
     "receipts"
    ],
    "summary": "Verify a settlement receipt (digest + relayer signature + signed terms)",
    "operationId": "receiptVerify",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "receipt": {
          "type": "object"
         }
        },
        "required": [
         "receipt"
        ]
       },
       "example": {
        "receipt": {
         "version": "anygas-receipt/1",
         "chainId": 8453,
         "legs": {},
         "issuedAt": 0,
         "receiptDigest": "0x…",
         "signer": "0x…",
         "signature": "0x…"
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{valid, checks}",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  },
  "/api/receipt/deliver": {
   "post": {
    "tags": [
     "receipts",
     "messaging"
    ],
    "summary": "Deliver a relayer-signed receipt to a counterparty's sealed v2 mailbox",
    "operationId": "receiptDeliver",
    "description": "Seals the receipt (ML-KEM-768+X25519, one-time handle) to the recipient's messaging-v2 mailbox and stores it on the same wire shape as any message, so the counterparty gets verifiable proof without contacting Robyn. Only receipts signed by this relayer are accepted. Content-hash idempotent (replay is a no-op); reachable over OHTTP.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "to": {
          "type": "string",
          "description": "address or Robyn name with a v2 mailbox"
         },
         "receipt": {
          "type": "object"
         },
         "pass": {
          "type": "object",
          "description": "optional Plus pass"
         }
        },
        "required": [
         "to",
         "receipt"
        ]
       },
       "example": {
        "to": "sphynx.robynchain.eth",
        "receipt": {
         "version": "anygas-receipt/1"
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{delivered, id, releaseAt, receiptDigest}",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "receipt does not verify / missing field"
     },
     "403": {
      "description": "not signed by this relayer"
     },
     "404": {
      "description": "recipient has no v2 mailbox"
     }
    }
   }
  },
  "/api/route/aggregators": {
   "get": {
    "summary": "Per-aggregator breaker state + rolling-hour live budget (read-only, no secrets)",
    "tags": [
     "routing"
    ],
    "responses": {
     "200": {
      "description": "health"
     }
    }
   }
  }
 }
}