Base URL and envelope
The API answers at https://api-production-b0c5e.up.railway.app. Every response is JSON in one envelope:
{ "data": { }, "meta": { "requestId": "…", "mode": "live", "asOf": "2026-10-16T09:12:04Z", "slot": "…" } }
or, on failure:
{ "error": { "code": "COVERAGE_TOO_LOW", "message": "…", "retryable": false, "details": { } }, "meta": { "requestId": "…", "mode": "live" } }
Amounts are decimal strings in USDC base units (six decimals). Addresses are base58. Timestamps are ISO 8601 in UTC, to the second. Hashes are hex.
The TypeScript client in @bonded/contract (bundled with the SDK) wraps every endpoint below with its types: createBondedClient({ baseUrl: 'https://api-production-b0c5e.up.railway.app' }).
Sign in
Sign-in proves a wallet holds its key, with one signed message and no transaction.
| Request | Body | Returns |
|---|---|---|
POST /v1/auth/challenge | { wallet } | { challengeId, message, expiresAt } |
POST /v1/auth/verify | { challengeId, signatureBase64 } | the Session, and a session cookie |
GET /v1/session | — | the current Session |
DELETE /v1/session | — | { signedOut: true } |
- Ask for a challenge for the wallet. The
messagebegins withBonded off-chain authentication v1, names the site's origin and the wallet, and expires within minutes. - Sign the exact message bytes with the wallet's key (Ed25519) and send the signature in base64.
- The API sets an HttpOnly
bonded_sessioncookie and returns the session:{ id, wallet, identities, expiresAt, csrfToken }.
identities lists the roles this wallet holds, each { role, subjectId, wallet }: admin of an operator, merchant, session_key of a vault, guardian, arbiter of an escrow.
Every request that changes something (POST, DELETE) carries the session's csrfToken in an X-CSRF-Token header. Clients that aren't browsers keep the cookie in their own cookie jar; the SDK does this for you.
Reads
GET /healthz, GET /v1/protocol and GET /v1/operators/{id}/public are public. Every other read needs a session, and returns only what that session's roles may see.
| Request | Returns |
|---|---|
GET /v1/protocol | the deployment: program id and cluster genesis hash (voucherDomain), USDC mint, protocol parameters and configured fees |
GET /v1/operators/{id} | the operator overview: operator, vaults, bond, lines, recent activity |
GET /v1/operators/{id}/public | what merchants read before extending credit: coverage, tabs settled, defaults |
GET /v1/vaults/{id} | a vault with its merchant rules, approvals and payments |
GET /v1/bonds/{id} | a bond with its lines and default events |
GET /v1/lines?operatorId=&vaultId=&merchantId=&cursor=&limit= | credit lines, paginated; an agent's session key lists its own vault's lines with vaultId |
GET /v1/lines/{id} | one line with its operator's public profile and its merchant |
GET /v1/merchants/{id} | the merchant overview: lines to it, operators' coverage, open claims, escrow jobs, settlements |
GET /v1/default-events/{id} | a default event with its claims |
GET /v1/escrows/{id} | an escrow job with the actions each role may take now |
GET /v1/activity?operatorId=&vaultId=&lineId=&merchantId=&cursor=&limit= | the activity feed, newest first |
Paginated responses are { items, nextCursor }; pass nextCursor back as cursor for the next page.
meta.asOf and meta.slot say how fresh a read is: reads come from the indexer's projection of the chain, which follows each confirmed transaction within seconds.
Actions: prepare, sign, submit
Every change on chain goes through three calls. The API never holds a user's key.
POST /v1/transactions/prepare { action, signerWallet, idempotencyKey }
→ { preparedId, transactionBase64, messageHash, blockhash, lastValidBlockHeight,
requiredSignerRoles, accounts, simulation: { allowed, summary, error }, expiresAt }
(sign transactionBase64 in the wallet)
POST /v1/transactions/submit { preparedId, signedTransactionBase64, idempotencyKey }
→ { id, preparedId, state, signature, error, updatedAt }
GET /v1/transactions/{id} → the same status, until state is confirmed, failed or expired
- Prepare builds the transaction for the action, with the signer as fee payer and a compute budget at the front (a limit sized from the simulation and a priority fee, 1,000 micro-lamports per unit by default), and simulates it. When
simulation.allowedis false,simulation.errorcarries the program's refusal (for exampleCOVERAGE_TOO_LOW); don't sign it. - Sign the exact bytes. A wallet may add ComputeBudget instructions (a priority fee) and Lighthouse guard instructions after the prepared ones; the API accepts those and refuses any other change.
- Submit verifies the signed transaction against the preparation and sends it.
statemoves fromsubmittedtoconfirmed(orfailed, with the program's error). A preparation expires with its blockhash, about a minute after prepare. Its key then answers409 DEADLINE_PASSEDwithdetails.prepareAgain: true: prepare again under a new key. - Idempotency.
idempotencyKeyis yours, up to 128 characters. While a preparation is valid, the same key returns it; use a distinct key for the submit (the app uses<key>:submit). Retrying either call with its key never sends an action twice.
The action is one object with a kind. The ones an operator, merchant or agent sends:
| Kind | Who signs | Fields |
|---|---|---|
create_operator | the admin wallet | guardian (address or null) |
create_vault | admin | operatorId, agentId, policy |
set_policy | admin | vaultId, policy |
fund_vault | admin | vaultId, amount |
set_merchant_rule | admin | vaultId, merchantId, perTransactionCap, windowBudget, quoteRequired |
rotate_session_key | admin | vaultId, sessionKey |
withdraw | admin | vaultId, amount, destinationTokenAccount |
freeze | admin or guardian | vaultId |
unfreeze | admin | vaultId |
post_bond | admin | operatorId, amount, sourceTokenAccount |
open_line | admin | bondId, vaultId, merchantId, limit, expiresAt |
resize_line | admin | lineId, limit |
renew_line | admin | lineId, expiresAt |
request_withdrawal | admin | bondId, amount |
finalize_withdrawal | admin | bondId, destinationTokenAccount |
refill_float | anyone | vaultId |
pay | session key | vaultId, merchantId, amount, intentHash, optional nonce, approvalId, quote |
pay_escrow | session key | vaultId, merchantId, quote, optional approvalId |
accept, dispute | session key or admin | escrowId |
settle_tab | merchant | lineId, voucher |
file_claim | merchant | defaultEventId, lineId, voucher |
mark_delivered | merchant | escrowId, deliverableHash |
release_after_window, refund_on_timeout, execute_default_payout | anyone, once due | escrowId / defaultEventId |
A policy is { sessionKey, floatCap, windowSeconds, windowBudget, cosigner, cosignThreshold, guardian }. The full list of kinds, protocol administration included, is in the whitepaper's instruction reference.
Live events
GET /v1/events?topics=operator:{id},merchant:{id} is a server-sent event stream for a signed-in session. Topics: protocol, operator:{id}, vault:{id}, line:{id}, merchant:{id}, bond:{id}, default:{id}, escrow:{id}; a session subscribes only to topics its roles may read.
Each event's data is { id, type, topics, at, activity?, resources, cursor }:
activity: something happened (a payment, a settlement, a claim);activitycarries it.invalidate: the listedresourceschanged; read them again.reset: the stream can't replay what you missed; read everything again.
Reconnect with after=<cursor> (or the Last-Event-ID header browsers send) to resume where you left off.
Errors
| Code | Means |
|---|---|
INVALID_INPUT | A field is missing or malformed; details names it. |
UNAUTHORIZED | No session, or it expired. Sign in again. |
FORBIDDEN | The session's roles don't allow this read or action. |
NOT_FOUND | No such account. |
CONFLICT, IDEMPOTENCY_CONFLICT | The state changed under you, or a key was reused for different content. |
RATE_LIMITED | Too many requests; wait for Retry-After. |
INDEXER_BEHIND | The read model hasn't caught up with the chain yet; retry shortly. |
CONFIGURATION_REQUIRED | The deployment isn't initialized for this action yet. |
SIMULATION_ONLY | The API is answering from fixtures; its transactions can't be signed or submitted. |
INTERNAL_ERROR | Something failed on our side; retryable says whether to retry. |
VAULT_FROZEN | The vault is frozen. |
WINDOW_BUDGET_EXCEEDED, PER_TRANSACTION_CAP_EXCEEDED | The payment is over the vault's window budget or the merchant's cap. |
APPROVAL_REQUIRED, APPROVAL_EXPIRED, APPROVAL_USED | The payment needs a co-signer's approval, or the approval can't be used. |
QUOTE_REQUIRED | This merchant takes only quoted payments. |
VOUCHER_EXPIRED, VOUCHER_REPLAY | A voucher is past its expiry, or not newer than the last one settled. |
INSUFFICIENT_FUNDS | The paying account holds too little. |
COVERAGE_TOO_LOW | The bond wouldn't cover its lines at the operator's minimum coverage. |
WITHDRAWAL_COOLING_DOWN | The bond withdrawal hasn't unlocked yet. |
DEADLINE_NOT_REACHED, DEADLINE_PASSED, INVALID_TRANSITION | An escrow or default step is early, late, or not allowed from the job's current state. |
retryable: true means the request may succeed later. It succeeds unchanged, except when details.prepareAgain is set: then prepare the action again under a new key.