Node API reference
The protocol the node client speaks. You never call these by hand — ai-trader does — but this is the contract behind register, bench, serve, and the auto-undercut ask.
Endpoints
| POST | /v1/nodes | Register a machine. Authenticated with your account key; returns a node key. |
| POST | /v1/node/bench | Record measured tok/s per model. Only benchmarked models may be quoted. |
| POST | /v1/node/asks | Rest an ask. A new ask for a model replaces your resting one. |
| WS | /v1/node/ws | Hold the link. Jobs arrive down it; generated text goes back up it. |
1 · Register the machine
Provider key in, node key out
POST /v1/nodes
Authorization: Bearer pk_live_... # your account key (sk_live_ works too)
{
"handle": "quartzite",
"gpu": "NVIDIA GeForce RTX 4090",
"accelerator": "cuda", # cuda | metal | rocm | cpu
"vram_gb": 24,
"mem_bandwidth_gbs": 1008,
"region": "us-west"
}
201 Created
{ "id": "nd_7f31", "node_key": "nk_live_..." }2 · Report the benchmark
Measured, not declared
POST /v1/node/bench
Authorization: Bearer nk_live_... # the node key from register
{
"models": [
{ "model": "llama-3.1-8b-instruct", "tok_per_sec": 142.9 },
{ "model": "qwen2.5-coder-32b", "tok_per_sec": 38.2 }
]
}
200 OK
{ "node_id": "nd_7f31", "registered": ["llama-3.1-8b-instruct", "qwen2.5-coder-32b"] }3 · Rest an ask
A new price replaces the old
POST /v1/node/asks
Authorization: Bearer nk_live_...
{
"model": "llama-3.1-8b-instruct",
"floor_per_mtok": 0.18, # your price, USD per Mtok
"size_tokens": 8000000,
"expires_in": 86400
}
201 Created
{
"id": "sk_4a90",
"status": "open",
"filled_tokens": 0,
"remaining_tokens": 8000000,
"replaced_asks": 1 # your prior ask for this model was retired
}4 · Hold the link and serve
GET /v1/node/ws (WebSocket)
→ {"t":"auth","key":"nk_live_..."}
← {"t":"ready","node_id":"nd_7f31"}
← {"t":"job","job":{"jobId":"jb_4c1f","modelId":"llama-3.1-8b-instruct",
"prompt":"Explain a limit order book.","maxTokens":512}}job frame arrives; you forward its prompt to your local backend and send the tokens back up the same socket as they decode.→ {"t":"chunk","job":"jb_4c1f","text":"A limit order book "}
→ {"t":"chunk","job":"jb_4c1f","text":"is a list of resting orders…"}
→ {"t":"done","job":"jb_4c1f","tokens":128,"reason":"stop"}
# generation failed instead
→ {"t":"error","job":"jb_4c1f","message":"model is not pulled"}
# stop decoding and send no trailer.
# reason "budget": the text you sent is delivered and paid for.
# reason "gone": the buyer left; this job earns nothing.
← {"t":"stop","job":"jb_4c1f","reason":"budget"}done trailer is not optional bookkeeping: it is the only way the venue learns whether the model finished or was cut off at maxTokens. A job that ends without one settles marked un-attested.5 · Prove the link is still there
Because a dead socket looks open
→ {"t":"ping"}
← {"t":"pong"}ping on an interval and expect pong straight back. Treat a few missed replies as a dropped link, reconnect, and re-rest your asks. ai-trader does this every 30s and gives up after 90s of silence. A node that skips it stays online in its own eyes while earning nothing.Two keys, two scopes
You authenticate to register with your account key — the same key you buy with. Accounts created on the Host side carry a pk_live_ prefix and ones created on the Prompt side an sk_live_ prefix; the prefix is a label, not a scope, and either works here. Everything after that uses the node key it hands back: the account and node behind an ask are fixed by the credential, so a node can only ever post supply for itself.
A new ask replaces the old one
Posting an ask for a model you already quote does not stack a second order beside the first — it retires the resting one and rests the new price in its place. The response says how many it replaced in replaced_asks. This is why lowering your floor is simply a matter of posting again, or restarting serve after editing config.toml: the config is the source of truth, and each reconnect re-rests it over whatever was there.
Asks escrow nothing, so replacing one moves no money. It is a status change and nothing more; any fills already struck against the old ask keep their jobs and settle on their own.
Auto-undercut: let your ask chase the fill
An ask can lower its own price over time until it wins flow. Add an auto_undercut block and the venue steps your floor down toward a hard minimum on a timer, re-checking the book at each step:
POST /v1/node/asks
{
"model": "llama-3.1-8b-instruct",
"floor_per_mtok": 0.22, # start here
"size_tokens": 8000000,
"auto_undercut": {
"step_per_mtok": 0.005, # shave this each interval
"every_seconds": 300, # every five minutes
"floor_per_mtok": 0.12 # never go below this
}
}It descends only as far as it must: the first interval at which your ask becomes the cheapest eligible one wins a fill and the descent stops. Reach the floor without filling and it simply rests there. Use it to hold the top of the queue on a busy model without babysitting a price, and set the floor to the number below which the electricity is not worth it.
Staying competitive
Jobs route to the lowest ask at or below the buyer's limit, price first and arrival time only as a tie-break. Being online longer earns you nothing if someone rests a cheaper ask — uptime is deliberately not a routing input, because paying for it starves new supply and rewards never restarting. To win more flow, lower your floor.
Small models are where a modest box competes best: the weights fit with room for KV cache, the bandwidth ceiling is high, and the edge is volume rather than margin per token. A single consumer card serving an 8B earns real but modest money, and it is worth doing because the hardware and the power are largely sunk. See the install guide for the bench that decides what you may list, and pricing for the fee schedule.
Errors worth handling
409 not_benchmarked— you quoted a model this node has no benchmark on record for. Runbenchfirst.4401on the socket — the key was refused, or an operator suspended the node. Reconnecting will not help until the key is replaced.4400on the socket — a frame arrived beforeauth.- Frames naming a job that did not route to you, or whose buyer has already gone, are dropped silently. Nothing is billed and nothing is delivered, so treat a job with no
stopand no acknowledgement as abandoned rather than retrying it.
Every endpoint here answers on the development venue, and the node client drives them end to end. The hosted installer is the packaging still to come; see Install the node.