API reference

devnet preview — all data is replay

One server, no API keys. Fourteen read endpoints anyone can call, one paid endpoint that answers 402 Payment Required until you sign, and two devnet write endpoints. Payment authority travels as signatures in the request body — there is no key to steal and no account to close.

Conventions

Base URL today: your local devserver (http://127.0.0.1:8645) or a locally run relay-server; the public endpoint publishes with the public devnet. All responses are JSON. Money and capacity amounts are decimal strings (they exceed float precision); heights and counts are numbers; ids and hashes are lowercase 0x-hex. Lists return {"data": [...], "next_cursor": …} and accept cursor and limit (default 100, max 1000).

The paid endpoint

POST /v1/chat/completions — OpenAI-shaped chat that settles as a one-fill lease. Call it bare and it does not fail; it quotes you:

1 · The 402 challenge

The response is an x402-style challenge. Inside accepts[0]: scheme exact, the settlement network and USDC asset, and maxAmountRequired — which is the escrow ceiling, not the charge. The challenge says so itself, in three fields under extra.dispute_tail: the ceiling is escrowed not charged, the settled amount is metered and may be lower, and settlement is optimistic with a clawback window after the 200. The full native terms sit in extra.freeside: the book and its key, the lease intent (price, units, spend ceiling, fill caps, heights, nonce), the relay's fee attribution, the relay-fee approval terms, the fill authorization, and the exact derivation of the request id.

2 · The signed retry

Sign locally and re-POST the same endpoint with a freeside object added to the body — a body envelope, not a header:

{
  "model": "freeside-chat",
  "messages": [...],
  "max_tokens": 100,
  "temperature": 0.7,        // optional sampling params pass through to the
  "top_p": 0.9,              // provider verbatim; the quote never reads them
  "freeside": {
    "approval_tx":            "0x…",  // optional: sets the relay-fee budget once, reused after
    "intent_tx":              "0x…",  // required: the signed lease intent
    "fill_authorization_tx":  "0x…",  // required: the signed fill authorization
    "acceptance_delegation":  "0x…"   // required on delegated-acceptance books: your signed §17.1
  }                                //   authorization (see the 402's acceptance_delegate terms)
}

The intent must bind the approval's digest before signing — the SDK does this for you; implementers, see the signing vectors in the repository.

3 · The response

A normal chat completion: id is the lease id, created is the block height, and usage is what settlement meters. What you see in usage equals what moved on chain — that equality is asserted by a cross-language test on every commit.

The read API

Open to anyone. No key, ever — prices never gate.

endpointparamsreturns
GET /v1/statusindex head: height, app_hash, ingested_height
GET /v1/modelsthe routed catalog: model slug, book, listing state, latest clearing price, minimum posted ask, per-fill token caps
GET /v1/bookscursor, limitlisted books
GET /v1/books/:hashone book: book_key, index_source_status, listed, listed_at_height
GET /v1/books/:hash/clearing-printsfrom_height, to_height, cursor, limitclearing prints: price, cleared units, match counts, concentration shares, status
GET /v1/books/:hash/benchmarkswindow, cursor, limitbenchmark prints: window bounds, benchmark price, eligible volume and count, method_id
GET /v1/books/:hash/obligationsstatus, cursor, limitposted asks: ask price, capacity committed and remaining, concurrency, per-fill token caps, status
GET /v1/leases/:idthe full lease object — escrow locked and remaining, cumulative usage, fees paid, fills remaining, status
GET /v1/leases/:id/fillscursor, limitfills: commitments, every deadline height, token counts, computed charge, status
GET /v1/accounts/:addrbalances in four buckets: available, locked_escrow, locked_bond, pending_withdrawal
GET /v1/accounts/:addr/leasesrole (user | provider), status, cursor, limitleases where the address is buyer or provider
GET /v1/providerscursor, limitprovider stats list
GET /v1/providers/:idone provider: completed fills, settled volume, fault counters
GET /v1/eventsfrom_height, kind, cursor, limitthe event log — the polling substitute for a stream

Devnet write endpoints

POST /v1/tx submits a hex-encoded signed transaction. POST /v1/faucet drips devnet funds — and it is not anonymous: you prove control of the account you want funded by signing a challenge with its own key. The payout is fixed server-side (callers cannot request an amount) and each address has a cooldown.

POST /v1/faucet
{ "public_key": "0x…32-byte ed25519", "signature": "0x…64-byte" }

The signature covers a 64-byte message you build locally — no round-trip, the challenge is deterministic:

message = DOMAIN_FAUCET ‖ account_address(public_key)

DOMAIN_FAUCET   = "FREESIDE_FAUCET_V1"  ascii, zero-padded to 32 bytes
DOMAIN_ACCOUNT  = "FREESIDE_ACCOUNT_V1" ascii, zero-padded to 32 bytes
account_address = SHA-256( DOMAIN_ACCOUNT ‖ 0x00 ‖ public_key )
                                        ↑ the ed25519 scheme byte of the
                                          canonical public-key encoding

That scheme byte is the detail worth writing down: the address is hashed over the canonical key encoding, which is the scheme tag followed by the raw key — not the bare 32 bytes. The SDK's accountAddress() does this for you; the derivation is spelled out here so nobody has to rediscover it from a failing signature.

Errors

Chat errors are OpenAI-shaped ({"error": {"message", "type", "code"}}); read-API errors are {"error": {"code", "message"}} with not_found or bad_request. The relay's error table is frozen and enforced verbatim by an HTTP conformance probe in CI — the rows below are tested, not aspirational:

httptypecodemeaning
400invalid_request_errorfreeside_bad_requestmalformed JSON or missing fields
400invalid_request_errorfreeside_checktx_0x····a signed transaction failed admission; the suffix is the CheckTx code
404invalid_request_errormodel_not_foundthe model did not route to a listed book
402insufficient_quotano_clearingthe order did not clear this auction; nothing was charged
402insufficient_quotainsufficient_availablethe account cannot escrow the order's ceiling
408timeoutfreeside_refunded_unacceptedno acceptance by deadline; the fill refunded, the provider unpaid
502api_errorfreeside_provider_unavailablepayload transport to the provider failed before activation
503api_errorfreeside_node_unavailablethe node is unreachable
500api_errorfreeside_internala relay fault — never an on-chain event

Note that 402 appears three ways: the payment challenge (a different body — see above) and the two insufficient_quota rows.

Limits, CORS, streaming

Rate limits are per-IP fixed 60-second windows, in three tiers: 120/min across all of /v1, 10/min on the mutating routes, and 60/min on POST /v1/chat/completions — its own tier so a sustained agent loop does not starve at the mutating limit. Over budget answers 429 freeside_rate_limited with a Retry-After header. Operators override each with FREESIDE_RELAY_RATE_GLOBAL, _RATE_MUTATING, _RATE_CHAT.

CORS echoes an exact allow-listed Origin (never *) with Vary: Origin, and answers preflight ahead of the limiter. The allow-list is FREESIDE_RELAY_CORS_ORIGINS (comma-separated); empty echoes nothing, i.e. same-origin only.

Streaming: send "stream": true and chat answers text/event-stream with OpenAI chat.completion.chunk frames terminated by data: [DONE]. Omit the field and the response is the one buffered JSON body, byte for byte as before.

On a stream, a 200 status line is not a settled completion. The status line is committed before the provider runs, so content can reach you and settlement can still fail afterwards. The verdict rides in the final chunk — the one carrying usage — as freeside: {lease_id, settlement: "settled"}. A stream that fails after delivery instead ends with one frame carrying the same error body this page's table specifies, and no terminator. So the rule holds even for a client that never reads the Freeside receipt: you saw [DONE] ⇒ the chain settled it. Treat a stream that stops any other way as unsettled. The SDK's chatStreamed already does: it rejects rather than return content the chain never confirmed.

There is still no subscribe endpoint for chain events — poll GET /v1/events with from_height instead.

Devserver differences

The quickstart's devserver is a strict subset. It serves the core paths (status, books, accounts, leases, provider by id, clearing prints, faucet, tx, chat) but not the full read API (no models catalog, single-book, benchmarks, obligations, lease fills, account-leases, provider list, or events routes), ignores query parameters on lists, and returns the native Freeside challenge JSON on 402 rather than the x402 skin. The production surface on this page is the contract; the devserver is the porch.