Relay API

The relay API lives under /api/v1. All responses are JSON with cache-control: no-store and access-control-allow-origin: *. Errors have the shape { "error": "message" } with a 4xx or 5xx status.

Integers that can exceed 2^53 (token amounts, prices) are decimal strings. bytes32 values and addresses are 0x-prefixed hex. Times are unix seconds.

The API never accepts plaintext order fields. The only write endpoints take an envelope or a cancel secret.

The relay commits each batch to KasumiEpochManager and settles through KasumiSettlement on Robinhood Chain. /config and /state report this as "mode": "live".

  • GET/configmode, chain, contracts, schedule, markets
  • GET/stateopen epoch, recent epochs, prices, balances
  • POST/ordersenvelope in, signed receipt out
  • POST/orders/cancelreveal a cancel secret before the cutoff
  • GET/epochs/:idbatch and, once decrypted, the full result
  • GET/epochs/:id/proof/:commitmentMerkle proof after the cutoff
  • GET/cronadvance closed epochs (cron secret)
  • GET/operator/epochs/:idbatch and cancel secrets (operator token)
All endpoints, under /api/v1. The two write endpoints accept an envelope or a cancel secret, never a plaintext order field.

Endpoints#

GET /api/v1/config#

Static configuration a client needs before it can build an order.

json
{
  "mode": "live",
  "chainId": 4663,
  "verifyingContract": "0x24E687F7e0BE7Bc4c4AFcD65dD7A22Dc87f3f399",
  "epochManager": "0xc18aEB9ED90549B9995754AFf56B7195F85848E7",
  "relay": "0x…",
  "rpcUrl": "https://rpc.mainnet.chain.robinhood.com",
  "explorerUrl": "https://robinhoodchain.blockscout.com",
  "scheme": 1,
  "schedule": {
    "firstEpochId": 1,
    "startTime": 1790812800,
    "epochDuration": 30,
    "revealDelay": 6,
    "settleWindow": 120
  },
  "markets": [
    {
      "symbol": "AAPL",
      "name": "Apple",
      "marketId": "0x…",
      "baseToken": "0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9",
      "quoteToken": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
      "baseDecimals": 18,
      "quoteDecimals": 6,
      "quoteSymbol": "USDG",
      "maxOracleDeviationBps": 300
    }
  ],
  "serverTime": 1790900000
}

chainId and verifyingContract form the EIP-712 domain. relay is the address that signs inclusion receipts. scheme 1 is drand quicknet timelock. rpcUrl and explorerUrl are the JSON-RPC endpoint and block explorer for chainId, for wallets, transaction links and reading the epoch root onchain.

  • verifyingContract is the KasumiSettlement address (the address users approve), epochManager is the KasumiEpochManager address, and schedule is read from the epoch manager. On mainnet that is 30-second epochs, a 15-second reveal delay and a 300-second settlement window.

GET /api/v1/state#

Polling endpoint used by the terminal. Optional query: address=0x… to include that address's balances.

json
{
  "mode": "live",
  "serverTime": 1790900012,
  "storage": "redis",
  "current": { "epochId": 2907, "status": "OPEN", "openTime": 1790900010, "closeTime": 1790900040,
               "decryptTime": 1790900046, "settleDeadline": 1790900166,
               "orderCount": 16, "root": null, "resultHash": null, "markets": [] },
  "recent": [ { "epochId": 2906, "status": "SETTLED", "…": "…" } ],
  "prices": [ { "marketId": "0x…", "price": "332188138", "updatedAt": 1790898700,
                "source": "chainlink", "stale": false } ],
  "account": { "address": "0x…", "balances": { "0x5fc5…d168": "250000000000" } }
}
  • current and each of recent (the previous 8 epochs) are epoch views, described below.
  • prices[].price is the protocol price: quote raw units per base raw unit times 1e18. "0" with source: "unavailable" means no reference price. stale is true when the Chainlink round is older than one hour.
  • account is null unless a valid address was given. Balance keys are lowercase token addresses. They are onchain balances, and the account also has allowances: for each token, the raw allowance the address has granted to the settlement contract. Both are read with one Multicall3 call and cached for a few seconds.
  • storage is "postgres", "redis" or "memory". With "memory" the state lives in one server process, which only works for a single local instance.

Calling this endpoint also triggers background housekeeping: committing closed epochs and settling decrypted ones onchain (see DEPLOYMENT.md, "Operating the live relay").

POST /api/v1/orders#

Submit a sealed order. The body is an envelope and nothing else.

json
{
  "protocolVersion": 1,
  "scheme": 1,
  "epochId": 2907,
  "ciphertext": "0x…",
  "ciphertextHash": "0x…",
  "orderCommitment": "0x…"
}

Response: a signed inclusion receipt.

json
{
  "orderCommitment": "0x…",
  "ciphertextHash": "0x…",
  "epochId": 2907,
  "sequence": 16,
  "receivedAt": 1790900021,
  "cutoff": 1790900040,
  "relay": "0x…",
  "relaySignature": "0x…"
}

Errors:

Status Message Cause
400 unexpected field "…" the envelope has a field that is not allowed
400 unsupported protocolVersion, invalid scheme, invalid epochId, ciphertext must be hex, ciphertext size out of range, ciphertextHash must be bytes32, orderCommitment must be bytes32, ciphertextHash does not match ciphertext malformed envelope. Ciphertext limit is 4096 bytes.
400 unsupported sealing scheme scheme is not 1
400 ciphertext is not a drand quicknet timelock ciphertext the tlock header could not be read
400 ciphertext is not locked to this epoch's decryption round sealed to the wrong drand round
400 request body must be JSON
409 epoch N is not open (current epoch is M) epochId is not the open epoch
409 epoch is closing, resubmit for the next one less than 1 second before the cutoff
409 this commitment was already accepted duplicate commitment in this epoch
413 request body too large body over 32,000 characters
429 rate limit: too many orders this epoch more than 12 orders from one IP in one epoch
429 epoch is full 120 orders already accepted (a market must settle in one transaction)

The request that submits an epoch's first order also keeps its server instance alive until that epoch is committed and settled.

POST /api/v1/orders/cancel#

Private cancellation. Only while the order's epoch is open.

json
{ "epochId": 2907, "orderCommitment": "0x…", "cancelSecret": "0x…" }

Response: { "ok": true }. The relay cannot check the secret until the epoch is decrypted. A wrong secret is accepted here and has no effect later.

Status Message
400 expected { epochId, orderCommitment, cancelSecret }
404 unknown commitment
409 the epoch has closed; use onchain nonce invalidation instead

GET /api/v1/epochs/{id}#

Everything public about one epoch. Returns an epoch view plus entries and result.

json
{
  "epochId": 2906,
  "status": "SETTLED",
  "openTime": 1790899980, "closeTime": 1790900010,
  "decryptTime": 1790900016, "settleDeadline": 1790900136,
  "orderCount": 17,
  "root": "0x…",
  "resultHash": "0x…",
  "markets": [
    { "marketId": "0x…", "symbol": "AAPL", "clearingPrice": "332188138", "oraclePrice": "332188138",
      "matchedBase": "41000000000000000000", "fills": 3, "marketHash": "0x…" }
  ],
  "entries": [
    { "sequence": 0, "receivedAt": 1790899985, "envelope": { "…": "…" } }
  ],
  "result": {
    "epochId": 2906, "root": "0x…", "orderCount": 17, "processedAt": 1790900019,
    "drandRound": 32698885, "resultHash": "0x…",
    "markets": [ "…" ],
    "orders": [
      { "sequence": 0, "commitment": "0x…", "ciphertextHash": "0x…",
        "outcome": "FILLED",
        "order": { "owner": "0x…", "marketId": "0x…", "side": 0, "baseAmount": "…",
                   "limitPrice": "…", "allowPartialFill": true, "minFillBase": "0" },
        "baseFilled": "…", "quoteAmount": "…" }
    ]
  }
}
  • status is one of SCHEDULED, OPEN, EMPTY, CLOSED, COMMITTED, DECRYPTABLE, SETTLED, CANCELLED. EMPTY means the epoch closed with no orders. CLOSED means the cutoff has passed and the commit transaction has not been recorded yet, and CANCELLED means the commit missed its window, the settlement deadline passed, or not every market settled. MATCHED is not reported by the relay; read KasumiEpochManager.status(epochId) for the onchain state.
  • Every epoch view has two optional fields. txs holds the transaction hashes: { "commit": "0x…", "postMatch": "0x…", "settle": { "<marketId>": "0x…" } }, each present once sent. failure is a short reason when the epoch did not settle, for example commit window missed.
  • entries is empty while the epoch is open. After the cutoff it is the committed batch: the envelopes exactly as received, in sequence order.
  • root is null while the epoch is open or if it had no orders.
  • result is null until the epoch has been processed after its decryption time. A cancelled epoch also has a result once its orders are public: it carries settled: false, failure, the decoded orders, and only the markets that did settle. orders[].outcome is FILLED, PARTIAL, UNFILLED or REJECTED. Rejected orders carry reason (codes in PROTOCOL.md §6) and may have no order field if the ciphertext could not be decoded.

Errors: 400 invalid epoch id.

Anyone can check a settled epoch: decrypt each entries[].envelope.ciphertext with the drand round, recompute the leaves and the root, run the matcher, and compare resultHash.

GET /api/v1/cron#

Backstop. Advances every pending epoch: commits those past their cutoff, settles those past their drand round. Vercel Cron calls it once a minute. Response: { "ok": true }. If CRON_SECRET is set the request must carry Authorization: Bearer <CRON_SECRET>, otherwise 401 unauthorized.

GET /api/v1/operator/epochs/{id}#

For a standalone operator process. Returns the batch the relay accepted and the cancel secrets users revealed before the cutoff. Requires Authorization: Bearer <KASUMI_OPERATOR_TOKEN>. Without the token, or if the relay has no token configured, it answers 401 unauthorized.

json
{
  "epochId": 12,
  "entries": [ { "sequence": 0, "receivedAt": 1790948260, "envelope": { "…": "…" } } ],
  "cancellations": { "0x<commitment, lowercase>": "0x<cancel secret>" }
}

Cancel secrets are sensitive until the epoch is decrypted, which is why they are not part of the public epoch endpoint. Unlike the public endpoint, entries is returned while the epoch is still open.

GET /api/v1/epochs/{id}/proof/{commitment}#

Merkle proof that a commitment is in the epoch's batch.

json
{ "epochId": 2906, "root": "0x…", "sequence": 4, "proof": ["0x…", "0x…"] }
Status Message
400 invalid epoch id, invalid commitment
404 commitment is not in this epoch
409 epoch is still open; the root is not fixed yet

SDK#

The TypeScript client and the protocol functions are documented in SDK.md.