Base URL for local preview: http://localhost:4200 · production: https://api.surplus.dev
All money is integer micro-USD — 1 USD = 1,000,000 µUSD.
The canonical contract lives in docs/API-CONTRACT.md ↗.
Every authenticated call carries Authorization: Bearer <key>. Keys look like
spl_live_<id>.<secret> — or sign in with a wallet and use the issued
sess_ token instead. A shared workspace key for trying the API:
spl_live_demo.DEMOSECRET
| Scope | Allows |
|---|---|
| rpc:execute | Route JSON-RPC calls via POST /v1/rpc/:chain. |
| tools:execute | Invoke fallback-catalog tools directly. |
| usage:read | Read balance and receipts. |
curl https://api.surplus.dev/v1/rpc/solana \ -H "Authorization: Bearer spl_live_demo.DEMOSECRET" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: docs-try-001" \ -d '{"jsonrpc":"2.0","id":1, "method":"getAssetsByOwner", "params":{"ownerAddress":"9xQe…", "page":1,"limit":10}}'
import { Surplus } from '@surplus/sdk'; const client = new Surplus({ baseUrl: 'http://localhost:4200', apiKey: 'spl_live_demo.DEMOSECRET', }); const res = await client.rpc('solana', { method: 'getAssetsByOwner', params: { ownerAddress: '9xQe…', page: 1, limit: 10 }, }); console.log(res.receipt.state); // "settled"
| Header | Direction | Meaning |
|---|---|---|
| X-Request-Id | response | Unique id per request; quote it when disputing a receipt. |
| X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset | response | Token-bucket allowance per key, what’s left, and when the window resets (unix seconds). |
| Idempotency-Key | request | Client-generated key on routed calls. Retries with the same key return the original result instead of executing again. |
| X-Surplus-Replay | response | true when the response is an idempotent replay — the call was not double-charged. |
| X-Surplus-Upstream | response | live when the call was answered by an upstream provider (passthrough mode); mock otherwise. Enum value per the contract. |
| X-Surplus-Route X-Surplus-Offer X-Surplus-Units X-Surplus-Charged-Usd X-Surplus-Billing-State | response | The per-request receipt in the envelope: who served it, how many units, what it cost, and the billing state. |
A real request builder for the routed call — it runs against the local gateway from your browser.
| Response header | Value |
|---|
Liveness check. No auth.
{ "ok": true, "service": "surplus-gateway", "mode": "local" }
Aggregate exchange stats. No auth.
{ "requests": 12847, "volumeMicroUsd": 483920,
"avgSavingsPct": 31, "activeOffers": 5 }
Server-sent event stream of market activity — fill, health,
and price events as JSON lines. No auth. The market page
ticker is driven by this stream.
# event: fill { "type": "fill", "route": "rpc/solana", "offerId": "ofr_sol_1", "units": 10, "chargedMicroUsd": 42, "listMicroUsd": 60, "ts": "…" } # event: health { "type": "health", "offerId": "ofr_eth_1", "health": "unhealthy" } # event: price { "type": "price", "offerId": "ofr_bsc_1", "askPerUnitMicroUsd": 2 }
List market offers. Optional query ?route=rpc/solana. No auth.
{
"offers": [ {
"id": "ofr_sol_1", "route": "rpc/solana", "provider": "northstar",
"askPerUnitMicroUsd": 4, "unit": "credit",
"listPriceMicroUsd": 6, "listPriceDated": "2026-09-01",
"capacityRemaining": 840000, "health": "healthy", "feeBps": 500
} ]
}
List fallback-catalog tools. No auth.
{
"tools": [ {
"id": "tool_bsc_getBlockByHash", "route": "rpc/bsc",
"method": "eth_getBlockByHash", "summary": "…",
"params": ["blockHash", "fullTransactions"],
"pricePerCallMicroUsd": 1, "source": "market", "status": "available"
} ],
"counts": { "available": 7, "listed": 8 }
}
Mint a scoped key. Locally no auth is required; in production this sits behind a wallet session.
// request { "label": "ci", "scopes": ["rpc:execute"], "monthlyBudgetMicroUsd": 5000000, "perRequestCeilingMicroUsd": 500 } // response — the secret is shown exactly once { "keyId": "key_01…", "apiKey": "spl_live_key_01…<secret>", "warning": "Shown once. Store it now." }
Workspace balance. Auth required.
{ "availableMicroUsd": 2499958, "heldMicroUsd": 42,
"currency": "USDC", "network": "solana", "mode": "local" }
Create a funding intent. This environment credits instantly.
// request { "amountMicroUsd": 25000000 } // response { "intentId": "fnd_01…", "depositAddress": "SurpL9f4Kw2i7mVxQeBn8cRtYp3Hs6Gd5Ja1", "network": "solana", "status": "credited", "mode": "local" }
The routed call. :chain ∈ solana, bsc, ethereum.
Auth + scope rpc:execute. Body is a JSON-RPC payload, passed through.
Routing picks the cheapest healthy offer; if none, the catalog serves it.
Billing: hold min(quote, key ceiling) → settle actual → receipt stored.
// response body { "jsonrpc": "2.0", "id": 1, "result": { … }, "surplus": { "receiptId": "rcpt_01…" } } // response headers X-Surplus-Route: market | catalog X-Surplus-Offer: ofr_xxx (market only) X-Surplus-Units: 10 X-Surplus-Charged-Usd: 0.000042 X-Surplus-Billing-State: settled
Receipt ledger, newest first. Auth required; :id returns a single receipt.
Paginate with ?limit= and &cursor= — the response carries
nextCursor while more pages exist.
{ "receipts": [ {
"id": "rcpt_01…", "route": "rpc/solana", "servedBy": "market",
"offerId": "ofr_sol_1", "units": 10, "chargedMicroUsd": 42,
"feeMicroUsd": 2, "state": "settled", "createdAt": "…"
} ],
"nextCursor": "rcpt_01…" }
Seller side. In this environment the probe always succeeds and the listing flips to
healthy on the next GET /v1/offers.
// request { "route": "rpc/solana", "providerKey": "sk-live-…", "askPerUnitMicroUsd": 4, "capacity": 1000000, "period": "monthly" } // response { "offerId": "ofr_…", "status": "probing", "attestationRequired": true }
| State | Meaning |
|---|---|
| held | Funds reserved at min(quote, per-request ceiling) before the call executes. |
| settled | Call completed; actual units charged, remainder of the hold released. |
| voided | Call failed or refused pre-execution; hold released in full. |
| under_review | Settle disagreed with quote; funds locked pending review. |
All errors share one shape, with a matching HTTP status:
{ "error": { "code": "…", "message": "…" } }
| Code | When |
|---|---|
| insufficient_balance | Available balance can’t cover the hold. |
| no_route | No healthy offer and no catalog entry for the route. |
| unauthorized | Missing/invalid key, or the key lacks the required scope. |
| invalid_request | Malformed body, unknown chain, bad JSON-RPC payload. |
| rate_limited | Token bucket empty — see X-RateLimit-Reset for when to retry. |