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)
Endpoints#
GET /api/v1/config#
Static configuration a client needs before it can build an order.
{
"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.
verifyingContractis theKasumiSettlementaddress (the address users approve),epochManageris theKasumiEpochManageraddress, andscheduleis 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.
{
"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" } }
}
currentand each ofrecent(the previous 8 epochs) are epoch views, described below.prices[].priceis the protocol price: quote raw units per base raw unit times 1e18."0"withsource: "unavailable"means no reference price.staleis true when the Chainlink round is older than one hour.accountisnullunless a validaddresswas given. Balance keys are lowercase token addresses. They are onchain balances, and the account also hasallowances: 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.storageis"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.
{
"protocolVersion": 1,
"scheme": 1,
"epochId": 2907,
"ciphertext": "0x…",
"ciphertextHash": "0x…",
"orderCommitment": "0x…"
}
Response: a signed inclusion receipt.
{
"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.
{ "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.
{
"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": "…" }
]
}
}
statusis one ofSCHEDULED,OPEN,EMPTY,CLOSED,COMMITTED,DECRYPTABLE,SETTLED,CANCELLED.EMPTYmeans the epoch closed with no orders.CLOSEDmeans the cutoff has passed and the commit transaction has not been recorded yet, andCANCELLEDmeans the commit missed its window, the settlement deadline passed, or not every market settled.MATCHEDis not reported by the relay; readKasumiEpochManager.status(epochId)for the onchain state.- Every epoch view has two optional fields.
txsholds the transaction hashes:{ "commit": "0x…", "postMatch": "0x…", "settle": { "<marketId>": "0x…" } }, each present once sent.failureis a short reason when the epoch did not settle, for examplecommit window missed. entriesis empty while the epoch is open. After the cutoff it is the committed batch: the envelopes exactly as received, in sequence order.rootisnullwhile the epoch is open or if it had no orders.resultisnulluntil the epoch has been processed after its decryption time. A cancelled epoch also has a result once its orders are public: it carriessettled: false,failure, the decoded orders, and only the markets that did settle.orders[].outcomeisFILLED,PARTIAL,UNFILLEDorREJECTED. Rejected orders carryreason(codes in PROTOCOL.md §6) and may have noorderfield 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.
{
"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.
{ "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.