BUILD ON AVENIA

Everything you need to submit runs, stream loss, list a GPU, and self-host the brain. The browser calls the same origin under /v1/*; the coordinator base URL is http://localhost:8788 in dev. The network settles in AVEN on Robinhood Chain.

Quickstart

Submit a run and read the streamed loss over SSE. The response is an event stream: routing โ†’ progress* โ†’ artifact โ†’ usage โ†’ done (or error).

curl ยท POST /v1/train
curl -N -X POST http://localhost:8788/v1/train \
  -H 'content-type: application/json' \
  -H 'accept: text/event-stream' \
  -H 'x-avenia-wallet: 0xYOURWALLET' \
  -d '{
    "model": "aven-qwen-0.5b",
    "dataset": [{"prompt": "hi", "completion": "hello there"}],
    "hyperparams": { "epochs": 3, "loraRank": 16 },
    "stream": true
  }'

Architecture

You never talk to a GPU directly. The coordinator is the privacy chokepoint - it strips your identity before dispatch, so the worker only ever sees a dataset URL and hyperparameters.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ YOU ยท client                                                   โ”‚
โ”‚ dataset.jsonl + base model + hyperparams                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚  POST /v1/train   (SSE stream opens)
                               v
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ COORDINATOR ยท the brain                                        โ”‚
โ”‚ holder-gate ยท acquire idle worker ยท relay ยท settle             โ”‚
โ”‚ privacy chokepoint - strips identity before dispatch           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚  assign DispatchedJob  (no wallet / no ip)
                               v
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ GPU WORKER ยท someone else's card                               โ”‚
โ”‚ pulls dataset ยท LoRA fine-tune ยท streams step + loss           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                               โ”‚  job_done  โ†’  adapter.safetensors
                               v
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ BACK TO YOU                                                    โ”‚
โ”‚ weights returned ยท dataset never persisted or logged           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

REST endpoints

All JSON. A missing or empty x-avenia-wallet fails open while the token CA is unset, so the API is usable before launch.

GET/health

Liveness probe. Returns instantly whether or not any worker is connected.

example response
{ "status": "ok", "uptime": 128734 }
GET/v1/models

The base-model registry plus whether at least one online worker can currently serve each one.

example response
[
  {
    "id": "aven-qwen-0.5b",
    "displayName": "Avenia Qwen 0.5B",
    "family": "qwen2.5",
    "params": 0.5,
    "minVramGb": 6,
    "pricePerGpuMinute": 4,
    "available": true
  }
]
GET/v1/network

Live worker-mesh snapshot: every worker plus aggregate online / idle / busy stats. Drives the Network page.

example response
{
  "workers": [
    {
      "id": "wk_9d2", "label": "marketplace", "status": "busy",
      "region": "us-west", "runtime": "cuda", "gpu": "A100 80GB", "vramGb": 80,
      "models": ["aven-qwen-0.5b"], "throughput": { "aven-qwen-0.5b": 88.0 },
      "jobsCompleted": 812, "lastSeen": 1720000000000
    }
  ],
  "stats": {
    "online": 6, "idle": 3, "busy": 3, "totalJobs": 1801,
    "modelsAvailable": ["aven-qwen-0.5b", "aven-llama-1b", "aven-llama-3b"],
    "gpuSecondsServed": 4182540
  }
}
GET/v1/providers

Provider leaderboard: jobs served, gpu-time, earnings, and current worker share (boosted when staked).

example response
[
  {
    "id": "prov_north",
    "jobsCompleted": 812, "gpuSecondsServed": 1940220,
    "earnings": 24806.4, "hasActiveStake": true, "workerShare": 0.85,
    "workers": [ /* NetworkWorker[] */ ]
  }
]
GET/v1/usage

Account credit balance plus the most recent settled jobs. Never includes dataset content.

paramnotes
accountthe account/wallet to summarize (query string)
example response
{
  "account": "0xYOURWALLET",
  "credits": 1284.66, "jobsCompleted": 37, "gpuSeconds": 51234,
  "recentJobs": [
    {
      "requestId": "req_a1f", "model": "aven-qwen-0.5b", "workerId": "wk_9d2",
      "trainSteps": 120, "gpuSeconds": 138, "finalLoss": 0.61, "evalLoss": 0.63,
      "credits": 9.2, "adapterUri": "avenia://adapter/โ€ฆ", "status": "completed",
      "createdAt": 1720000000000
    }
  ]
}
GETPOST/v1/stake

Read the holder staking vault (GET), or stake / unstake (POST { account, amount }). Staking a worker's account unlocks the boosted share.

paramnotes
accountGET query / POST body - the holder account
amountPOST body - signed delta to stake (+) or unstake (โˆ’)
example response
{
  "account": "0xYOURWALLET",
  "staked": 250000, "totalStaked": 41820000, "tier": "boosted",
  "baseShare": 0.7, "boostedShare": 0.85,
  "rewardsAccrued": 1284.66, "rewardPool": 96540.2
}
POST/v1/train

Submit a training job. With accept: text/event-stream the response is an SSE stream of TrainEvents (see below). Body is a TrainRequest.

paramnotes
modelbase-model id from /v1/models (required)
datasetinline [{prompt, completion}] for small runs
datasetUrlor a URL the worker pulls the jsonl from
hyperparamspartial - omitted fields fall back to defaults
streamtrue to receive SSE (otherwise a single JSON summary)
example response
data: {"type":"routing","workerId":"wk_9d2","workerLabel":"marketplace","gpu":"A100 80GB"}
data: {"type":"progress","step":40,"totalSteps":120,"loss":1.42,"etaMs":1800}
data: {"type":"artifact","adapterUri":"avenia://adapter/โ€ฆ","adapterBytes":18400000,"evalLoss":0.63}
data: {"type":"usage","gpuSeconds":138,"trainSteps":120,"credits":9.2}
data: {"type":"done"}
GET/v1/artifact/:id

Stream the trained LoRA adapter (adapter.safetensors) back to the submitter. Binary body; the id comes from the artifact event's adapterUri.

example response
HTTP/1.1 200 OK
content-type: application/octet-stream
content-disposition: attachment; filename="adapter.safetensors"
< binary safetensors bytes >
GET/v1/dataset/:id

The short-lived internal URL a worker pulls a job's dataset from. Hosted only for the run's duration; never persisted to the ledger.

example response
{"prompt":"โ€ฆ","completion":"โ€ฆ"}
{"prompt":"โ€ฆ","completion":"โ€ฆ"}   // jsonl, one example per line
GET/v1/access/challenge

Get a nonce to sign for the holder gate. The wallet signs the returned message to prove ownership before submitting.

paramnotes
walletthe wallet address requesting access
example response
{ "nonce": "a1f9โ€ฆ", "message": "Avenia access: sign nonce a1f9โ€ฆ to submit a run" }
GET/v1/access/status

Whether a wallet currently clears the hold-to-submit gate. Fails open (ok:true) while the token CA is unset.

paramnotes
walletthe wallet address to check
example response
{ "wallet": "0xYOURWALLET", "holds": 248500, "required": 1000, "ok": true }

SSE - the TrainEvent sequence

Each SSE data: line is one TrainEvent. A healthy run emits, in order:

  • routing - the worker + GPU your job landed on.
  • progress* - repeated: step, totalSteps, loss, etaMs.
  • artifact - adapterUri, adapterBytes, evalLoss (your quality receipt).
  • usage - gpuSeconds, trainSteps, credits billed.
  • done - terminal success (or error with a message).
[1] SUBMIT     POST /v1/train  ยท  dataset.jsonl + base model + hyperparams
     โ”‚
     v
[2] ROUTE      enforce holder-gate  โ”€>  acquire idle eligible worker
     โ”‚                                  (selected by measured throughput)
     v
[3] TRAIN      the worker fine-tunes a LoRA ยท streams step + loss for minutes
     โ”‚
     โ”‚  loss 2.41 โ”คโ–“โ–’โ–‘
     โ”‚       1.78 โ”ค   โ–’โ–“โ–’โ–‘
     โ”‚       1.14 โ”ค       โ–‘โ–’โ–“โ–’โ–‘
     โ”‚       0.79 โ”ค           โ–‘โ–’โ–“โ–’โ–’โ–‘โ–‘___
     โ”‚       0.63 โ”ค                    โ–‘โ–’โ–‘โ–‘________
     โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> step
     v
[4] ADAPTER    eval loss = your quality receipt ยท adapter.safetensors uploaded
     โ”‚
     v
[5] SETTLE     credits = gpu_minutes ร— model_rate ยท one atomic tx, Robinhood Chain
               worker keeps 70/85%  ยท  holders 80% of margin  ยท  treasury 20%

WebSocket - the worker protocol

Workers connect to ws://localhost:8788/worker. One JSON object per frame, discriminated by type. Heartbeats every 5s; the coordinator waits up to 30s for an idle worker before failing a submit.

Worker โ†’ Coordinator

  • register - capabilities (gpu, vram, runtime, models, throughput).
  • heartbeat - { status: idle | busy | offline }.
  • benchmark_result - measured samples/sec per model.
  • job_progress - step, totalSteps, loss, etaMs.
  • job_done - adapterUri, gpuSeconds, finalLoss, evalLoss.
  • job_error - jobId + message.

Coordinator โ†’ Worker

  • registered - the assigned workerId.
  • job_assign - a DispatchedJob (no wallet / no IP).
  • cancel - abort a running job by id.
  • ping - keepalive.
DispatchedJob - what a worker actually receives
{
  "jobId": "job_7c3",
  "model": "aven-qwen-0.5b",
  "backend": "Qwen/Qwen2.5-0.5B-Instruct",
  "datasetUrl": "http://localhost:8788/v1/dataset/ds_a1f",
  "datasetRows": 3,
  "hyperparams": {
    "epochs": 3, "loraRank": 16,
    "loraAlpha": 32, "learningRate": 0.0002,
    "batchSize": 2
  }
}

Code samples

JavaScript (fetch + stream reader)

submit-and-stream.js
// The stream is a POST, so use fetch + a stream reader (not EventSource,
// which is GET-only). This is exactly what the live console does.
const res = await fetch("/v1/train", {
  method: "POST",
  headers: { "content-type": "application/json", accept: "text/event-stream" },
  body: JSON.stringify({
    model: "aven-qwen-0.5b",
    dataset: [{ prompt: "hi", completion: "hello there" }],
    hyperparams: { epochs: 3, loraRank: 16 },
    stream: true,
  }),
})

const reader = res.body.getReader()
const dec = new TextDecoder()
let buf = ""
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  buf += dec.decode(value, { stream: true })
  const blocks = buf.split("\n\n")
  buf = blocks.pop() ?? ""
  for (const block of blocks) {
    const line = block.split("\n").find((l) => l.startsWith("data:"))
    if (!line) continue
    const ev = JSON.parse(line.slice(5).trim()) // TrainEvent
    console.log(ev.type, ev)
  }
}

// For a GET SSE endpoint you would instead use:
//   const es = new EventSource(url); es.onmessage = (e) => JSON.parse(e.data)

Python (requests, streamed)

submit_and_stream.py
import json, requests

with requests.post(
    "http://localhost:8788/v1/train",
    headers={"content-type": "application/json", "accept": "text/event-stream"},
    json={
        "model": "aven-qwen-0.5b",
        "dataset": [{"prompt": "hi", "completion": "hello there"}],
        "hyperparams": {"epochs": 3, "loraRank": 16},
        "stream": True,
    },
    stream=True,
) as r:
    for line in r.iter_lines():
        if line and line.startswith(b"data:"):
            ev = json.loads(line[5:].strip())  # TrainEvent
            print(ev["type"], ev)

Run a worker

A worker advertises which base models it can fine-tune (by VRAM) and earns 70โ€“85% of every job. The mock runtime needs no GPU; the Python runtime spawns a real trainer sidecar.

run a worker
# mock runtime - no GPU, deterministic loss curve (dev / CI default)
AVENIA_RUNTIME=mock npm run worker -- start --models aven-qwen-0.5b

# real GPU - install the trainer sidecar first, then serve
pip install -r packages/worker/trainer/requirements.txt
npm run worker -- start \
  --runtime python \
  --models aven-qwen-0.5b,aven-llama-1b,aven-llama-3b \
  --account acct_you \
  --label marketplace

Self-host the coordinator

The coordinator is the whole backend: an HTTP + WebSocket server on port 8788. Point the web app at a hosted one with COORDINATOR_HTTP_URL.

self-host
# clone + install the workspace (coordinator, worker, cli, web)
git clone <repo> avenia && cd avenia && npm install

# boot the coordinator (HTTP + WS on :8788) with a mock worker
npm run dev

# point the web app at a hosted coordinator instead of localhost
COORDINATOR_HTTP_URL=https://coord.example.com npm run web

Pricing & settlement

job_price = gpu_minutes ร— model_rate. On completion the price is split in one atomic transaction:

JOB PRICE   =  gpu_minutes ร— model_rate                      (100%)
โ”‚
โ”œโ”€โ”€>  WORKER REWARD ......... 70%     (85% when the worker is staked)
โ”‚
โ””โ”€โ”€>  PROTOCOL MARGIN ....... 30%     (15% when the worker is staked)
          โ”‚
          โ”œโ”€โ”€>  HOLDER POOL .... 80% of margin  โ”€>  pro-rata to $AVEN holders
          โ”‚
          โ””โ”€โ”€>  TREASURY ....... 20% of margin  โ”€>  ops ยท grants ยท runway

The worker keeps 70% (85% if staked); of the remaining margin, 80% goes to the holder pool and the rest to the treasury. Steps are clamped at 5,000. Nothing is minted - supply is fixed at 1,000,000,000 AVEN.