Final DeFi API
Φ₀ · prodEvery route the Final DeFi backend answers over HTTPS, with the shapes it accepts and returns.
The api service serves this page itself, from the image every test roll and
prod promotion deploys: what is documented here is what the running service
answers.
Hosts
| Plane | Host | Notes |
|---|---|---|
| prod | https://api.final-de.fi |
|
| test | https://test-api.final-de.fi |
one service, two spellings |
| test | https://api-test.final-de.fi |
the same service |
A browser opening either host’s root gets this page. A client asking for /
without text/html in its Accept header gets the JSON 404 it always did.
/docs is the page for every client.
Three doors
The backend is reachable three ways. This page documents the first.
This API. HTTPS, JSON, no session, rate-limited per client (below).
The Final Network Protocol tunnel. What Final Wallet uses. A WebSocket at
wss://tunnel.final-de.fi/fnp (prod), wss://test-tunnel.final-de.fi/fnp or
wss://tunnel-test.final-de.fi/fnp (test); one binary message is one FNP
record. The handshake is four KEMs to the node’s certificate and ML-DSA-87 over
the transcript. An anonymous session reads; a session that presented a
registered, unrevoked certificate is identified and may also reach the holder
routes. A full handshake is budgeted at 12 per minute per peer address. The
lanes, with their budgets per minute per session:
| Lane | Admits | Budget |
|---|---|---|
api |
GET under /final-chain/, /v01/, /intents/, /chain-status, /oracle/, /accounts/register/; POST /fees/compose, POST /accounts/register |
240 |
api, identified |
POST /v01/intents/envelopes, POST /v01/intent/approve, POST /v01/intent/cancel |
240 |
open |
POST /accounts/open, POST /accounts/request |
5 |
rpc |
JSON-RPC reads on every chain the tunnel serves, each request naming its chainId |
120 |
send |
eth_sendRawTransaction on the rpc lane, for wallet sessions: a wallet broadcasts its own transactions here |
12; JSON-RPC error -32005 when spent |
oracle |
GET /tree/status, /regions, /usage on the oracle service |
60 |
config |
GET /config.json |
60 |
The tiered public RPC. https://chain.final-de.fi (prod),
https://test-chain.final-de.fi or https://chain-test.final-de.fi (test):
JSON-RPC onto Final Chain, reads only. The tier comes from an x-api-key
header; without one a request is anonymous. No key class carries a write. Over
the tier’s rate: JSON-RPC error -32005.
| Tier | Requests per minute | Batch | eth_call gas |
eth_getLogs range and results |
Receipts |
|---|---|---|---|---|---|
| anonymous | 10 | 3 | 10,000,000 | 2,000 blocks, 1,000 rows | no |
| free | 60 | 10 | 30,000,000 | 10,000 blocks, 5,000 rows | yes |
| builder | 600 | 20 | 30,000,000 | 50,000 blocks, 10,000 rows | yes |
| protocol | 3,000 | 50 | 50,000,000 | 100,000 blocks, 10,000 rows | yes |
Conventions
-
Bodies are JSON and every response is
application/jsonunless a route says otherwise. -
Amounts are decimal strings in base units (wei for a native asset). Never floats.
-
An address is
0xplus 20 bytes. AchainRefis0xplus 32 bytes,keccak(DOMAIN, namespace, reference). A chain id is a decimal EIP-155 id. -
Refusals name themselves. Read routes answer
{ "error": "CODE", "message": "…" }. The account lanes answer{ "outcome", "reason", "detail" }. A body that fails its schema answers400withdetails, the schema’s issues. -
A refusal and an outage never share a status:
4xxis the caller’s to fix,502and503are ours.
Rate limits
Per client, per minute, reported on every answer as RateLimit-Limit,
RateLimit-Remaining and RateLimit-Reset. Over the limit the answer is
429 { "error": "Too Many Requests", "bucket", "retryAfterSeconds" } with a
Retry-After header.
| Bucket | Routes | Limit |
|---|---|---|
read |
every route not listed below, including GET /slot-keys/* |
300 |
oracle |
/oracle/*, /v01/fee-quote, /chain-status |
60 |
simulate |
POST /preflight, POST /intents, POST /v01/intents, POST /recovery, POST /fees/compose |
30 |
open |
POST /accounts/open, POST /accounts/request |
6 |
relay |
POST /presale/rpc/* |
120 |
/health and /ready are exempt. Three account lanes also carry a daily bucket
per account, five per day: acct-open, acct-register and
acct-recovery-attest.
Operator routes
GET /metrics and the full view of GET /system/config need the operator
identity: an x-final-identity request signed by a registered identity holding
the registrar role. They are marked operator below. Everything else is
public.
Intent lifecycle
An intent submitted through POST /intents moves through RECEIVED,
PREFLIGHT_PASSED (or PREFLIGHT_FAILED, terminal for that attempt), QUEUED,
ACCEPTED, GATEWAY_SUBMITTED, EXECUTION_SUBMITTED, EXECUTION_CONFIRMED,
or FAILED.
-
EXECUTION_CONFIRMEDis where an API-submitted intent completes.SETTLEMENT_SUBMITTED,SETTLEDandUNLOCKEDexist in the vocabulary but are written by the settlement path, never reached fromPOST /intents. A client polling forSETTLEDwaits forever. -
The terminal states are
UNLOCKEDandFAILED, nothing else. Terminality is a property of the status; a failed intent’s stage ordinal says nothing about how far it got. -
POST /intentsdoes not execute inline.202 QUEUEDis the success shape; pollGET /intents/{id}for progress. -
Idempotency is by
bundleHash, derived from the signed bundle: the same bundle again answers200with the original intent’s current state andduplicate: true. Reconcile onbundleHash, not on the returned id. -
When the queue is unavailable
POST /intentsanswers503withretryable: true; nothing was submitted on-chain and the intent is markedFAILED.
A fee is required on every row
There is no gasless lane and no subsidy. Every row of a bundle pays:
POST /intents refuses a bundle in which any row carries no fee with
402 FEE_REQUIRED, before it claims the bundle hash or writes to the queue. A
user who cannot pay is prefunded by somebody who can, through the permissionless
depositFor(account) on the gateway: a third party paying, never the protocol.
Carrying a fee is the cheapest of four gates every submitted bundle passes, in cost order:
| Gate | Refusal | Receipt on GET /intents/pq_{leaf} |
|---|---|---|
| the row carries a fee and it covers the chain’s admission floor | FEE_REQUIRED, ADMISSION_FLOOR_UNCOVERED |
REJECTED_FEE_REQUIRED |
the row’s equivWei is at least what the fee schedule composes for it |
FEE_BELOW_SCHEDULE |
REJECTED_FEE_BELOW_SCHEDULE |
| the row has no more legs than its chain admits | MAX_LEGS_EXCEEDED |
REJECTED_MAX_LEGS_EXCEEDED |
| the bundle collects at least its envelope plus the gas margin | NET_NEGATIVE_BUNDLE |
REJECTED_NET_NEGATIVE_BUNDLE |
The schedule figure is recomposed from the row itself: the intent type from its
calldata’s selector, the notional from the value it moves, “paying in PHI” from
the fee token’s own symbol. Nothing a caller asserts can buy a discount. Quote
first with POST /fees/compose, sign the result, then submit.
Self-submission
FinalGateway.executeBundle is permissionless. A user holding their own signed
UserIntent[] and BundleFee can call it from any address with gas for the
wrapper; the relayer is a convenience, not an authority. The signature does not
depend on who broadcasts, so a bundle prepared for this API is submittable as-is
on the lane it was signed for. POST /preflight is a read-only eth_call and
stays useful when self-submitting.
Service ¶
The process itself.
Health ¶
LivenessGET/health
Unconditional wherever the process is up. A failing liveness probe kills the
container, so this never reports a condition a restart cannot fix. Exempt from
rate limits. The relayer block is abridged here.
Example URI
200Headers
Content-Type: application/jsonBody
{
"status": "ok",
"timestamp": 1788652800000,
"service": "FinalBackend",
"relayer": {
"status": "healthy",
"activeBuilders": 3,
"emulatedBuilders": 0,
"virginEoaSupported": true
}
}Readiness ¶
Whether this instance can do its jobGET/ready
Home and counterpart chain reachability decide. Redis, the per-provider health
and the event watchers are reported for monitoring, not gating: an ingestion
stall is an alert, and shedding the instance would not restore the RPC it cannot
reach. providers carries each chain’s provider health report; watchers the
event watchers with their consecutive failures. Exempt from rate limits.
Example URI
200Headers
Content-Type: application/jsonBody
{
"status": "ready",
"homeBlock": "23456789",
"counterpartBlock": "145678901",
"redis": "ok",
"providers": {
"home": {},
"counterpart": {}
},
"watchers": [],
"ingestion": "ok"
}503Headers
Content-Type: application/jsonBody
{
"status": "not-ready",
"error": "request timeout"
}Chain status ¶
Head blocks of both chainsGET/chain-status
A runtime probe rather than part of the contract: the head block of the home and
counterpart chains. Bucket oracle.
Example URI
200Headers
Content-Type: application/jsonBody
{
"homeBlockNumber": "23456789",
"counterpartBlockNumber": "145678901"
}Metrics ¶
Prometheus scrapeGET/metrics
operator. Route inventory, per-route latency and volume, process internals. World-readable it was a reconnaissance and load-oracle feed, so it sits behind the operator identity.
Example URI
200Headers
Content-Type: text/plainBody
# HELP http_request_duration_seconds request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",method="GET",route="/health",status_code="200"} 1204401Headers
Content-Type: application/jsonBody
{
"error": "Unauthorized",
"reason": "missing x-final-identity"
}System configuration ¶
The facts a wallet needs to compose a transactionGET/system/config
Two views of one route. Without the operator identity the answer is the chain
ids and the deployed addresses: the public on-chain facts. With it, the full
topology (RPC URLs, the builder set, relayer health), which stays gated because
the builder list is a targeting list for a denial of service aimed at bundle
inclusion and every CHAIN_<id>_RPC_URL carries a provider key.
Example URI
200Headers
Content-Type: application/jsonBody
{
"networks": {
"home": {
"chainId": 1,
"mirrorChainId": 1
},
"counterpart": {
"chainId": 10,
"mirrorChainId": 10
}
},
"contracts": {
"finalGateway": "0x6a1f0000000000000000000000000000000000f1",
"finalWalletFactory": "0x6a1f0000000000000000000000000000000000f2"
}
}This page ¶
The API pageGET/docs
This document, rendered. GET / answers the same page to a client whose
Accept header names text/html and keeps the JSON 404 for everyone else. The
page follows the system colour scheme; /theme/light, /theme/dark and
/theme/auto remember a choice in a cookie and bounce back. Nothing on the page
is fetched from a third party.
Example URI
200Headers
Content-Type: text/htmlIntents ¶
The relayer-owned write surface: a signed bundle in, an intent id out, execution
by the solver worker. The canonical payload is gatewayIntents; legacy raw
intents are not accepted.
A RelayerBundlePayload carries ownerAddress, gatewayIntents[], an optional
bundleFee (method 0 is Payable, 1 is Coinbase; recipient) and the flags
needsWalletDeployment and needsSubAccountDeployment. Each row carries the
walletCalldata and signature, the four fee fields the owner signed
(feeToken, feeAmount, equivWei, feeRecipient), walletCallValue,
deadline, intentFlags, targetChainId, dependsOn and feeSource. A
schedule field is refused with SCHEDULE_MODE_REMOVED (set executeNotBefore
in the sealed header instead) and a row with the PQ flag set is refused with
PLAINTEXT_PQ_INTENT_REFUSED: a PQ row is an envelope or it is nothing.
Preflight ¶
Check a bundle's admissionPOST/preflight
A read-only eth_call through Final Preflight against live chain state, plus
the paymaster fee-token allowlist. Reports whether the bundle would be admitted
and commits the backend to nothing. Bucket simulate.
Example URI
Headers
Content-Type: application/jsonBody
{
"ownerAddress": "0x1111111111111111111111111111111111111111",
"gatewayIntents": [
{
"wallet": "0x2222222222222222222222222222222222222222",
"walletCallValue": "0",
"walletCalldata": "0x3f2a0b7c000000000000000000000000000000000000000000000000000000000000002a",
"deadline": "1790000000",
"feeToken": "0x0000000000000000000000000000000000000000",
"feeAmount": "1200000000000000",
"equivWei": "1200000000000000",
"feeRecipient": "0x0000000000000000000000000000000000000000",
"signature": "0x9b1c…e21b",
"intentFlags": 0,
"targetChainId": "10"
}
],
"bundleFee": {
"method": 1,
"recipient": "0x0000000000000000000000000000000000000000"
}
}200Headers
Content-Type: application/jsonBody
{
"success": true,
"status": "PREFLIGHT_PASSED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071",
"gatewayIntents": []
}
}409Headers
Content-Type: application/jsonBody
{
"success": false,
"status": "PREFLIGHT_FAILED",
"preflight": {
"ok": false,
"reason": "UNSUPPORTED_FEE_TOKEN"
}
}400Headers
Content-Type: application/jsonBody
{
"error": "Invalid RelayerBundlePayload format",
"details": []
}Intents ¶
Submit a bundlePOST/intents
Preflight, then the fee rule, then the queue. 202 QUEUED is the success
shape; the same signed bundle a second time answers 200 with the original
intent. Bucket simulate. The body is a RelayerBundlePayload, exactly as for
POST /preflight.
Example URI
Headers
Content-Type: application/jsonBody
{
"ownerAddress": "0x1111111111111111111111111111111111111111",
"gatewayIntents": [
{
"wallet": "0x2222222222222222222222222222222222222222",
"walletCallValue": "0",
"walletCalldata": "0x3f2a0b7c000000000000000000000000000000000000000000000000000000000000002a",
"deadline": "1790000000",
"feeToken": "0x0000000000000000000000000000000000000000",
"feeAmount": "1200000000000000",
"equivWei": "1200000000000000",
"feeRecipient": "0x0000000000000000000000000000000000000000",
"signature": "0x9b1c…e21b",
"intentFlags": 0,
"targetChainId": "10"
}
],
"bundleFee": {
"method": 1,
"recipient": "0x0000000000000000000000000000000000000000"
}
}202Headers
Content-Type: application/jsonBody
{
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"status": "QUEUED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
}
}200Headers
Content-Type: application/jsonBody
{
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"status": "GATEWAY_SUBMITTED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
},
"duplicate": true
}402Headers
Content-Type: application/jsonBody
{
"error": "Fee required",
"reason": "FEE_REQUIRED",
"status": "PREFLIGHT_FAILED",
"index": 0,
"message": "gatewayIntents[0] carries no fee. Every row pays: quote feeToken/feeAmount/equivWei at or above the admission floor, or fund the payer through the gateway deposit ledger."
}409Headers
Content-Type: application/jsonBody
{
"error": "FinalPreflight rejected bundle",
"status": "PREFLIGHT_FAILED",
"preflight": {
"ok": false,
"reason": "UNSUPPORTED_FEE_TOKEN"
}
}503Headers
Content-Type: application/jsonBody
{
"error": "Intent queue unavailable",
"status": "FAILED",
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"retryable": true,
"message": "The bundle passed preflight but could not be durably queued. Nothing was submitted on-chain. Retry, or self-submit (see docs/api-contract.md — executeBundle is permissionless)."
}Intents, versioned ¶
Submit a bundlePOST/v01/intents
The same route as POST /intents under the version prefix: one handler, one
bucket, the same answers.
Example URI
202Headers
Content-Type: application/jsonBody
{
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"status": "QUEUED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
}
}Intent ¶
Read an intent's stateGET/intents/{id}
The intent’s current lifecycle state. A posted PQ intent that the watch loop has
refused is answered from its receipt (REJECTED_FEE_REQUIRED,
REJECTED_FEE_BELOW_SCHEDULE, REJECTED_MAX_LEGS_EXCEEDED,
REJECTED_NET_NEGATIVE_BUNDLE), or from the chain when the receipt is not in
this process’s store.
Example URI
- id
string(required) Example: tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1The id a submission answered with, or
pq_<leaf>for a posted PQ intent.
200Headers
Content-Type: application/jsonBody
{
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"status": "EXECUTION_CONFIRMED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
},
"txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2"
}404Headers
Content-Type: application/jsonBody
{
"error": "Intent not found"
}400Headers
Content-Type: application/jsonBody
{
"error": "Invalid intent id"
}Intent, versioned ¶
Read an intent's stateGET/v01/intents/{id}
The same handler as GET /intents/{id} under the version prefix.
Example URI
- id
string(required) Example: tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1As for
GET /intents/{id}.
200Headers
Content-Type: application/jsonBody
{
"intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
"status": "QUEUED",
"preflight": {
"ok": true,
"bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
}
}EOA KEM typed data ¶
B7 — the EOA KEM service. An externally owned account has no PQ key of its own,
so the plane derives one two-of-two: from the holder’s EIP-712 signature over
one fixed message and this plane’s master secret (EOA_KEM_MASTER_SECRET).
Nothing is stored — the same signature and the same master derive the same
keypair every time, and neither half alone derives anything.
Typed data to signGET/intents/eoa-kem/{address}
The EIP-712 payload the client signs, and whether this plane holds its half
(configured). No signature, no derivation.
Example URI
- address
string(required)the externally owned account
200Headers
Content-Type: application/jsonBody
{
"domain": {
"name": "Final DeFi EOA KEM",
"version": "1"
},
"types": {
"EoaKem": [
{
"name": "address",
"type": "address"
},
{
"name": "purpose",
"type": "string"
}
]
},
"primaryType": "EoaKem",
"message": {
"address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"purpose": "final-defi-eoa-kem-v1"
},
"configured": true
}400Headers
Content-Type: application/jsonBody
{
"error": "Invalid EOA KEM request",
"reason": "ADDRESS_INVALID",
"message": "not an address"
}EOA KEM derivation ¶
Derive the keypairPOST/intents/eoa-kem
The signature over the typed data above proves control of the address; the
plane’s master is the other half. The secret half rides back to the signer only
— a client that cannot sign for the address gets nothing. ML-KEM-1024 always;
HQC-5 beside it where the image carries a seedable HQC (hqc5 is null and
hqc5Unavailable names why otherwise).
Example URI
Headers
Content-Type: application/jsonBody
{
"address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"signature": "0x…"
}200Headers
Content-Type: application/jsonBody
{
"address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"purpose": "final-defi-eoa-kem-v1",
"mlKem1024": {
"alg": 3,
"name": "ML-KEM-1024",
"publicKey": "0x…",
"secretKey": "0x…"
},
"hqc5": null,
"hqc5Unavailable": "NOT_REGISTERED"
}400Headers
Content-Type: application/jsonBody
{
"error": "EOA KEM refused",
"reason": "SIGNATURE_MISMATCH",
"message": "the signature does not recover to the address"
}503Headers
Content-Type: application/jsonBody
{
"error": "EOA KEM service unconfigured",
"reason": "UNCONFIGURED",
"message": "EOA_KEM_MASTER_SECRET is not mounted on this plane; the two-of-two derivation has no second half"
}Envelopes ¶
Post sealed intentsPOST/v01/intents/envelopes
The PQ lane’s only submission door. The body is ciphertext end to end, so no
preflight is possible here: every check, the fee included, runs at execution
time after the chain has opened the intent’s gate and the watch loop has
decrypted it. An unpaid posting is terminal for the watch and reads back as
REJECTED_FEE_REQUIRED on GET /intents/pq_{leaf}.
Each envelope is leaf (the intent leaf the header seals), header and
ciphertext as hex. A contentKey is refused (CONTENT_KEY_REFUSED): the
executor section is the only key path. At most MAX_GATEWAY_INTENTS envelopes
per call. Through the tunnel this route needs an identified session.
Example URI
Headers
Content-Type: application/jsonBody
{
"intentEnvelopes": [
{
"leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
"header": "0x01a4…",
"ciphertext": "0x8e02…"
}
]
}202Headers
Content-Type: application/jsonBody
{
"status": "POSTED",
"posted": [
"0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d"
],
"alreadyOpen": []
}400Headers
Content-Type: application/jsonBody
{
"error": "Invalid envelope submission",
"details": []
}502Headers
Content-Type: application/jsonBody
{
"error": "Envelope posting failed",
"message": "FINAL_INTENT_LOG_ADDRESS is not configured; envelopes cannot be posted."
}Approve ¶
Approve a posted intentPOST/v01/intent/approve
The holder’s approval of a posted private intent, forwarded verbatim to the
account-intake, which serves the gate: it reads the posting back from
FinalIntentLog, checks the approval key against the posting’s commitment and
the signature over the approval digest, then pays the Final Chain transaction.
The intake’s status and body come back as answered. executorSection is the
executor’s key section and belongs to approve alone. Through the tunnel this
route needs an identified session.
Example URI
Headers
Content-Type: application/jsonBody
{
"leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
"publicKey": "0x7a11…",
"signature": "0xc0de…",
"executorSection": "0x9f00…"
}200Headers
Content-Type: application/jsonBody
{
"action": "approve",
"leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
"txHash": "0x51ab…"
}404Headers
Content-Type: application/jsonBody
{
"error": "NOT_POSTED",
"message": "no posting for this leaf"
}422Headers
Content-Type: application/jsonBody
{
"error": "APPROVAL_KEY_MISMATCH",
"message": "the key does not match the posting's commitment"
}503Headers
Content-Type: application/jsonBody
{
"error": "INTENT_GATE_UNCONFIGURED",
"message": "ACCOUNT_INTAKE_URL is not set"
}Cancel ¶
Cancel a posted intentPOST/v01/intent/cancel
The holder’s cancel, through the same gate as the approval: the same body
without an executorSection, the signature over the cancel digest, the
intake’s answer returned as given. Through the tunnel this route needs an
identified session.
Example URI
Headers
Content-Type: application/jsonBody
{
"leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
"publicKey": "0x7a11…",
"signature": "0xc0de…"
}200Headers
Content-Type: application/jsonBody
{
"action": "cancel",
"leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
"txHash": "0x51ac…"
}404Headers
Content-Type: application/jsonBody
{
"error": "NOT_POSTED",
"message": "no posting for this leaf"
}422Headers
Content-Type: application/jsonBody
{
"error": "SIGNATURE_REFUSED",
"message": "the signature does not verify over the cancel digest"
}503Headers
Content-Type: application/jsonBody
{
"error": "INTENT_GATE_UNCONFIGURED",
"message": "ACCOUNT_INTAKE_URL is not set"
}Stream ¶
Control-plane WebSocketGET/v01/stream
A WebSocket for older extension and control-plane clients. Messages are JSON
{ "type", "payload" } with type one of SUBSCRIBE, UNSUBSCRIBE,
GATEWAY_STATUS, SUBMIT_INTENT and SUBMIT_TX. SUBSCRIBE and
UNSUBSCRIBE name an intentId in the payload and are acknowledged with
SUBSCRIBED and UNSUBSCRIBED; GATEWAY_STATUS answers
GATEWAY_STATUS_RESPONSE. SUBMIT_INTENT and SUBMIT_TX are refused with an
ERROR naming POST /preflight and POST /intents: mutable submission over
the socket has been removed. Anything else is ERROR.
Example URI
101Fees ¶
What a row pays, quoted before it is signed. The user signs the four fee fields inside the intent; the backend never issues a signed quote, so these routes are informational and the gates at admission are the authority.
Fee tokens ¶
The tokens the relayer admits as feeGET/v01/fee-tokens
Per token: transferTaxBps is what the token declares and the right number for
a “this token taxes transfers” warning; effectiveTransferTaxBps is what
pricing applies (declared plus a 25 % safety margin) and the only correct number
to gross up with. A client that grosses up by the declared tax quotes an amount
the admission gate refuses.
Example URI
200Headers
Content-Type: application/jsonBody
{
"tokens": [
{
"address": "0x0000000000000000000000000000000000000000",
"symbol": "ETH",
"decimals": 18,
"kind": "native",
"transferTaxBps": 0,
"effectiveTransferTaxBps": 0
},
{
"address": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
"symbol": "USDC",
"decimals": 6,
"kind": "erc20",
"transferTaxBps": 0,
"effectiveTransferTaxBps": 0
}
]
}Fee quote ¶
The other side of a fee pairGET/v01/fee-quote{?feeToken,feeAmount,equivWei}
Either direction. The equivWei to feeAmount direction already applies the
effective transfer tax and rounds up, so a quote taken here survives the gate
that prices it at zero slippage. Prefer it over computing the gross-up yourself.
Bucket oracle.
Example URI
- feeToken
string(required) Example: 0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0fAn admitted fee token.
- feeAmount
string(optional) Example: 2500000Token base units; answers the matching
equivWei. Exactly one offeeAmountandequivWei.- equivWei
string(optional) Example: 1200000000000000Wei; answers the matching
feeAmount, grossed up byeffectiveTransferTaxBpsand rounded up.
200Headers
Content-Type: application/jsonBody
{
"feeToken": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
"symbol": "USDC",
"decimals": 6,
"kind": "erc20",
"priceSource": "tree-4",
"feeAmount": "2500000",
"equivWei": "1200000000000000"
}400Headers
Content-Type: application/jsonBody
{
"error": "UNSUPPORTED_FEE_TOKEN",
"feeToken": "0x00000000000000000000000000000000000000ff"
}Compose ¶
Compose the fee for a routePOST/fees/compose
The figure a row must bind, composed the way admission recomposes it: gas cost
basis for the legs, the payer’s tier markup on the gas, the per-type service
charge and the target chain’s admission floor, converted to the fee token at the
composition rate. totalWei, equivWei and feeAmount are what a poster
binds; costBasisWei, markupWei, serviceFeeWei and floorWei the
breakdown; admission the gate’s own floor figure. headroom says whether a
remote leg on targetChainId is admitted against the target’s float or held.
legs is optional: without it only headroom is answered. gasLimit is
required on every leg but a creation leg. Bucket simulate.
intent.type is re-derived at admission from the calldata, so the two agree;
payInPhi: true is refused with 409 PHI_PAYMENT_LANE_OWED until the PHI
payment lane exists.
Two answer fields are about TIME. delay.leadMs is the target chain’s schedule
lead (90 000 ms by default) and delay.opensAtMs is executeNotBefore − leadMs
— when the attempt may begin, when a private row’s shares are released, and when
the holder’s cancel window closes; null for an immediate row.
privacy.slotIndex is the slot a private row for this quote would seal to
(slotOf(executeNotBefore), else slotOf(now), on privacy.slotSeconds), and
null where the slot-key registry is unwired — a client must then not offer the
private option rather than guess. Both are answered on visible quotes too, so
the client and the fleet agree on the slot before anything is posted.
Example URI
Headers
Content-Type: application/jsonBody
{
"targetChainId": 10,
"boundedMaxWei": "900000000000000",
"legs": [
{
"chainId": 10,
"kind": "execution",
"gasLimit": "210000",
"tipPerGas": "1000000",
"calldataBytes": 420
}
],
"feeToken": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
"intent": {
"type": "transfer",
"notionalWei": "250000000000000000"
},
"wallet": "0x2222222222222222222222222222222222222222"
}200Headers
Content-Type: application/jsonBody
{
"headroom": {
"held": false,
"reason": null,
"headroomWei": "412000000000000000",
"neededWei": "900000000000000"
},
"quote": {
"totalWei": "1734000000000000",
"equivWei": "1734000000000000",
"feeAmount": "3612000",
"costBasisWei": "630000000000000",
"markupWei": "63000000000000",
"serviceFeeWei": "125000000000000",
"floorWei": "916000000000000",
"postingFeeWei": "0",
"delayFeeWei": "0",
"privacyFeeWei": "0",
"privacy": {
"option": "visible",
"premiumUsdMicros": "0",
"premiumWei": "0",
"laneAvailable": false,
"slotIndex": 480123,
"slotSeconds": 3600
},
"delay": {
"horizonSeconds": 0,
"weeksBeyondFirst": 0,
"usdMicros": "0",
"leadMs": 90000,
"opensAtMs": null
},
"admission": {
"floorWei": "916000000000000",
"floorUsdMicros": "1000000"
}
}
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_COMPOSE_REQUEST",
"details": []
}409Headers
Content-Type: application/jsonBody
{
"error": "PHI_PAYMENT_LANE_OWED",
"message": "paying in PHI debits locked PHI on Final Chain; that lane is not available yet — quote without payInPhi"
}503Headers
Content-Type: application/jsonBody
{
"error": "COMPOSE_UNAVAILABLE",
"detail": "fee oracle unreachable"
}Slot keys ¶
The co-signers' publics for one slotGET/slot-keys/{slot}
What a wallet seals a PRIVATE intent to. members is every identity holding
ROLE_MMR_COSIGNER, ascending by address — the recipient order of a version-2
executor section. publicKey comes from the registry’s SlotKeyPublished
event, keyHash from its storage: the HASH is the authority, so check the bytes
against slotKeyOf / isUsable (and tree 8 branch 3) before sealing, and treat
null as “not published”, never as a substitute. complete decides whether
the option may be offered at all — the section key is split N-of-N, so four
keys of five seal something nobody can open. Public, no identity.
Example URI
- slot
number(required) Example: 480123the slot index, from
/slot-keys/currentorprivacy.slotIndex
200Headers
Content-Type: application/jsonBody
{
"slot": 480123,
"window": {
"startSec": 1728442800,
"endSec": 1728446400
},
"graceSec": 3600,
"complete": true,
"members": [
{
"member": "0x1111111111111111111111111111111111111111",
"algorithm": 1,
"keyHash": "0x9a…",
"publicKey": "0x…",
"usable": true
}
],
"leadMs": {
"11155111": 90000,
"11155420": 90000
}
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_SLOT",
"message": "the slot index is a non-negative integer"
}503Headers
Content-Type: application/jsonBody
{
"error": "SLOT_KEYS_UNCONFIGURED",
"message": "this deployment has no FINAL_CHAIN_SLOT_KEY_REGISTRY; the private option is unavailable"
}Current slot keys ¶
The slot a posting made now would nameGET/slot-keys/current{?executeNotBefore,targetChainId}
slotOf(executeNotBefore) when the row is scheduled, else slotOf(now) — the
same rule the co-signers check a section’s named slot against, so the client and
the fleet cannot disagree about the number. The answer is /slot-keys/{slot}'s
plus opensAtMs (T − LEAD, when the shares are released; null for an
immediate row, which is sealed to the members and released at once).
Example URI
- executeNotBefore
number(optional) Example: 1728450000unix SECONDS; omit for an immediate row
- targetChainId
number(optional) Example: 11155420which chain’s lead
opensAtMsuses
200Headers
Content-Type: application/jsonBody
{
"slot": 480125,
"window": {
"startSec": 1728450000,
"endSec": 1728453600
},
"graceSec": 3600,
"complete": true,
"members": [],
"leadMs": {
"11155111": 90000
},
"opensAtMs": 1728449910000,
"targetChainId": 11155420,
"leadMsForTarget": 90000,
"slotSeconds": 3600
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_EXECUTE_NOT_BEFORE",
"message": "executeNotBefore is a non-negative integer of unix SECONDS"
}Treasury state ¶
The gateway's float and accumulatorsGET/v01/treasury/state
Read-only: the gateway’s native float and the per-token fee accumulator on the
home chain, plus chains[], one entry per configured chain, because the
paymaster fronts gas on every chain a user can land on. Dashboards and
operations tooling consume this to watch paymaster economics.
Example URI
200Headers
Content-Type: application/jsonBody
{
"chainId": 1,
"gatewayAddress": "0x6a1f0000000000000000000000000000000000f1",
"ethFloat": "4210000000000000000",
"accumulators": [
{
"address": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
"symbol": "USDC",
"decimals": 6,
"kind": "erc20",
"accumulated": "1284000000"
}
],
"chains": [
{
"chainId": 1,
"gatewayAddress": "0x6a1f0000000000000000000000000000000000f1",
"ethFloat": "4210000000000000000",
"accumulators": []
},
{
"chainId": 10,
"gatewayAddress": "0x6a1f0000000000000000000000000000000000f1",
"ethFloat": "1730000000000000000",
"accumulators": []
}
]
}503Headers
Content-Type: application/jsonBody
{
"error": "Treasury reader unavailable",
"message": "FINAL_GATEWAY is not configured"
}Recovery ¶
Wallet recovery routed through the gateway like any other intent, and the attestations the recovery validators accept.
Recovery action ¶
Route a recovery actionPOST/recovery
actionType is one of POLICY_ACCEPTANCE, CONTROLLER_ESCAPE and
CHANCE_VAULT_CLAIM; signedPayload is the owner-signed data with its
signature and an optional deadline. A recovery row pays the M9 write floor and
its own leg’s gas, no service fee and no markup; the fee fields are inside the
owner’s EIP-712 digest, so a payload carrying none is refused FEE_REQUIRED
naming the wei figure to sign, and one carrying too little FEE_BELOW_SCHEDULE
naming both figures. Quote first with POST /fees/compose. Bucket simulate.
Example URI
Headers
Content-Type: application/jsonBody
{
"actionType": "POLICY_ACCEPTANCE",
"walletAddress": "0x2222222222222222222222222222222222222222",
"signedPayload": {
"data": "0x1c3d…",
"deadline": 1790000000,
"signature": "0x9b1c…e21b"
}
}200Headers
Content-Type: application/jsonBody
{
"success": true,
"txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2"
}400Headers
Content-Type: application/jsonBody
{
"error": "Invalid Recovery Payload",
"details": []
}500Headers
Content-Type: application/jsonBody
{
"error": "Recovery Action Failed",
"message": "FEE_REQUIRED: sign equivWei 916000000000000"
}Validator context ¶
What a recovery validator must sign overPOST/recovery/validator-context
The wallet’s recovery state on chainId for the given step (queue or
finalize): the recovery nonce, the pinned validators and their threshold,
whether validator is authorised, and one entry per attestor with its nonce and
the passkey challenge to sign.
Example URI
Headers
Content-Type: application/jsonBody
{
"chainId": 1,
"step": "queue",
"wallet": "0x2222222222222222222222222222222222222222",
"proposedNewOwner": "0x3333333333333333333333333333333333333333",
"intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
"validator": "0x4444444444444444444444444444444444444444"
}200Headers
Content-Type: application/jsonBody
{
"step": "queue",
"chainId": 1,
"wallet": "0x2222222222222222222222222222222222222222",
"proposedNewOwner": "0x3333333333333333333333333333333333333333",
"recoveryNonce": "2",
"intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
"recoveryValidators": [
"0x4444444444444444444444444444444444444444"
],
"recoveryValidatorThreshold": 1,
"validatorAuthorized": true,
"entries": [
{
"validator": "0x4444444444444444444444444444444444444444",
"nonce": "7",
"passkeyChallenge": "0x1f2e…",
"passkeyChallengeBase64Url": "Hy5d…"
}
]
}400Headers
Content-Type: application/jsonBody
{
"error": "UNSUPPORTED_CHAIN",
"message": "no configured read client for chain 99"
}JWT attestation ¶
Mint a recovery attestation from a JWTPOST/recovery/attest/jwt
The validator context plus the identity provider’s jwt and, optionally, a
walletProof (curve secp256k1 or ed25519, signature) binding the token
to the wallet. Answers the attestation the wallet submits on-chain. The issuer
set is what GET /recovery/attestors lists. Five per wallet per day
(acct-recovery-attest); a bad token answers 401.
Example URI
Headers
Content-Type: application/jsonBody
{
"chainId": 1,
"step": "queue",
"wallet": "0x2222222222222222222222222222222222222222",
"proposedNewOwner": "0x3333333333333333333333333333333333333333",
"intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
"validator": "0x4444444444444444444444444444444444444444",
"jwt": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.c2ln",
"walletProof": {
"curve": "secp256k1",
"signature": "0x9b1c…e21b"
}
}200Headers
Content-Type: application/jsonBody
{
"step": "queue",
"chainId": 1,
"wallet": "0x2222222222222222222222222222222222222222",
"proposedNewOwner": "0x3333333333333333333333333333333333333333",
"recoveryNonce": "2",
"intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
"validator": "0x4444444444444444444444444444444444444444",
"nonce": "7",
"issuerId": "google",
"issuer": "https://accounts.google.com",
"binding": "wallet-proof",
"boundWallet": "0x2222222222222222222222222222222222222222",
"issuedAt": 1788652800,
"expiresAt": 1788656400,
"attestor": "0x4444444444444444444444444444444444444444",
"proof": "0x1a2b…"
}401Headers
Content-Type: application/jsonBody
{
"error": "JWT_INVALID",
"message": "signature verification failed"
}429Headers
Content-Type: application/jsonBody
{
"error": "ACCOUNT_RATE_LIMITED",
"count": 5,
"limit": 5
}Attestors ¶
The armed attestor identitiesGET/recovery/attestors
What an onboarding flow pins on-chain through registerAttestor. Attestor
addresses, issuers and audiences are public by construction, so the listing is
ungated.
Example URI
200Headers
Content-Type: application/jsonBody
{
"attestors": [
{
"id": "google",
"issuer": "https://accounts.google.com",
"attestor": "0x4444444444444444444444444444444444444444",
"audiences": [
"final-wallet"
]
}
]
}Accounts ¶
The Final Chain account lanes. The api proposes and validates; the funded
account-intake pays the Final Chain transaction; the ledger verifies the
credential. Every answer here is { "outcome", "reason", "detail" }, and a
refusal (422) never shares a status with an outage (503).
Open ¶
Self-serve genesisPOST/accounts/open
Opens the holder’s own account. The browser mints the identity (four signing
keys, both KEM pairs, the mined four-zero-byte address) and signs the exact
genesis it wants opened. identity carries wallet, serial, factory and
the commitments; lane is pq or ecdsa, and the ecdsa lane also names an
owner and carries an ownerProof (EIP-191 over the same digest). The api
verifies the request (derivation against the factory’s initCodeHash, the
mined prefix, the holder proof, the owner proof, both KEM words, not already
open), collects the openers’ approvals and hands the batch to the intake, which
pays. Every field the holder does not choose is the open policy: every enabled
chain, the default delay, no guardians. Bucket open; acct-open per account
per day.
Example URI
Headers
Content-Type: application/jsonBody
{
"identity": {
"wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"serial": "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"factory": "0x6a1f0000000000000000000000000000000000f2",
"commitments": {
"liveAccess": "0x1111111111111111111111111111111111111111111111111111111111111111",
"liveTransaction": "0x2222222222222222222222222222222222222222222222222222222222222222",
"recoveryAccess": "0x3333333333333333333333333333333333333333333333333333333333333333",
"recoveryTransaction": "0x4444444444444444444444444444444444444444444444444444444444444444"
}
},
"lane": "pq",
"holderProof": "0x6d6c…"
}200Headers
Content-Type: application/jsonBody
{
"outcome": "OPENED",
"wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2",
"blockNumber": 1204577,
"openers": [
"0x5555555555555555555555555555555555555555",
"0x6666666666666666666666666666666666666666"
]
}422Headers
Content-Type: application/jsonBody
{
"outcome": "REFUSED",
"reason": "PREFIX_NOT_MINED",
"detail": "identity.wallet does not carry the four zero bytes"
}503Headers
Content-Type: application/jsonBody
{
"outcome": "UNAVAILABLE",
"reason": "CHAIN_SET_UNPUBLISHED",
"detail": "the enabled EVM chain set is not readable from tree 6 — an account can be opened nowhere right now"
}Register ¶
Register a mined identityPOST/accounts/register
Registers an identity on FinalIdentityRegistry in two phases over one body.
Without a proof, { account, liveTbs, recoveryTbs, roles?, version? }
answers 200 with the challenge to sign. The same body plus
proof: { mlDsaSignature, slhDsaSignature } answers 202 with a ticket and
runs the registrar round behind it; repeating the call while a round is running
returns that round’s ticket. First registrations only: a registered account
answers ALREADY_REGISTERED, one presenting new certificates
ROTATION_IS_PAID. Bucket api at the tunnel; acct-register per account per
day.
Example URI
Headers
Content-Type: application/jsonBody
{
"account": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"liveTbs": "0x3082…",
"recoveryTbs": "0x3082…",
"roles": 0,
"version": 5,
"proof": {
"mlDsaSignature": "0x0f1e…",
"slhDsaSignature": "0x2d3c…"
}
}200Headers
Content-Type: application/jsonBody
{
"challenge": {
"chainId": 48359,
"registry": "0x6a1f0000000000000000000000000000000000f3",
"liveCertHash": "0x7777777777777777777777777777777777777777777777777777777777777777",
"recoveryCertHash": "0x8888888888888888888888888888888888888888888888888888888888888888",
"admissionNonce": "14",
"digest": "0x9999999999999999999999999999999999999999999999999999999999999999"
}
}202Headers
Content-Type: application/jsonBody
{
"ticket": "rt_3c9f1e7a2b4d5e6f"
}422Headers
Content-Type: application/jsonBody
{
"outcome": "REFUSED",
"reason": "ALREADY_REGISTERED",
"detail": "the account holds a live certificate"
}503Headers
Content-Type: application/jsonBody
{
"outcome": "UNAVAILABLE",
"reason": "REGISTER_LANE_UNCONFIGURED",
"detail": "FINAL_CHAIN_IDENTITY_REGISTRY is not set"
}Registration ticket ¶
Read a registration's progressGET/accounts/register/{ticket}
PENDING while the round runs, then ACTIVE, REFUSED or UNAVAILABLE with
the round’s detail. Readable for 24 hours after it resolves.
Example URI
- ticket
string(required) Example: rt_3c9f1e7a2b4d5e6fThe ticket
POST /accounts/registeranswered with.
200Headers
Content-Type: application/jsonBody
{
"status": "ACTIVE",
"account": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2",
"certHash": "0x7777777777777777777777777777777777777777777777777777777777777777"
}404Headers
Content-Type: application/jsonBody
{
"error": "UNKNOWN_TICKET"
}Request ¶
Submit an account-state requestPOST/accounts/request
One holder-signed request: freeze, unfreeze, initiate-rotation,
cancel-rotation, finalize-rotation, initiate-guardian-change,
cancel-guardian-change, finalize-guardian-change, transfer-owner,
enable-pq or set-chain-account. request is FinalAccountLedger.Request
as JSON, payload the action’s arguments ABI-encoded; credential and
guardianAuths may be omitted, as may the request wrapper around a bare
request. The api validates the shape and relays the envelope to the intake,
which pays submitRequest; the ledger verifies the credential. Bucket open.
Example URI
Headers
Content-Type: application/jsonBody
{
"request": {
"wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"action": "initiate-rotation",
"nonce": 3,
"expiresAt": "1790000000000",
"payload": "0x1111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333333"
},
"credential": {
"pqBlob": "0x0f1e…",
"ownerSignature": "0x"
},
"guardianAuths": []
}200Headers
Content-Type: application/jsonBody
{
"outcome": "applied",
"leaf": "0xabababababababababababababababababababababababababababababababab"
}400Headers
Content-Type: application/jsonBody
{
"outcome": "refused",
"reason": "MALFORMED_REQUEST",
"detail": "request.action",
"issues": []
}422Headers
Content-Type: application/jsonBody
{
"outcome": "refused",
"reason": "NONCE_USED",
"detail": "nonce 3 is behind the account's 4"
}503Headers
Content-Type: application/jsonBody
{
"outcome": "unavailable",
"reason": "ACCOUNT_REQUEST_UNCONFIGURED",
"detail": "ACCOUNT_INTAKE_URL is not set"
}Final Chain ¶
Reads of the Final Chain state trees: the enabled chain set and the asset registry (tree 6), accounts (tree 1), PHI (tree 2) and any leaf with its proof. Amounts are decimal strings.
Chains ¶
The enabled chain setGET/final-chain/chains
Every enabled EVM row of tree 6, decoded, ascending by chain id, with
byChainId indexing the same rows; nonEvm carries entries this backend cannot
address by numeric id and withoutFinalityRule the enabled chains whose row
names no finality rule. source is registry once the tree has been read. The
rows are abridged here.
Example URI
200Headers
Content-Type: application/jsonBody
{
"chainIds": [
1,
10
],
"chains": [
{
"chainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
"caipNamespace": "eip155",
"chainId": 1,
"evm": true,
"settlementAddress": "0x6a1f0000000000000000000000000000000000f4",
"gatewayAddress": "0x6a1f0000000000000000000000000000000000f1",
"finalityKind": 1,
"finalityParam": 64,
"blockTimeMs": 12000,
"enabled": true,
"observed": false,
"epoch": 6
}
],
"byChainId": {},
"source": "registry",
"nonEvm": [],
"withoutFinalityRule": [],
"epoch": 6
}502Headers
Content-Type: application/jsonBody
{
"error": "CHAIN_SET_READ_FAILED",
"message": "tree 6 read timed out"
}Assets ¶
The asset registry, as a wallet lists itGET/final-chain/assets
Every asset of tree 6 kind 6, enabled or not, and the per-chain rows for the
enabled chain set. originToken is the address on the asset’s origin chain,
the zero address for a native; uses is a subset of settle, morph, fee,
refill and settle-denied. Each per-chain row names where to act on the
asset on THAT chain: token, morph (the morph contract) and vAsset (the
settlement’s clone) as EVM addresses — null when the row’s word is zero (not
deployed there) or is not an address — with the registry’s raw 32-byte word
beside each (tokenWord, morphWord, vAssetWord). paymasters is the fee
lane per chain, null meaning the chain’s gateway. Every address is a tree-6
row: a change is a new registry epoch, never a client release. Cached per
registry epoch for a minute.
Example URI
200Headers
Content-Type: application/jsonBody
{
"epoch": 6,
"source": "registry",
"chainIds": [
1,
10
],
"assets": [
{
"assetId": "0xe7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7",
"symbol": "ETH",
"name": "Ether",
"decimals": 18,
"originChainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
"originChainId": 1,
"originToken": "0x0000000000000000000000000000000000000000",
"native": true,
"uses": [
"settle",
"fee"
],
"enabled": true,
"priceCadenceMs": 60000,
"maxAgeMs": 900000,
"epoch": 6
}
],
"chains": {
"1": [
{
"assetId": "0xe7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7",
"uses": [
"settle",
"fee"
],
"enabled": true,
"maxLeveragePct": 0,
"epoch": 6,
"token": null,
"tokenWord": "0x0000000000000000000000000000000000000000000000000000000000000000",
"morph": "0x9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a",
"morphWord": "0x0000000000000000000000009a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a",
"vAsset": "0x5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c",
"vAssetWord": "0x0000000000000000000000005c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c"
}
],
"10": [
{
"assetId": "0xe7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7",
"uses": [
"settle",
"fee"
],
"enabled": true,
"maxLeveragePct": 0,
"epoch": 6,
"token": null,
"tokenWord": "0x0000000000000000000000000000000000000000000000000000000000000000",
"morph": null,
"morphWord": "0x0000000000000000000000000000000000000000000000000000000000000000",
"vAsset": null,
"vAssetWord": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
]
},
"paymasters": {
"1": null,
"10": "0x7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b"
}
}503Headers
Content-Type: application/jsonBody
{
"error": "ASSET_REGISTRY_UNCONFIGURED",
"message": "ASSET_ROSTER_UNCONFIGURED: FINAL_ASSET_REGISTRY is not set"
}502Headers
Content-Type: application/jsonBody
{
"error": "ASSET_REGISTRY_READ_FAILED",
"message": "registry read timed out"
}Open policy ¶
What a self-serve genesis is signed overGET/final-chain/open-policy
The chain ids an account is created on (every FULL chain of the enabled set)
and the fixed policy every open request carries: delayMs 0 meaning the
ledger’s default delay, no guardians, zero thresholds. 503 when the set is not
readable or publishes no FULL chain: an open request composed then could not be
signed over the real grant.
Example URI
200Headers
Content-Type: application/jsonBody
{
"chainIds": [
1,
10
],
"epoch": 6,
"policy": {
"delayMs": 0,
"guardians": [],
"threshold": 0,
"cancelThreshold": 0
}
}503Headers
Content-Type: application/jsonBody
{
"error": "CHAIN_SET_UNPUBLISHED",
"message": "the enabled EVM chain set is not readable from tree 6 — an open request composed now could not be signed over the real grant",
"source": "constants"
}Trees ¶
The published state treesGET/final-chain/trees
The last publication round and, per tree, the published and live roots, the
slots used and the writer nonce. Tree ids: 1 accounts, 2 phi, 3 vasset,
4 oracle, 5 settlement, 6 allowlist, 7 intents, 8 identity.
Example URI
200Headers
Content-Type: application/jsonBody
{
"round": 3120,
"publishedBlock": 1204570,
"publishedAt": 1788652740,
"trees": [
{
"id": 1,
"name": "accounts",
"publishedRoot": "0xaaaa…",
"liveRoot": "0xaaaa…",
"slotsUsed": 812,
"nonce": 3120
},
{
"id": 2,
"name": "phi",
"publishedRoot": "0xbbbb…",
"liveRoot": "0xbbbb…",
"slotsUsed": 640,
"nonce": 3120
}
]
}503Headers
Content-Type: application/jsonBody
{
"error": "FINAL_CHAIN_UNCONFIGURED",
"message": "FINAL_CHAIN_RPC_URL and FINAL_CHAIN_STATE_TREES are required"
}502Headers
Content-Type: application/jsonBody
{
"error": "CHAIN_READ_FAILED",
"message": "state trees read timed out"
}Account ¶
One wallet's tree-1 leafGET/final-chain/account/{address}
The account leaf with the tree root and the Merkle proof, so a client can verify
it against a published root. present: false with a null leaf is an answer, not
an error.
Example URI
- address
string(required) Example: 0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90The wallet.
200Headers
Content-Type: application/jsonBody
{
"tree": {
"id": 1,
"name": "accounts"
},
"key": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
"present": true,
"leaf": "0x0102…",
"root": "0xaaaa…",
"proof": [
"0x1111…",
"0x2222…"
],
"address": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90"
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_ADDRESS",
"message": "INVALID_ADDRESS: not-an-address"
}Wallets of an owner ¶
The genesis owner-to-wallet phonebookGET/final-chain/wallet-of/{owner}
Every account ever opened for owner, from the ledger’s owner index
(source: index), or from the ledger’s own AccountOpened logs on a ledger
deployed before the index (source: logs, rows then carry pqEnabled and
blockNumber). Genesis-time truth: verify the live leaf before treating a hit
as current ownership. An empty list is an answer.
Example URI
- owner
string(required) Example: 0x3333333333333333333333333333333333333333The owner address.
200Headers
Content-Type: application/jsonBody
{
"owner": "0x3333333333333333333333333333333333333333",
"ledger": "0x6a1f0000000000000000000000000000000000f5",
"wallets": [
{
"wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90"
}
],
"capped": false,
"source": "index"
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_ADDRESS",
"message": "INVALID_ADDRESS: 0x12"
}Tiers ¶
The particle ladderGET/final-chain/tiers
The PHI tiers that discount fees: per tier its index, name, minPhi,
discountPct and the gas markupBps; phiPaymentDiscountPct is the further
discount for paying in PHI; phiListed whether PHI has a listed price; source
is registry once the tree-6 tier ranges have been read, constants before.
Example URI
200Headers
Content-Type: application/jsonBody
{
"tiers": [
{
"index": 0,
"name": "Neutrino",
"minPhi": "0",
"discountPct": "0",
"markupBps": "1000"
},
{
"index": 1,
"name": "Positron",
"minPhi": "1000000000000000000000",
"discountPct": "10",
"markupBps": "800"
},
{
"index": 2,
"name": "Muon",
"minPhi": "10000000000000000000000",
"discountPct": "25",
"markupBps": "600"
},
{
"index": 3,
"name": "Z Boson",
"minPhi": "100000000000000000000000",
"discountPct": "40",
"markupBps": "400"
},
{
"index": 4,
"name": "Top Quark",
"minPhi": "1000000000000000000000000",
"discountPct": "60",
"markupBps": "200"
}
],
"phiPaymentDiscountPct": "20",
"phiListed": false,
"source": "registry"
}PHI ¶
A wallet's PHI rows and its tierGET/final-chain/phi/{address}{?chains,subaccounts}
The tree-2 rows of the wallet on the given chains, plus the rows of every
wallet opened with it as owner. phiTotal counts every unit once (available,
locked and in transit) across all of them and tier places it on the ladder.
Example URI
- address
string(required) Example: 0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90The wallet.
- chains
string(optional) Example: 1,10Chain ids to read; the enabled set when omitted.
- subaccounts
string(optional) Example: 10skips the wallets opened with this one as owner.
200Headers
Content-Type: application/jsonBody
{
"address": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
"present": true,
"chainIds": [
1,
10
],
"rows": [
{
"chainId": 1,
"available": "1500000000000000000000",
"locked": "500000000000000000000",
"inTransit": "0"
}
],
"subaccounts": [],
"phiTotal": "2000000000000000000000",
"tier": {
"index": 1,
"name": "Positron",
"minPhi": "1000000000000000000000",
"discountPct": "10",
"markupBps": "800"
},
"tierSource": "registry"
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_CHAINS",
"message": "chains must be decimal chain ids separated by commas"
}503Headers
Content-Type: application/jsonBody
{
"error": "CHAIN_SET_UNPUBLISHED",
"message": "the enabled EVM chain set is not readable from tree 6 — pass ?chains=… to read a chosen set"
}Leaf ¶
Any leaf, with its proofGET/final-chain/tree/{tree}/leaf/{key}
The leaf at key in the named tree, the tree root and the Merkle proof.
Example URI
- tree
string(required) Example: phiA tree id or name:
accounts,phi,vasset,oracle,settlement,allowlist,intents,identity.- key
string(required) Example: 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdThe leaf key, 32 bytes.
200Headers
Content-Type: application/jsonBody
{
"tree": {
"id": 2,
"name": "phi"
},
"key": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
"present": true,
"leaf": "0x0304…",
"root": "0xbbbb…",
"proof": [
"0x1111…",
"0x2222…"
]
}400Headers
Content-Type: application/jsonBody
{
"error": "UNKNOWN_TREE",
"message": "no such tree: prices",
"known": [
{
"id": 1,
"name": "accounts"
},
{
"id": 2,
"name": "phi"
},
{
"id": 4,
"name": "oracle"
}
]
}502Headers
Content-Type: application/jsonBody
{
"error": "CHAIN_READ_FAILED",
"message": "state trees read timed out"
}Oracle ¶
The published mark, read from tree 4. {asset} is a monitored asset’s symbol or
token address, or a tree-6 symbol; an unrecognisable one answers
400 UNSUPPORTED_ASSET with the supported symbols. Bucket oracle.
Price ¶
The published markGET/oracle/{asset}
The asset’s price from the oracle tree (tree 4): the chain row when the asset
has one, else the USD composite, else the native composite. present: false
means no mark has been published yet.
Example URI
- asset
string(required) Example: ETHA symbol or token address.
200Headers
Content-Type: application/jsonBody
{
"asset": "ETH",
"address": null,
"source": "tree-4",
"key": "usd-composite",
"quote": "USD",
"present": true,
"price": "3412500000",
"usdMicros": "3412500000",
"kind": 1,
"kindName": "TWAP",
"sourceCount": 3,
"transferTaxBps": 0,
"epoch": 6,
"observedAt": 1788652740,
"ageSeconds": 42,
"root": "0xdddd…"
}400Headers
Content-Type: application/jsonBody
{
"error": "UNSUPPORTED_ASSET",
"asset": "DOGE",
"supported": [
"ETH",
"PHI",
"USDC"
]
}503Headers
Content-Type: application/jsonBody
{
"error": "PRICE_SOURCE_UNAVAILABLE",
"detail": "PRICE_READER_UNCONFIGURED"
}Presale ¶
The PHI presale: supply and admissions, an indicative quote, a purchase’s state, and a narrow JSON-RPC relay for the presale page.
Summary ¶
Sold, remaining and the latest admissionsGET/presale/summary
Read at one Final Chain block; cached for thirty seconds.
Example URI
200Headers
Content-Type: application/jsonBody
{
"phiSold": "1240000.0",
"phiSoldWei": "1240000000000000000000000",
"remainingPhi": "3760000.0",
"remainingPhiWei": "3760000000000000000000000",
"capPhi": "5000000.0",
"capPhiWei": "5000000000000000000000000",
"priceUsdMicros": "100000",
"block": 1204577,
"admissions": []
}502Headers
Content-Type: application/jsonBody
{
"error": "SUMMARY_FAILED",
"message": "Final Chain read timed out"
}Quote ¶
An indicative price for a purchaseGET/presale/quote{?chainRef,token,amount,targetChainRef,beneficiary}
The PHI for amount of token at the published mark, and the network fee of
the delivery, paid on top. Indicative: the purchase is priced at Final Chain
admission, and the PHI is not reduced by the fee.
Example URI
- chainRef
string(required) Example: 0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1dThe chain the payment is made on.
- token
string(required) Example: nativeA token address on that chain, or
native.- amount
string(required) Example: 250000000000000000Base units of
token.- targetChainRef
string(required) Example: 0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0aThe chain the PHI is delivered on.
- beneficiary
string(optional) Example: 0x2222222222222222222222222222222222222222Who receives; prices the fee at their tier. Cold base tier when omitted.
200Headers
Content-Type: application/jsonBody
{
"phiAmount": "8531.25",
"phiAmountWei": "8531250000000000000000",
"usdMicros": "853125000",
"markUsdMicros": "3412500000",
"pricedAtBlock": 1204577,
"fee": {
"usdMicros": "1000000",
"amount": "293040293040293",
"legs": {},
"tier": {
"name": "Neutrino",
"index": 0,
"markupBps": "1000",
"discountPct": "0"
},
"warm": false,
"beneficiary": "0x2222222222222222222222222222222222222222",
"targetChainRef": "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"
},
"totalAmount": "250293040293040293",
"minimumPhiWei": "20000000000000000000",
"note": "indicative — the purchase is priced at Final Chain admission; the fee is paid on top, the PHI is not reduced by it"
}400Headers
Content-Type: application/jsonBody
{
"error": "UNKNOWN_CHAIN",
"message": "chainRef is not on the enabled chain set"
}503Headers
Content-Type: application/jsonBody
{
"error": "MARK_UNAVAILABLE",
"message": "no published tree-4 mark for this asset yet — the quote would be a guess"
}502Headers
Content-Type: application/jsonBody
{
"error": "QUOTE_FAILED",
"message": "Final Chain read timed out"
}Purchase status ¶
Pending, held, admitted or deliveredGET/presale/status/{chainRef}/{seq}
status is the uppercase name — PENDING, HELD, ADMITTED, DELIVERED,
REVERT_QUEUED — and state its lower-case twin. PENDING until Final Chain
admits the purchase; ADMITTED with the PHI and the move; DELIVERED once the
spawn has been consumed on the delivery chain.
HELD is the compliance gate: the identity behind the buy is not clear on both
live checks, so the payment is escrowed and not admitted. It is re-checked every
admission round and admitted the moment the identity clears. A hold still
uncleared when the sale ends is returned to the sender — the one scoped
exception to the sale’s no-refund rule, because an uncleared hold was never a
buy. since is the buy’s own block and revertAt the sale deadline.
Example URI
- chainRef
string(required) Example: 0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1dThe payment chain.
- seq
string(required) Example: 17The purchase sequence on that chain.
200Headers
Content-Type: application/jsonBody
{
"state": "admitted",
"status": "ADMITTED",
"phiAmount": "8531.25",
"phiAmountWei": "8531250000000000000000",
"usdMicros": "853125000",
"targetChainRef": "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"moveSeq": "912"
}200Headers
Content-Type: application/jsonBody
{
"state": "held",
"status": "HELD",
"reason": "COMPLIANCE_NOT_CLEAR",
"sub": "AML_UNKNOWN",
"hold": {
"kyc": "clear",
"aml": "unknown",
"since": 8123456,
"revertAt": 1757894400
}
}400Headers
Content-Type: application/jsonBody
{
"error": "INVALID_SEQ",
"message": "seq must be a positive integer"
}502Headers
Content-Type: application/jsonBody
{
"error": "STATUS_FAILED",
"message": "Final Chain read timed out"
}Compliance holds ¶
The held purchases, and the revert queue once the sale has endedGET/presale/holds
Every purchase the compliance gate is holding, per hosting chain, with the two
check states and the reason. Once a chain’s sale deadline has passed its holds
are REVERT_QUEUED and appear in reverts — the list the return-to-sender path
is driven from. Rows carry an identity ref (a hash), never an address.
Example URI
200Headers
Content-Type: application/jsonBody
{
"holds": [
{
"sourceChainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
"seq": "17",
"identityRef": "0x9c1185a5c5e9fc54612808977ee8f548b2258d31c8d4d5c1f0f2b8a3d3d9d1e2",
"state": "HELD",
"kyc": "clear",
"aml": "unknown",
"sub": "AML_UNKNOWN",
"sinceBlock": 8123456,
"revertAt": 1757894400
}
],
"reverts": [],
"scannedBlocks": 5000
}503Headers
Content-Type: application/jsonBody
{
"error": "COMPLIANCE_GATE_OFF",
"message": "COMPLIANCE_ENFORCE is not 1 on this service — there are no holds to report"
}Intake escrow ¶
The unreleased escrow per accepted asset, per hosting chainGET/presale/escrow
The balances a treasury sweep leaves in the intake: sweepAll moves
balanceOf − heldOf(token) per accepted asset, so whatever is still held between
a buy and its release stays where it is. Contract facts read from each hosting
chain — no compliance gate. A chain that could not be read this pass answers
null for its escrow, never zero; the zero address is the native asset.
Example URI
200Headers
Content-Type: application/jsonBody
{
"intake": "0x00000000FEd55110D84e7435d53e528e83953496",
"chains": [
{
"sourceChainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
"chainId": 11155111,
"escrow": [
{
"token": "0x0000000000000000000000000000000000000000",
"heldWei": "1500000000000000"
},
{
"token": "0x2222222222222222222222222222222222222222",
"heldWei": "250000000"
}
]
},
{
"sourceChainRef": "0x2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e",
"chainId": 84532,
"escrow": null
}
]
}503Headers
Content-Type: application/jsonBody
{
"error": "PRESALE_UNCONFIGURED",
"message": "PHI_PRESALE_INTAKE_ADDRESS must be set"
}Release proof ¶
The anchored release leaf of an admitted hold with its inclusion proof at the hosting chain’s mirrored settlement size —
what FinalPhiPresale.releaseHeldProved(seq, leafIndex, proof) takes. The door is permissionless: the settlement keeper
submits it by default; anyone may. /presale/status stays the HELD record.
Read the release proofGET/presale/release/{chainRef}/{seq}
Example URI
- chainRef
string(required) Example: 0x000000000000000000000000000000000000000000000000000000000000aa36a7The hosting chain’s 32-byte chain ref.
- seq
number(required) Example: 7The hold’s sequence number on that chain.
200Headers
Content-Type: application/jsonBody
{
"chainRef": "0x000000000000000000000000000000000000000000000000000000000000aa36a7",
"chainId": 11155111,
"seq": "7",
"intake": "0x00000000000000000000000000000000000001a7",
"hold": {
"payer": "0x…",
"token": "0x…",
"amount": "1500000",
"targetChainRef": "0x…",
"beneficiary": "0x…",
"attestationNullifier": "0x…",
"heldAt": "1757400000"
},
"leafIndex": 4213,
"proof": [
"0x…"
],
"settlementSize": 4300,
"anchoredAt": {
"anchorBlock": 900,
"asOfBlock": 1000,
"asOfTime": "1757498000"
},
"door": {
"function": "releaseHeldProved",
"selector": "0x…",
"calldata": "0x…"
}
}202Headers
Content-Type: application/jsonBody
{
"pending": true,
"reason": "RELEASE_NOT_ANCHORED",
"detail": "no release leaf for this hold in this region yet — the next anchor round carries it once the ledger has admitted the hold"
}404Headers
Content-Type: application/jsonBody
{
"error": "HOLD_UNKNOWN",
"detail": "seq 7 was never issued on 0x…"
}409Headers
Content-Type: application/jsonBody
{
"error": "HOLD_SETTLED",
"state": "released"
}503Headers
Content-Type: application/jsonBody
{
"error": "PRESALE_UNCONFIGURED",
"missing": [
"PHI_PRESALE_INTAKE_ADDRESS"
]
}Presale RPC relay ¶
A narrow JSON-RPC relayPOST/presale/rpc/{chainId}
One JSON-RPC request object, no batches: eth_chainId, eth_blockNumber,
eth_getBalance, eth_feeHistory and eth_call limited to the presale
contract’s read selectors (accepted, acceptedList, saleOpen,
saleDeadline, symbol, decimals, balanceOf) and the testnet faucet’s
reads. Anything else answers a JSON-RPC error. Bucket relay.
Example URI
- chainId
number(required) Example: 1A decimal EIP-155 id with a configured RPC pool.
Headers
Content-Type: application/jsonBody
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_blockNumber",
"params": []
}200Headers
Content-Type: application/jsonBody
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x165ec15"
}400Headers
Content-Type: application/jsonBody
{
"error": "BAD_REQUEST",
"message": "one JSON-RPC request object (no batches)"
}404Headers
Content-Type: application/jsonBody
{
"error": "UNKNOWN_CHAIN",
"message": "no RPC pool for chain 99"
}Compliance ¶
The execution gate’s two client answers. Before the fleet executes anything for
a Final Identity it checks that the KYC credential is inside its validity and
asks the provider for a live AML status; both clear, or the action is refused
COMPLIANCE_NOT_CLEAR with a sub-reason (KYC_EXPIRED, AML_FLAGGED,
AML_UNKNOWN, PROVIDER_ERROR, IDENTITY_UNKNOWN, CAPABILITY_UNAVAILABLE,
CAPABILITY_LIMITED_EXCEEDED).
Both routes answer for the identity of the FNP tunnel handshake — the wallet
reaches the backend only through that lane — and neither takes an address:
an address linked to a verified identity is personal data, and a path parameter
is written into every access log on the way. Where the tunnel is not the only
door to them they are closed, answering 401 IDENTITY_REQUIRED.
My compliance state ¶
What this identity may do, and what is missingGET/compliance/me
The same two live checks the executors enforce, reported rather than enforced —
so a wallet told clear here and refused at execution has hit a change between
the two calls, not two implementations disagreeing. Capabilities come from the
versioned, hashed jurisdiction policy: available, an object of limits, or
unavailable. No name, no country, no bucket name and no screening detail is
returned.
Example URI
200Headers
Content-Type: application/jsonBody
{
"commitment": "0x9c1185a5c5e9fc54612808977ee8f548b2258d31c8d4d5c1f0f2b8a3d3d9d1e2",
"known": true,
"kyc": {
"clear": true,
"expiresAt": 1788134400000
},
"aml": {
"status": "clear",
"checkedAt": 1757260800000
},
"capabilities": {
"presale.buy": "available",
"morph.open": {
"limited": true,
"maxLeverage": 2
},
"phi.crossChainMove": "available",
"vasset.create": "available",
"lock.open": "available"
},
"terms": {
"currentVersion": "schedule-1-v2",
"acceptedVersion": "schedule-1-v2",
"acceptedAt": "2026-09-06T09:14:00.000Z",
"saleTermsVersion": "sale-terms-v1",
"saleTermsAccepted": true,
"reacceptRequired": false
},
"policyVersion": 4
}401Headers
Content-Type: application/jsonBody
{
"error": "IDENTITY_REQUIRED",
"message": "this route answers for the identity of the tunnel handshake — open a session with your certificate"
}503Headers
Content-Type: application/jsonBody
{
"error": "COMPLIANCE_UNAVAILABLE",
"message": "the compliance gate is not configured on this service"
}Terms acceptance ¶
Record which terms were in forcePOST/compliance/terms/accept
The versions are the compliance policy document’s, so an acceptance is always of a version the fleet can name and prove. A version that is not the one in force is refused rather than recorded. What is stored is the version, its hash, the timestamp and the channel — no IP and no user agent.
Example URI
Headers
Content-Type: application/jsonBody
{
"version": "schedule-1-v2",
"saleTermsVersion": "sale-terms-v1"
}201Headers
Content-Type: application/jsonBody
{
"ok": true,
"acceptanceId": "0f6f3f7e-9a1c-4a1e-9c2f-1c2d3e4f5a6b",
"acceptedVersion": "schedule-1-v2",
"saleTermsVersion": "sale-terms-v1",
"policyVersion": 4
}409Headers
Content-Type: application/jsonBody
{
"ok": false,
"reason": "TERMS_VERSION_MISMATCH",
"currentVersion": "schedule-1-v2"
}400Headers
Content-Type: application/jsonBody
{
"error": "MALFORMED",
"message": "body is {version, saleTermsVersion?}"
}KYC session ¶
Start a hosted verificationPOST/compliance/kyc/session
Identity = the tunnel handshake, like /compliance/me. ONE Shufti hosted
verification (document + face + background checks) under a reference the api
draws at random and stores as the record’s providerApplicantRef — the
provider never learns a wallet or a commitment, and the wallet never learns the
provider’s identifiers. The person completes it at verificationUrl; the
provider then posts its signed result to this plane’s callback below.
Example URI
Headers
Content-Type: application/jsonBody
{
"country": "PL",
"language": "EN",
"email": "optional@example.org"
}201Headers
Content-Type: application/jsonBody
{
"reference": "fd-8a5e2b1c-3d4f-4e6a-9b0c-1d2e3f4a5b6c",
"verificationUrl": "https://app.shuftipro.com/verification/…",
"ttlMinutes": 60
}401Headers
Content-Type: application/jsonBody
{
"error": "IDENTITY_REQUIRED",
"message": "this route answers for the identity of the tunnel handshake — open a session with your certificate"
}503Headers
Content-Type: application/jsonBody
{
"error": "PROVIDER_UNCONFIGURED",
"missing": [
"COMPLIANCE_PROVIDER_CLIENT_ID_SECRET"
]
}502Headers
Content-Type: application/jsonBody
{
"error": "PROVIDER_ERROR",
"message": "the provider did not open a session"
}KYC callback ¶
The provider's doorPOST/compliance/kyc/callback
Not for wallets. The Signature header over the raw body is the whole
authorisation — sha256(body ‖ sha256(secret)) — and a body that does not carry
it is refused 401 SIGNATURE_INVALID. verification.accepted writes the
approved record (credential clock = the document’s expiry, capped by the
policy’s credential.validityDays, default 365; jurisdiction bucket from
residence and nationality under the policy version) and runs the first
screening; any other verified event marks the record refused with the
provider’s codes. An unknown reference answers 200 {ok, ignored} so the
provider stops retrying.
Example URI
Headers
Content-Type: application/jsonBody
{
"reference": "fd-8a5e2b1c-…",
"event": "verification.accepted",
"verification_result": {
"document": 1,
"face": 1,
"background_checks": 1
},
"verification_data": {
"document": {
"expiry_date": "2031-04-30",
"country": "PL"
}
}
}200Headers
Content-Type: application/jsonBody
{
"ok": true,
"reference": "fd-8a5e2b1c-…",
"status": "approved"
}401Headers
Content-Type: application/jsonBody
{
"error": "SIGNATURE_INVALID"
}KYC challenge ¶
A challenge to signGET/compliance/kyc/challenge{?purpose}
Tree 9 (compliance) — the privacy layer over the KYC record. The wallet opens a
commitment H(secret ‖ wallet ‖ salt) and later nullifiers
H(secret ‖ actionType ‖ salt); the salt is drawn once per identity and this
route hands it out with a short-lived challenge the wallet signs
(FINAL_KYC_<PURPOSE>_v01:<challenge>) to prove it holds the wallet.
Example URI
- purpose
string(required)APPROVEorATTEST
200Headers
Content-Type: application/jsonBody
{
"salt": "0x…",
"challenge": "0x…",
"message": "FINAL_KYC_APPROVE_v01:0x…",
"expiresAt": 1757265400000
}404Headers
Content-Type: application/jsonBody
{
"error": "IDENTITY_UNKNOWN"
}KYC approval leaf ¶
Commit an approved identityPOST/compliance/kyc/approve
The opening is recomputed from secret, the signer’s wallet and the salt and
must equal commitment; the challenge signature must recover to that wallet;
the identity must be approved and clear. The commitment is stored, the secret
is not. The branch-1 leaf is written by the registrar quorum’s tree-9 lane on
its next round; until then the answer is pending.
Example URI
Headers
Content-Type: application/jsonBody
{
"commitment": "0x…",
"secret": "0x…",
"challenge": "0x…",
"signature": "0x…"
}202Headers
Content-Type: application/jsonBody
{
"commitment": "0x…",
"key": "0x…",
"status": "pending"
}401Headers
Content-Type: application/jsonBody
{
"error": "CHALLENGE_INVALID"
}403Headers
Content-Type: application/jsonBody
{
"error": "NOT_APPROVED"
}409Headers
Content-Type: application/jsonBody
{
"error": "COMMITMENT_MISMATCH",
"detail": "the opening does not hash to the commitment"
}KYC attestation ¶
Attest one actionPOST/compliance/kyc/attest
A short-lived (600 s) branch-4 leaf for one actionType, keyed by the
nullifier H(secret ‖ actionType ‖ salt) and bound to the wallet
(H(nullifier ‖ wallet)). The live gate must clear at this moment — KYC
inside validity, AML clear — or the attestation is refused by name. A contract
that needs “this wallet may do X now” proves this leaf under tree 9’s root and
learns nothing else.
Example URI
Headers
Content-Type: application/jsonBody
{
"actionType": 1,
"nullifier": "0x…",
"secret": "0x…",
"challenge": "0x…",
"signature": "0x…",
"chainId": 11155111
}202Headers
Content-Type: application/jsonBody
{
"nullifier": "0x…",
"key": "0x…",
"issuedAt": 1757265400,
"expiresAt": 1757266000,
"status": "pending"
}403Headers
Content-Type: application/jsonBody
{
"error": "NOT_CLEAR"
}409Headers
Content-Type: application/jsonBody
{
"error": "NULLIFIER_MISMATCH"
}KYC attestation state ¶
Read one attestationGET/compliance/kyc/attest/{nullifier}
The leaf’s state and, once the lane has written it, the inclusion path
(proofFor(9, key)) a contract call can carry.
Example URI
- nullifier
string(required)the attestation’s nullifier
200Headers
Content-Type: application/jsonBody
{
"ok": true,
"nullifier": "0x…",
"key": "0x…",
"issuedAt": 1757265400,
"expiresAt": 1757266000,
"policyVersion": 4,
"bucket": "eu",
"status": "written",
"txHash": "0x…",
"path": [
"0x…"
]
}404Headers
Content-Type: application/jsonBody
{
"error": "UNKNOWN_NULLIFIER"
}KYC revocation ¶
Revoke my own credentialPOST/compliance/kyc/revoke
The holder’s own revocation (reason code 4, HOLDER_REQUEST): the commitment
and every live nullifier become branch-2 leaves on the next round, and every
executor that proves tree 9 refuses them from that root on. The record itself
stays, refused. revoked counts the subjects queued.
Example URI
Headers
Content-Type: application/jsonBody
{}202Headers
Content-Type: application/jsonBody
{
"revoked": 3,
"reasonCode": 4
}404Headers
Content-Type: application/jsonBody
{
"error": "IDENTITY_UNKNOWN"
}Attestation staples ¶
D4 — the chain’s counterpart of an issuer signature. A .fstaple is an inclusion
proof of a certificate’s identity leaf (tree 8) — or its account leaf (tree 1) —
against a root the ACCOUNT roster anchored, plus the roster’s own envelope over
that anchoring, lifted verbatim from the advanceAccountPlane transaction’s
public calldata. A certificate without a staple is a claim; with one it is issued,
and a fresh staple is at the same time an unrevoked-as-of-that-root statement.
Bytes and verification: docs/fstaple-schema.md. Public, unauthenticated: proof
material about a hash the caller already holds, naming no person.
Staple ¶
One certificate's stapleGET/fstaple/{certHash}{?kind,format}
The proof is read live from FinalStateTrees; the envelope is the newest
anchoring across the execution chains WHOSE WANTED HALF IS PRESENT and whose
anchored root equals the live root the proof was read against. When the live
root has moved past every anchoring the staple ships with anchorRef = 0
(“verify the root online”) rather than an envelope that cannot tie to the proof.
Example URI
- certHash
string(required)keccak256of the certificate’s TBS bytes, 32 bytes hex- kind
string(optional)identity(default, tree 8) oraccount(tree 1)- format
string(optional)jsondecodes the staple instead of returning the bytes
200Headers
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="0x9c11….fstaple"Body
<917 bytes + envelope>200Headers
Content-Type: application/jsonBody
{
"kind": "identity",
"staple": {
"kind": 1,
"certHash": "0x9c11…",
"leaf": "0x7d4e…",
"leafIndex": 4242,
"siblings": [
"0x…",
"…24 words…"
],
"root": "0x5b1c…",
"rootVersion": "17",
"envelope": {
"chainId": 11155111,
"authority": "0x000000003a76B820BB600691DFE13ef697221C25",
"factory": "0x…",
"accountRoot": "0x…",
"accountEpoch": "41",
"gateway": "0x…",
"identityRoot": "0x5b1c…",
"identitySeq": "17",
"anchoredAt": "1757400000",
"nonce": "99",
"signers": [
{
"slot": 0,
"keyId": "0x…",
"height": 20,
"root": "0x…",
"signature": "0x…"
}
],
"txHash": "0x…",
"blockNumber": 9120031
}
},
"bytes": "0x46535450…"
}404Headers
Content-Type: application/jsonBody
{
"error": "CERT_UNKNOWN",
"certHash": "0x9c11…"
}404Headers
Content-Type: application/jsonBody
{
"error": "LEAF_ABSENT",
"certHash": "0x9c11…",
"account": "0x…"
}503Headers
Content-Type: application/jsonBody
{
"error": "STAPLE_UNCONFIGURED",
"missing": [
"FINAL_CHAIN_IDENTITY_REGISTRY"
]
}Batch token ¶
One publication's batch tokenGET/fstaple/batch/{kind}/{identitySeq}{?certs,format}
One envelope — the anchoring at exactly that sequence — amortised over one proof
per certificate, all against the root that anchoring published. 409 ROOT_MOVED
when the live tree no longer holds that root: proofs cannot be read against a
superseded root, ask for the current publication.
Example URI
- kind
string(required)identityoraccount- identitySeq
number(required)the publication’s
identitySeq(oraccountEpochforaccount)- certs
string(required)comma-separated certificate hashes, at most 256
- format
string(optional)json
200Headers
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="17.fstaple-batch"Body
<envelope + N × 836 bytes>404Headers
Content-Type: application/jsonBody
{
"error": "PUBLICATION_UNKNOWN",
"seq": "16"
}409Headers
Content-Type: application/jsonBody
{
"error": "ROOT_MOVED",
"anchoredRoot": "0x…",
"liveRoot": "0x…"
}400Headers
Content-Type: application/jsonBody
{
"error": "TOO_MANY",
"detail": "at most 256 certificates per token"
}Settlement ¶
Cancel terms ¶
W4 (ruled 2026-09-09). A scheduled crossing (lockScheduled, notBefore in the
future) can be cancelled by its LOCKER through a door in FinalSettlement,
cancelScheduledProved(op, terms, leafIndex, proof), until notBefore − 120 s
(the lead, one constant everywhere). The fee is the schedule’s USD 0.10 at the
holder’s ladder tier, converted by the settlement co-signers into the op’s asset
units at the tree-4 mark and anchored as a CancelTerms leaf under the
settlement root while the window is open — “anchor + prove”: this route hands
the holder the terms, the leaf index and the inclusion proof against the root
the origin chain holds. The crossing fee taken at lock is not refunded; the
cancel fee is retained by the contract; the refund is amount − fee.
The holder's requestPOST/settlement/cancel-terms
Example URI
Headers
Content-Type: application/jsonBody
{
"chainId": 11155111,
"opId": "0x7d4e…"
}200Headers
Content-Type: application/jsonBody
{
"chainId": 11155111,
"opId": "0x7d4e…",
"op": {
"seq": "12",
"token": "0x…",
"beneficiary": "0x…",
"amount": "1000000000000000000",
"targetChainRef": "0x…",
"notBefore": "1757500000"
},
"terms": {
"opId": "0x7d4e…",
"holder": "0x…",
"feeUnits": "50000000000000",
"notAfter": "1757499880",
"seq": "12"
},
"leafIndex": 4213,
"proof": [
"0x…"
],
"settlementSize": 4290,
"deadline": "1757499880",
"lead": "120",
"anchoredAt": {
"anchorBlock": 9120031,
"asOfBlock": 7345120,
"asOfTime": "1757498000"
},
"door": {
"function": "cancelScheduledProved",
"selector": "0x…"
}
}202Headers
Content-Type: application/jsonBody
{
"pending": true,
"reason": "TERMS_NOT_ANCHORED",
"detail": "no cancel terms for this op in this region yet — the next anchor round issues them while the window is open"
}409Headers
Content-Type: application/jsonBody
{
"error": "OP_NOT_OPEN",
"detail": "the op is already Returned (cancelled or refunded)"
}410Headers
Content-Type: application/jsonBody
{
"error": "CANCEL_WINDOW_CLOSED",
"notBefore": "1757500000",
"deadline": "1757499880",
"now": "1757499900"
}Cancel terms, by path ¶
The same answer, as a readGET/settlement/cancel-terms/{chainId}/{opId}
Example URI
- chainId
number(required)the chain the op was locked on
- opId
string(required)the scheduled op id (
AssetLockedScheduled.opId)
200Headers
Content-Type: application/jsonBody
{
"chainId": 11155111,
"opId": "0x7d4e…",
"terms": {
"…": "…"
},
"leafIndex": 4213,
"proof": [
"0x…"
]
}