Back to top

Final DeFi API

Φ₀ · prod

Every route the Final DeFi backend answers over HTTPS, with the shapes it accepts and returns.

host
api.final-de.fi
revision
68a03363ea27d5c5d75d460a5f0c38d3bf4a684a
doors
this API, the Final Network Protocol tunnel, the tiered public RPC

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/json unless a route says otherwise.

  • Amounts are decimal strings in base units (wei for a native asset). Never floats.

  • An address is 0x plus 20 bytes. A chainRef is 0x plus 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 answers 400 with details, the schema’s issues.

  • A refusal and an outage never share a status: 4xx is the caller’s to fix, 502 and 503 are 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_CONFIRMED is where an API-submitted intent completes. SETTLEMENT_SUBMITTED, SETTLED and UNLOCKED exist in the vocabulary but are written by the settlement path, never reached from POST /intents. A client polling for SETTLED waits forever.

  • The terminal states are UNLOCKED and FAILED, nothing else. Terminality is a property of the status; a failed intent’s stage ordinal says nothing about how far it got.

  • POST /intents does not execute inline. 202 QUEUED is the success shape; poll GET /intents/{id} for progress.

  • Idempotency is by bundleHash, derived from the signed bundle: the same bundle again answers 200 with the original intent’s current state and duplicate: true. Reconcile on bundleHash, not on the returned id.

  • When the queue is unavailable POST /intents answers 503 with retryable: true; nothing was submitted on-chain and the intent is marked FAILED.

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

Liveness
GET/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

GET https://api.final-de.fi/health
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "status": "ok",
  "timestamp": 1788652800000,
  "service": "FinalBackend",
  "relayer": {
    "status": "healthy",
    "activeBuilders": 3,
    "emulatedBuilders": 0,
    "virginEoaSupported": true
  }
}

Readiness

Whether this instance can do its job
GET/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

GET https://api.final-de.fi/ready
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "status": "ready",
  "homeBlock": "23456789",
  "counterpartBlock": "145678901",
  "redis": "ok",
  "providers": {
    "home": {},
    "counterpart": {}
  },
  "watchers": [],
  "ingestion": "ok"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "status": "not-ready",
  "error": "request timeout"
}

Chain status

Head blocks of both chains
GET/chain-status

A runtime probe rather than part of the contract: the head block of the home and counterpart chains. Bucket oracle.

Example URI

GET https://api.final-de.fi/chain-status
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "homeBlockNumber": "23456789",
  "counterpartBlockNumber": "145678901"
}

Metrics

Prometheus scrape
GET/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

GET https://api.final-de.fi/metrics
Response  200
HideShow
Headers
Content-Type: text/plain
Body
# 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"} 1204
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Unauthorized",
  "reason": "missing x-final-identity"
}

System configuration

The facts a wallet needs to compose a transaction
GET/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

GET https://api.final-de.fi/system/config
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "networks": {
    "home": {
      "chainId": 1,
      "mirrorChainId": 1
    },
    "counterpart": {
      "chainId": 10,
      "mirrorChainId": 10
    }
  },
  "contracts": {
    "finalGateway": "0x6a1f0000000000000000000000000000000000f1",
    "finalWalletFactory": "0x6a1f0000000000000000000000000000000000f2"
  }
}

This page

The API page
GET/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

GET https://api.final-de.fi/docs
Response  200
HideShow
Headers
Content-Type: text/html

Intents

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 admission
POST/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

POST https://api.final-de.fi/preflight
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "success": true,
  "status": "PREFLIGHT_PASSED",
  "preflight": {
    "ok": true,
    "bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071",
    "gatewayIntents": []
  }
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "success": false,
  "status": "PREFLIGHT_FAILED",
  "preflight": {
    "ok": false,
    "reason": "UNSUPPORTED_FEE_TOKEN"
  }
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Invalid RelayerBundlePayload format",
  "details": []
}

Intents

Submit a bundle
POST/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

POST https://api.final-de.fi/intents
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
  }
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
  "status": "QUEUED",
  "preflight": {
    "ok": true,
    "bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
  "status": "GATEWAY_SUBMITTED",
  "preflight": {
    "ok": true,
    "bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
  },
  "duplicate": true
}
Response  402
HideShow
Headers
Content-Type: application/json
Body
{
  "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."
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "FinalPreflight rejected bundle",
  "status": "PREFLIGHT_FAILED",
  "preflight": {
    "ok": false,
    "reason": "UNSUPPORTED_FEE_TOKEN"
  }
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 bundle
POST/v01/intents

The same route as POST /intents under the version prefix: one handler, one bucket, the same answers.

Example URI

POST https://api.final-de.fi/v01/intents
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
  "status": "QUEUED",
  "preflight": {
    "ok": true,
    "bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
  }
}

Intent

Read an intent's state
GET/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

GET https://api.final-de.fi/intents/tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1
URI Parameters
HideShow
id
string (required) Example: tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1

The id a submission answered with, or pq_<leaf> for a posted PQ intent.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "intentId": "tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1",
  "status": "EXECUTION_CONFIRMED",
  "preflight": {
    "ok": true,
    "bundleHash": "0x7d4e0c3a9f0b6b1e5b1c2a3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071"
  },
  "txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Intent not found"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Invalid intent id"
}

Intent, versioned

Read an intent's state
GET/v01/intents/{id}

The same handler as GET /intents/{id} under the version prefix.

Example URI

GET https://api.final-de.fi/v01/intents/tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1
URI Parameters
HideShow
id
string (required) Example: tx_4f0c1d9e2b3a48c7a1e5f6d7c8b9a0e1

As for GET /intents/{id}.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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 sign
GET/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

GET https://api.final-de.fi/intents/eoa-kem/address
URI Parameters
HideShow
address
string (required) 

the externally owned account

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Invalid EOA KEM request",
  "reason": "ADDRESS_INVALID",
  "message": "not an address"
}

EOA KEM derivation

Derive the keypair
POST/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

POST https://api.final-de.fi/intents/eoa-kem
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
  "signature": "0x…"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "address": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
  "purpose": "final-defi-eoa-kem-v1",
  "mlKem1024": {
    "alg": 3,
    "name": "ML-KEM-1024",
    "publicKey": "0x…",
    "secretKey": "0x…"
  },
  "hqc5": null,
  "hqc5Unavailable": "NOT_REGISTERED"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "EOA KEM refused",
  "reason": "SIGNATURE_MISMATCH",
  "message": "the signature does not recover to the address"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 intents
POST/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

POST https://api.final-de.fi/v01/intents/envelopes
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "intentEnvelopes": [
    {
      "leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
      "header": "0x01a4…",
      "ciphertext": "0x8e02…"
    }
  ]
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "status": "POSTED",
  "posted": [
    "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d"
  ],
  "alreadyOpen": []
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Invalid envelope submission",
  "details": []
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Envelope posting failed",
  "message": "FINAL_INTENT_LOG_ADDRESS is not configured; envelopes cannot be posted."
}

Approve

Approve a posted intent
POST/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

POST https://api.final-de.fi/v01/intent/approve
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
  "publicKey": "0x7a11…",
  "signature": "0xc0de…",
  "executorSection": "0x9f00…"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "action": "approve",
  "leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
  "txHash": "0x51ab…"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "NOT_POSTED",
  "message": "no posting for this leaf"
}
Response  422
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "APPROVAL_KEY_MISMATCH",
  "message": "the key does not match the posting's commitment"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INTENT_GATE_UNCONFIGURED",
  "message": "ACCOUNT_INTAKE_URL is not set"
}

Cancel

Cancel a posted intent
POST/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

POST https://api.final-de.fi/v01/intent/cancel
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
  "publicKey": "0x7a11…",
  "signature": "0xc0de…"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "action": "cancel",
  "leaf": "0x4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d",
  "txHash": "0x51ac…"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "NOT_POSTED",
  "message": "no posting for this leaf"
}
Response  422
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "SIGNATURE_REFUSED",
  "message": "the signature does not verify over the cancel digest"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INTENT_GATE_UNCONFIGURED",
  "message": "ACCOUNT_INTAKE_URL is not set"
}

Stream

Control-plane WebSocket
GET/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

GET https://api.final-de.fi/v01/stream
Response  101

Fees

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 fee
GET/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

GET https://api.final-de.fi/v01/fee-tokens
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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 pair
GET/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

GET https://api.final-de.fi/v01/fee-quote?feeToken=0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f&feeAmount=2500000&equivWei=1200000000000000
URI Parameters
HideShow
feeToken
string (required) Example: 0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f

An admitted fee token.

feeAmount
string (optional) Example: 2500000

Token base units; answers the matching equivWei. Exactly one of feeAmount and equivWei.

equivWei
string (optional) Example: 1200000000000000

Wei; answers the matching feeAmount, grossed up by effectiveTransferTaxBps and rounded up.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "feeToken": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
  "symbol": "USDC",
  "decimals": 6,
  "kind": "erc20",
  "priceSource": "tree-4",
  "feeAmount": "2500000",
  "equivWei": "1200000000000000"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNSUPPORTED_FEE_TOKEN",
  "feeToken": "0x00000000000000000000000000000000000000ff"
}

Compose

Compose the fee for a route
POST/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

POST https://api.final-de.fi/fees/compose
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "targetChainId": 10,
  "boundedMaxWei": "900000000000000",
  "legs": [
    {
      "chainId": 10,
      "kind": "execution",
      "gasLimit": "210000",
      "tipPerGas": "1000000",
      "calldataBytes": 420
    }
  ],
  "feeToken": "0x0b2c22d3c5b8b3d0a01b6f0d4c1e9a9f3c2d1e0f",
  "intent": {
    "type": "transfer",
    "notionalWei": "250000000000000000"
  },
  "wallet": "0x2222222222222222222222222222222222222222"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
    }
  }
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_COMPOSE_REQUEST",
  "details": []
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "PHI_PAYMENT_LANE_OWED",
  "message": "paying in PHI debits locked PHI on Final Chain; that lane is not available yet — quote without payInPhi"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "COMPOSE_UNAVAILABLE",
  "detail": "fee oracle unreachable"
}

Slot keys

The co-signers' publics for one slot
GET/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

GET https://api.final-de.fi/slot-keys/480123
URI Parameters
HideShow
slot
number (required) Example: 480123

the slot index, from /slot-keys/current or privacy.slotIndex

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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
  }
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_SLOT",
  "message": "the slot index is a non-negative integer"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 name
GET/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

GET https://api.final-de.fi/slot-keys/current?executeNotBefore=1728450000&targetChainId=11155420
URI Parameters
HideShow
executeNotBefore
number (optional) Example: 1728450000

unix SECONDS; omit for an immediate row

targetChainId
number (optional) Example: 11155420

which chain’s lead opensAtMs uses

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "slot": 480125,
  "window": {
    "startSec": 1728450000,
    "endSec": 1728453600
  },
  "graceSec": 3600,
  "complete": true,
  "members": [],
  "leadMs": {
    "11155111": 90000
  },
  "opensAtMs": 1728449910000,
  "targetChainId": 11155420,
  "leadMsForTarget": 90000,
  "slotSeconds": 3600
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_EXECUTE_NOT_BEFORE",
  "message": "executeNotBefore is a non-negative integer of unix SECONDS"
}

Treasury state

The gateway's float and accumulators
GET/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

GET https://api.final-de.fi/v01/treasury/state
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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": []
    }
  ]
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 action
POST/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

POST https://api.final-de.fi/recovery
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "actionType": "POLICY_ACCEPTANCE",
  "walletAddress": "0x2222222222222222222222222222222222222222",
  "signedPayload": {
    "data": "0x1c3d…",
    "deadline": 1790000000,
    "signature": "0x9b1c…e21b"
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "success": true,
  "txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Invalid Recovery Payload",
  "details": []
}
Response  500
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "Recovery Action Failed",
  "message": "FEE_REQUIRED: sign equivWei 916000000000000"
}

Validator context

What a recovery validator must sign over
POST/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

POST https://api.final-de.fi/recovery/validator-context
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "chainId": 1,
  "step": "queue",
  "wallet": "0x2222222222222222222222222222222222222222",
  "proposedNewOwner": "0x3333333333333333333333333333333333333333",
  "intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
  "validator": "0x4444444444444444444444444444444444444444"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
    }
  ]
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNSUPPORTED_CHAIN",
  "message": "no configured read client for chain 99"
}

JWT attestation

Mint a recovery attestation from a JWT
POST/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

POST https://api.final-de.fi/recovery/attest/jwt
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "chainId": 1,
  "step": "queue",
  "wallet": "0x2222222222222222222222222222222222222222",
  "proposedNewOwner": "0x3333333333333333333333333333333333333333",
  "intentHash": "0x5e5955e3000000000000000000000000000000000000000000000000000000aa",
  "validator": "0x4444444444444444444444444444444444444444",
  "jwt": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.c2ln",
  "walletProof": {
    "curve": "secp256k1",
    "signature": "0x9b1c…e21b"
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
}
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "JWT_INVALID",
  "message": "signature verification failed"
}
Response  429
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "ACCOUNT_RATE_LIMITED",
  "count": 5,
  "limit": 5
}

Attestors

The armed attestor identities
GET/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

GET https://api.final-de.fi/recovery/attestors
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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 genesis
POST/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

POST https://api.final-de.fi/accounts/open
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "identity": {
    "wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "serial": "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
    "factory": "0x6a1f0000000000000000000000000000000000f2",
    "commitments": {
      "liveAccess": "0x1111111111111111111111111111111111111111111111111111111111111111",
      "liveTransaction": "0x2222222222222222222222222222222222222222222222222222222222222222",
      "recoveryAccess": "0x3333333333333333333333333333333333333333333333333333333333333333",
      "recoveryTransaction": "0x4444444444444444444444444444444444444444444444444444444444444444"
    }
  },
  "lane": "pq",
  "holderProof": "0x6d6c…"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "OPENED",
  "wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2",
  "blockNumber": 1204577,
  "openers": [
    "0x5555555555555555555555555555555555555555",
    "0x6666666666666666666666666666666666666666"
  ]
}
Response  422
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "REFUSED",
  "reason": "PREFIX_NOT_MINED",
  "detail": "identity.wallet does not carry the four zero bytes"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 identity
POST/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

POST https://api.final-de.fi/accounts/register
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "account": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "liveTbs": "0x3082…",
  "recoveryTbs": "0x3082…",
  "roles": 0,
  "version": 5,
  "proof": {
    "mlDsaSignature": "0x0f1e…",
    "slhDsaSignature": "0x2d3c…"
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "challenge": {
    "chainId": 48359,
    "registry": "0x6a1f0000000000000000000000000000000000f3",
    "liveCertHash": "0x7777777777777777777777777777777777777777777777777777777777777777",
    "recoveryCertHash": "0x8888888888888888888888888888888888888888888888888888888888888888",
    "admissionNonce": "14",
    "digest": "0x9999999999999999999999999999999999999999999999999999999999999999"
  }
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "ticket": "rt_3c9f1e7a2b4d5e6f"
}
Response  422
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "REFUSED",
  "reason": "ALREADY_REGISTERED",
  "detail": "the account holds a live certificate"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "UNAVAILABLE",
  "reason": "REGISTER_LANE_UNCONFIGURED",
  "detail": "FINAL_CHAIN_IDENTITY_REGISTRY is not set"
}

Registration ticket

Read a registration's progress
GET/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

GET https://api.final-de.fi/accounts/register/rt_3c9f1e7a2b4d5e6f
URI Parameters
HideShow
ticket
string (required) Example: rt_3c9f1e7a2b4d5e6f

The ticket POST /accounts/register answered with.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "status": "ACTIVE",
  "account": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "txHash": "0x2b7f3a8d9c0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2",
  "certHash": "0x7777777777777777777777777777777777777777777777777777777777777777"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNKNOWN_TICKET"
}

Request

Submit an account-state request
POST/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

POST https://api.final-de.fi/accounts/request
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "request": {
    "wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90",
    "action": "initiate-rotation",
    "nonce": 3,
    "expiresAt": "1790000000000",
    "payload": "0x1111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333333"
  },
  "credential": {
    "pqBlob": "0x0f1e…",
    "ownerSignature": "0x"
  },
  "guardianAuths": []
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "applied",
  "leaf": "0xabababababababababababababababababababababababababababababababab"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "refused",
  "reason": "MALFORMED_REQUEST",
  "detail": "request.action",
  "issues": []
}
Response  422
HideShow
Headers
Content-Type: application/json
Body
{
  "outcome": "refused",
  "reason": "NONCE_USED",
  "detail": "nonce 3 is behind the account's 4"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 set
GET/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

GET https://api.final-de.fi/final-chain/chains
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "CHAIN_SET_READ_FAILED",
  "message": "tree 6 read timed out"
}

Assets

The asset registry, as a wallet lists it
GET/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

GET https://api.final-de.fi/final-chain/assets
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
  }
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "ASSET_REGISTRY_UNCONFIGURED",
  "message": "ASSET_ROSTER_UNCONFIGURED: FINAL_ASSET_REGISTRY is not set"
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "ASSET_REGISTRY_READ_FAILED",
  "message": "registry read timed out"
}

Open policy

What a self-serve genesis is signed over
GET/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

GET https://api.final-de.fi/final-chain/open-policy
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "chainIds": [
    1,
    10
  ],
  "epoch": 6,
  "policy": {
    "delayMs": 0,
    "guardians": [],
    "threshold": 0,
    "cancelThreshold": 0
  }
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 trees
GET/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

GET https://api.final-de.fi/final-chain/trees
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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
    }
  ]
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "FINAL_CHAIN_UNCONFIGURED",
  "message": "FINAL_CHAIN_RPC_URL and FINAL_CHAIN_STATE_TREES are required"
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "CHAIN_READ_FAILED",
  "message": "state trees read timed out"
}

Account

One wallet's tree-1 leaf
GET/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

GET https://api.final-de.fi/final-chain/account/0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90
URI Parameters
HideShow
address
string (required) Example: 0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90

The wallet.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "tree": {
    "id": 1,
    "name": "accounts"
  },
  "key": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
  "present": true,
  "leaf": "0x0102…",
  "root": "0xaaaa…",
  "proof": [
    "0x1111…",
    "0x2222…"
  ],
  "address": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_ADDRESS",
  "message": "INVALID_ADDRESS: not-an-address"
}

Wallets of an owner

The genesis owner-to-wallet phonebook
GET/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

GET https://api.final-de.fi/final-chain/wallet-of/0x3333333333333333333333333333333333333333
URI Parameters
HideShow
owner
string (required) Example: 0x3333333333333333333333333333333333333333

The owner address.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "owner": "0x3333333333333333333333333333333333333333",
  "ledger": "0x6a1f0000000000000000000000000000000000f5",
  "wallets": [
    {
      "wallet": "0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90"
    }
  ],
  "capped": false,
  "source": "index"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_ADDRESS",
  "message": "INVALID_ADDRESS: 0x12"
}

Tiers

The particle ladder
GET/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

GET https://api.final-de.fi/final-chain/tiers
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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 tier
GET/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

GET https://api.final-de.fi/final-chain/phi/0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90?chains=1,10&subaccounts=1
URI Parameters
HideShow
address
string (required) Example: 0x00000000a1b2c3d4e5f60718293a4b5c6d7e8f90

The wallet.

chains
string (optional) Example: 1,10

Chain ids to read; the enabled set when omitted.

subaccounts
string (optional) Example: 1

0 skips the wallets opened with this one as owner.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_CHAINS",
  "message": "chains must be decimal chain ids separated by commas"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 proof
GET/final-chain/tree/{tree}/leaf/{key}

The leaf at key in the named tree, the tree root and the Merkle proof.

Example URI

GET https://api.final-de.fi/final-chain/tree/phi/leaf/0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
URI Parameters
HideShow
tree
string (required) Example: phi

A tree id or name: accounts, phi, vasset, oracle, settlement, allowlist, intents, identity.

key
string (required) Example: 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd

The leaf key, 32 bytes.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "tree": {
    "id": 2,
    "name": "phi"
  },
  "key": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
  "present": true,
  "leaf": "0x0304…",
  "root": "0xbbbb…",
  "proof": [
    "0x1111…",
    "0x2222…"
  ]
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNKNOWN_TREE",
  "message": "no such tree: prices",
  "known": [
    {
      "id": 1,
      "name": "accounts"
    },
    {
      "id": 2,
      "name": "phi"
    },
    {
      "id": 4,
      "name": "oracle"
    }
  ]
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "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 mark
GET/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

GET https://api.final-de.fi/oracle/ETH
URI Parameters
HideShow
asset
string (required) Example: ETH

A symbol or token address.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNSUPPORTED_ASSET",
  "asset": "DOGE",
  "supported": [
    "ETH",
    "PHI",
    "USDC"
  ]
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 admissions
GET/presale/summary

Read at one Final Chain block; cached for thirty seconds.

Example URI

GET https://api.final-de.fi/presale/summary
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "phiSold": "1240000.0",
  "phiSoldWei": "1240000000000000000000000",
  "remainingPhi": "3760000.0",
  "remainingPhiWei": "3760000000000000000000000",
  "capPhi": "5000000.0",
  "capPhiWei": "5000000000000000000000000",
  "priceUsdMicros": "100000",
  "block": 1204577,
  "admissions": []
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "SUMMARY_FAILED",
  "message": "Final Chain read timed out"
}

Quote

An indicative price for a purchase
GET/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

GET https://api.final-de.fi/presale/quote?chainRef=0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d&token=native&amount=250000000000000000&targetChainRef=0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a&beneficiary=0x2222222222222222222222222222222222222222
URI Parameters
HideShow
chainRef
string (required) Example: 0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d

The chain the payment is made on.

token
string (required) Example: native

A token address on that chain, or native.

amount
string (required) Example: 250000000000000000

Base units of token.

targetChainRef
string (required) Example: 0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a

The chain the PHI is delivered on.

beneficiary
string (optional) Example: 0x2222222222222222222222222222222222222222

Who receives; prices the fee at their tier. Cold base tier when omitted.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNKNOWN_CHAIN",
  "message": "chainRef is not on the enabled chain set"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "MARK_UNAVAILABLE",
  "message": "no published tree-4 mark for this asset yet — the quote would be a guess"
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "QUOTE_FAILED",
  "message": "Final Chain read timed out"
}

Purchase status

Pending, held, admitted or delivered
GET/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

GET https://api.final-de.fi/presale/status/0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d/17
URI Parameters
HideShow
chainRef
string (required) Example: 0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d

The payment chain.

seq
string (required) Example: 17

The purchase sequence on that chain.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "state": "admitted",
  "status": "ADMITTED",
  "phiAmount": "8531.25",
  "phiAmountWei": "8531250000000000000000",
  "usdMicros": "853125000",
  "targetChainRef": "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
  "moveSeq": "912"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "state": "held",
  "status": "HELD",
  "reason": "COMPLIANCE_NOT_CLEAR",
  "sub": "AML_UNKNOWN",
  "hold": {
    "kyc": "clear",
    "aml": "unknown",
    "since": 8123456,
    "revertAt": 1757894400
  }
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "INVALID_SEQ",
  "message": "seq must be a positive integer"
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "STATUS_FAILED",
  "message": "Final Chain read timed out"
}

Compliance holds

The held purchases, and the revert queue once the sale has ended
GET/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

GET https://api.final-de.fi/presale/holds
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "holds": [
    {
      "sourceChainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
      "seq": "17",
      "identityRef": "0x9c1185a5c5e9fc54612808977ee8f548b2258d31c8d4d5c1f0f2b8a3d3d9d1e2",
      "state": "HELD",
      "kyc": "clear",
      "aml": "unknown",
      "sub": "AML_UNKNOWN",
      "sinceBlock": 8123456,
      "revertAt": 1757894400
    }
  ],
  "reverts": [],
  "scannedBlocks": 5000
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 chain
GET/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

GET https://api.final-de.fi/presale/escrow
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "intake": "0x00000000FEd55110D84e7435d53e528e83953496",
  "chains": [
    {
      "sourceChainRef": "0x1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d",
      "chainId": 11155111,
      "escrow": [
        {
          "token": "0x0000000000000000000000000000000000000000",
          "heldWei": "1500000000000000"
        },
        {
          "token": "0x2222222222222222222222222222222222222222",
          "heldWei": "250000000"
        }
      ]
    },
    {
      "sourceChainRef": "0x2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e",
      "chainId": 84532,
      "escrow": null
    }
  ]
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "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 proof
GET/presale/release/{chainRef}/{seq}

Example URI

GET https://api.final-de.fi/presale/release/0x000000000000000000000000000000000000000000000000000000000000aa36a7/7
URI Parameters
HideShow
chainRef
string (required) Example: 0x000000000000000000000000000000000000000000000000000000000000aa36a7

The hosting chain’s 32-byte chain ref.

seq
number (required) Example: 7

The hold’s sequence number on that chain.

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
  }
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "HOLD_UNKNOWN",
  "detail": "seq 7 was never issued on 0x…"
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "HOLD_SETTLED",
  "state": "released"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "PRESALE_UNCONFIGURED",
  "missing": [
    "PHI_PRESALE_INTAKE_ADDRESS"
  ]
}

Presale RPC relay

A narrow JSON-RPC relay
POST/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

POST https://api.final-de.fi/presale/rpc/1
URI Parameters
HideShow
chainId
number (required) Example: 1

A decimal EIP-155 id with a configured RPC pool.

Request
HideShow
Headers
Content-Type: application/json
Body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_blockNumber",
  "params": []
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "0x165ec15"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "BAD_REQUEST",
  "message": "one JSON-RPC request object (no batches)"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "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 missing
GET/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

GET https://api.final-de.fi/compliance/me
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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
}
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "IDENTITY_REQUIRED",
  "message": "this route answers for the identity of the tunnel handshake — open a session with your certificate"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "COMPLIANCE_UNAVAILABLE",
  "message": "the compliance gate is not configured on this service"
}

Terms acceptance

Record which terms were in force
POST/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

POST https://api.final-de.fi/compliance/terms/accept
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "version": "schedule-1-v2",
  "saleTermsVersion": "sale-terms-v1"
}
Response  201
HideShow
Headers
Content-Type: application/json
Body
{
  "ok": true,
  "acceptanceId": "0f6f3f7e-9a1c-4a1e-9c2f-1c2d3e4f5a6b",
  "acceptedVersion": "schedule-1-v2",
  "saleTermsVersion": "sale-terms-v1",
  "policyVersion": 4
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "ok": false,
  "reason": "TERMS_VERSION_MISMATCH",
  "currentVersion": "schedule-1-v2"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "MALFORMED",
  "message": "body is {version, saleTermsVersion?}"
}

KYC session

Start a hosted verification
POST/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

POST https://api.final-de.fi/compliance/kyc/session
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "country": "PL",
  "language": "EN",
  "email": "optional@example.org"
}
Response  201
HideShow
Headers
Content-Type: application/json
Body
{
  "reference": "fd-8a5e2b1c-3d4f-4e6a-9b0c-1d2e3f4a5b6c",
  "verificationUrl": "https://app.shuftipro.com/verification/…",
  "ttlMinutes": 60
}
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "IDENTITY_REQUIRED",
  "message": "this route answers for the identity of the tunnel handshake — open a session with your certificate"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "PROVIDER_UNCONFIGURED",
  "missing": [
    "COMPLIANCE_PROVIDER_CLIENT_ID_SECRET"
  ]
}
Response  502
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "PROVIDER_ERROR",
  "message": "the provider did not open a session"
}

KYC callback

The provider's door
POST/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

POST https://api.final-de.fi/compliance/kyc/callback
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
    }
  }
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "ok": true,
  "reference": "fd-8a5e2b1c-…",
  "status": "approved"
}
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "SIGNATURE_INVALID"
}

KYC challenge

A challenge to sign
GET/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

GET https://api.final-de.fi/compliance/kyc/challenge?purpose=
URI Parameters
HideShow
purpose
string (required) 

APPROVE or ATTEST

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "salt": "0x…",
  "challenge": "0x…",
  "message": "FINAL_KYC_APPROVE_v01:0x…",
  "expiresAt": 1757265400000
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "IDENTITY_UNKNOWN"
}

KYC approval leaf

Commit an approved identity
POST/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

POST https://api.final-de.fi/compliance/kyc/approve
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "commitment": "0x…",
  "secret": "0x…",
  "challenge": "0x…",
  "signature": "0x…"
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "commitment": "0x…",
  "key": "0x…",
  "status": "pending"
}
Response  401
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "CHALLENGE_INVALID"
}
Response  403
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "NOT_APPROVED"
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "COMMITMENT_MISMATCH",
  "detail": "the opening does not hash to the commitment"
}

KYC attestation

Attest one action
POST/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

POST https://api.final-de.fi/compliance/kyc/attest
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "actionType": 1,
  "nullifier": "0x…",
  "secret": "0x…",
  "challenge": "0x…",
  "signature": "0x…",
  "chainId": 11155111
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "nullifier": "0x…",
  "key": "0x…",
  "issuedAt": 1757265400,
  "expiresAt": 1757266000,
  "status": "pending"
}
Response  403
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "NOT_CLEAR"
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "NULLIFIER_MISMATCH"
}

KYC attestation state

Read one attestation
GET/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

GET https://api.final-de.fi/compliance/kyc/attest/nullifier
URI Parameters
HideShow
nullifier
string (required) 

the attestation’s nullifier

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "ok": true,
  "nullifier": "0x…",
  "key": "0x…",
  "issuedAt": 1757265400,
  "expiresAt": 1757266000,
  "policyVersion": 4,
  "bucket": "eu",
  "status": "written",
  "txHash": "0x…",
  "path": [
    "0x…"
  ]
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "UNKNOWN_NULLIFIER"
}

KYC revocation

Revoke my own credential
POST/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

POST https://api.final-de.fi/compliance/kyc/revoke
Request
HideShow
Headers
Content-Type: application/json
Body
{}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "revoked": 3,
  "reasonCode": 4
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "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 staple
GET/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

GET https://api.final-de.fi/fstaple/certHash?kind=&format=
URI Parameters
HideShow
certHash
string (required) 

keccak256 of the certificate’s TBS bytes, 32 bytes hex

kind
string (optional) 

identity (default, tree 8) or account (tree 1)

format
string (optional) 

json decodes the staple instead of returning the bytes

Response  200
HideShow
Headers
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="0x9c11….fstaple"
Body
<917 bytes + envelope>
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "CERT_UNKNOWN",
  "certHash": "0x9c11…"
}
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "LEAF_ABSENT",
  "certHash": "0x9c11…",
  "account": "0x…"
}
Response  503
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "STAPLE_UNCONFIGURED",
  "missing": [
    "FINAL_CHAIN_IDENTITY_REGISTRY"
  ]
}

Batch token

One publication's batch token
GET/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

GET https://api.final-de.fi/fstaple/batch/kind/identitySeq?certs=&format=
URI Parameters
HideShow
kind
string (required) 

identity or account

identitySeq
number (required) 

the publication’s identitySeq (or accountEpoch for account)

certs
string (required) 

comma-separated certificate hashes, at most 256

format
string (optional) 

json

Response  200
HideShow
Headers
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="17.fstaple-batch"
Body
<envelope + N × 836 bytes>
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "PUBLICATION_UNKNOWN",
  "seq": "16"
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "ROOT_MOVED",
  "anchoredRoot": "0x…",
  "liveRoot": "0x…"
}
Response  400
HideShow
Headers
Content-Type: application/json
Body
{
  "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 request
POST/settlement/cancel-terms

Example URI

POST https://api.final-de.fi/settlement/cancel-terms
Request
HideShow
Headers
Content-Type: application/json
Body
{
  "chainId": 11155111,
  "opId": "0x7d4e…"
}
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "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…"
  }
}
Response  202
HideShow
Headers
Content-Type: application/json
Body
{
  "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"
}
Response  409
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "OP_NOT_OPEN",
  "detail": "the op is already Returned (cancelled or refunded)"
}
Response  410
HideShow
Headers
Content-Type: application/json
Body
{
  "error": "CANCEL_WINDOW_CLOSED",
  "notBefore": "1757500000",
  "deadline": "1757499880",
  "now": "1757499900"
}

Cancel terms, by path

The same answer, as a read
GET/settlement/cancel-terms/{chainId}/{opId}

Example URI

GET https://api.final-de.fi/settlement/cancel-terms/chainId/opId
URI Parameters
HideShow
chainId
number (required) 

the chain the op was locked on

opId
string (required) 

the scheduled op id (AssetLockedScheduled.opId)

Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  "chainId": 11155111,
  "opId": "0x7d4e…",
  "terms": {
    "": "…"
  },
  "leafIndex": 4213,
  "proof": [
    "0x…"
  ]
}