The panel's API. Loopback only: the request must come from a loopback address with a localhost or 127.0.0.1 Host header and (if present) a localhost Origin; anything else gets 403. Never proxy /api/* to a network. JSON in, JSON out; errors are {"error":{"message":"..."}} with a 4xx/5xx status.
Source of truth: router/http/api.mjs and router/http/api-chain.mjs. If this page and the code disagree, the code wins.
The OpenAI-compatible endpoint (/v1/*) is documented in USAGE; the peer protocol (/p2p/*) in PROTOCOL.
API=http://localhost:8545
curl -s $API/api/status
Node and market#
| Method and path | Body | Returns |
|---|---|---|
GET /api/status |
node (id, address, name, version, protocol, publicUrl, port, dataDir path, encrypted boolean: identity and wallet are encrypted at rest because ELYRA_PASSPHRASE is set; never the passphrase or any key), endpoint, balance, spendCap ({usdDay, spentUsd}: the daily spend cap or null, and what the ledger shows spent in the last 24 h), stats (24 h counters and 14-day series), network (peersLive, peersKnown, simPeers, models, listings, teeListings, liveListings, demoNetwork), providing, tee, policy |
|
GET /api/models?q=text |
{models:[...]} every listing with score; q filters by model, peer or vendor |
|
POST /api/route/preview |
{model} (default auto) |
{model, options: top 6 ranked, total} |
GET /api/routes |
{routes:[...]} last 60 routes, newest first. Metadata only: model, peer, token counts, amounts. Never prompt text |
|
PUT /api/settings |
{demoNetwork?: bool, name?: string, spendCapUsdDay?: number|null, registryDiscover?: bool, registryAnnounce?: bool} |
{settings, name} (spendCapUsdDay: dollars per rolling 24 h that paid outgoing requests may cost, null or "" = no cap, 0 = block every paid route; a negative, non-finite or non-numeric value is 400 and nothing else in the request is applied. registryDiscover: read the on-chain registry for peers; registryAnnounce: list this seller on-chain, costs gas) |
Policy#
GET /api/policy |
{weights:{price,speed,reputation,privacy}, teeOnly, maxPrice} |
PUT /api/policy |
any of weights (normalised), teeOnly (bool), maxPrice (number, or null/"" for no limit). Returns the stored policy |
Peers and trust#
GET /api/peers |
{peers:[...]} |
POST /api/peers |
{url}: fetches the peer's signed hello and adds it. 400 cannot add peer: ... on failure (protocol mismatch, unreachable, private URL without --allow-private-peers) |
DELETE /api/peers/:id |
{ok:true} |
POST /api/peers/refresh |
re-fetch all peers |
GET /api/trust |
{allowedImages, allowedHw, issuer, audience} |
PUT /api/trust |
allowedImages: array of sha256:<64 hex>; allowedHw: subset of GCP_AMD_SEV, GCP_INTEL_TDX; jwksInline is test-only and needs ELYRA_ALLOW_TEST_TRUST=1 |
Model sources (upstreams)#
GET /api/upstreams |
{upstreams:[{id,name,kind,baseUrl,hasKey,price,models,builtin,remote,health}]} (keys are never returned, only hasKey). health is `{status:"up" |
POST /api/upstreams |
{name, baseUrl} required; apiKey?, price? (your cost, USD/MTok, default 0), models?, remote? (true when the URL is loopback or LAN but really another machine, for example an SSH tunnel: it is then labelled REMOTE and never counted as private). Without models the node lists them from <baseUrl>/models (400 if it cannot, or if the source reports none). In TEE mode only local sources are accepted |
GET /api/upstreams/detect |
{found:[...]} probes localhost for Ollama (11434), LM Studio (1234), 8000, 8080 |
POST /api/upstreams/:id/refresh |
re-discover models from the source |
DELETE /api/upstreams/:id |
remove (the built-in demo engine cannot be removed) |
Providing#
GET /api/provide returns the provider config plus offered (models actually offered) and publicUrl. PUT /api/provide accepts any of:
| Field | Rule |
|---|---|
enabled |
bool |
upstreamId |
id from /api/upstreams, or "*" for all healthy local/tunnel sources (excludes hosted API keys and the demo engine); changing it clears models |
upstreamIds |
array of ids from /api/upstreams (or ["*"]) to offer models from several sources in priority order; changing it clears models |
models |
array of model ids; empty means all |
price |
USD per million tokens, finite and >= 0 |
prices |
{modelId: usdPerMillionTokens} per-model price overrides (0 is valid; invalid/negative/null entries are dropped) |
tee |
"none" or "claimed" only. claimed is self-declared and never counts as verified |
maxConcurrent |
1 to 64 (default 4) |
creditUsd |
max unredeemed USD per consumer wallet (default 0.5) |
maxDepositUsd |
max on-chain escrow deposit in USDG accepted per buyer wallet while the contract is unaudited (0 < x <= 10000, default 20) |
autoRedeem, autoRedeemUsd |
auto-redeem on/off and threshold (default 0.25) |
payout |
address that receives swept USDG, or "" |
ratePerMin |
1 to 10000 (default 120) |
deny |
array of node ids to refuse (max 500) |
Ledger and settlement#
GET /api/ledger |
{balance, funded, address, chain, mode: "sim"|"chain", entries, batches, stats}. Receipt signatures are truncated |
POST /api/ledger/fund |
{amount?} adds simulated funds (default 10); simulated ledger only |
POST /api/ledger/settle |
settle outstanding vouchers; returns {batches, failed} |
Chain (escrow, USDG)#
GET /api/chain |
overview: configured network, escrow, token, balances, maxDepositUsd cap, faucets, explorer links |
PUT /api/chain |
{network?, rpcUrl?, chainId?, escrow?, registry?, registryBlock?, maxDepositUsd?}; network must be a known one, escrow/registry an address (registry = ElyraRegistry, registryBlock its deploy block), maxDepositUsd in (0, 10000] (the same cap as PUT /api/provide). Mainnet additionally needs the --allow-mainnet flag at boot |
GET /api/registry |
registry state: configured address, discover/announce switches, block cursor, last scan, last errors, this node's listing, entries and the ones still pending verification (with the reason) |
POST /api/registry/revoke |
unlist this node on-chain now (gas) |
POST /api/chain/mint |
{usd} 0 < usd <= 10000; only works for the mock test token |
POST /api/chain/deposit |
{usd} 0 < usd <= maxDepositUsd (default 20, cumulative currentDeposit + usd <= maxDepositUsd); approve + deposit into the escrow |
POST /api/chain/withdraw |
{action: "request"|"cancel"|"execute", usd?}; execute needs usd. 1-day delay, 7-day window |
Chain failures return 502 with type: "chain_error".
Live feed (owner only)#
GET /api/events |
Server-Sent Events. Each message is one JSON object on a data: line with id: set to the event id; heartbeat comments (: ping) every 15 s. Without a resume point you get only what happens next; Last-Event-ID (what a browser sends when it reconnects) or ?since=<id> replays newer events from the ring buffer first (an id from before a node restart replays what the ring holds). At most 8 streams at once (429 beyond), and a stream ends after 30 minutes so the client reconnects and its session is checked again |
GET /api/events/recent |
{events, lastId}: the ring buffer (at most 200 events, oldest first) for the first paint of a page |
Like all of /api, the feed is for loopback or a signed-in admin (token or allow-listed wallet) on the admin host; a wallet user session and everyone signed out never reach it. It is kept in memory only (nothing is written to disk, a restart empties it) and holds metadata only: an event is built from a whitelist (router/core/events.mjs), so there is no field that could carry a prompt, a completion, a key, an API key or an IP address. Wallets and tx hashes are shortened (0x1234…abcd).
Every event has id (rises by one per event), ts (ms) and kind:
kind |
Fields |
|---|---|
request |
role (buyer or seller), status (ok, error, refused), peer (name; a buyer's node id prefix as peer abc123 when you are the seller), model, promptTokens, completionTokens, costUsd, ttftMs, latencyMs, simulated (true for the demo market: never real, never settled), reason (why it was refused or failed: spend_cap, unfunded, no_route, rate_limited, busy, payment_required, denied, no_model, price_mismatch, upstream_error, ...), counterparty (the paying buyer's short wallet, only when you are the seller and settle on-chain) |
peer |
state (online, offline, discovered), name, viaRegistry, rttMs |
voucher |
action (issued, received, redeemed), amountUsd, tx (short, redeemed only), counterparty (short wallet), peer |
alert |
code (spend_cap, low_gas, upstream_down), subject, capUsd, spentUsd |
Repeated alerts (same code and subject) are folded to one per minute, and refused or failed requests from the same seller-side peer and reason to one per 5 seconds, so a flapping source or a stranger cannot fill the buffer.
curl -N $API/api/events # live
curl -N "$API/api/events?since=0" # everything in the buffer, then live
Operations endpoints#
GET /healthz returns {"ok":true,"version","protocol","node","uptime","peersOnline"} and is safe to expose. GET /metrics (Prometheus, loopback only) never contains prompt text.