Sitowise
API

API overview

A small read-only HTTP API over the same ledger the dashboard uses. No key, no signup, and no versioning games: six public endpoints, JSON in and out, rate limited by IP.

Base URL and conventions

ItemValue
Base path/api on this origin
MethodsGET for everything public
Content typeapplication/json
Errors{ "error": "human readable" } with a correct status
AuthenticationNone for public endpoints. See Authentication.

The public endpoints

Public surface
GET /api/stats
GET /api/nodes/:address
GET /api/node/:id?limit=50
GET /api/distributions?limit=50
GET /api/deploy-quote
GET /api/price

/api/deploy-quote answers { priceWei, paymentAddress, chainId }, and it is the only honest source for the first two. Buying a node is a plain ETH transfer to a payments wallet, so the contract never sees the payment: it has no price() to read and no address to point at, and both figures live in the server’s environment instead. Quote them from here rather than hardcoding either. A transfer sent to the wrong address, or carrying the wrong amount, does not become a node on its own.

/api/price has no page of its own because there is nothing to it: it proxies a spot ETH quote so the dashboard can print a dollar figure beside an ETH one, and answers { "usd": null } with a 200 when no quote is available. There is no fallback price, because a made-up number beside a real balance is worse than no number. It is a market rate for display, and unrelated to priceWei above.

Two more routes exist behind a wallet session, GET /api/me and POST /api/nodes/sync, and are covered on Authentication. Nothing in this API withdraws, and no endpoint here can move funds; see Withdrawing.

Wei is a string, always

Every value field ending in Wei is a decimal string of wei, never a number. Values routinely exceed what a double can represent exactly, and a client that parses them as numbers loses precision silently rather than failing.

Parsing correctly
const res  = await fetch("/api/stats");
const data = await res.json();

const total = BigInt(data.totalDistributedWei);   // correct
const wrong = Number(data.totalDistributedWei);   // loses precision above 2^53

Timestamps are ISO 8601 strings in UTC, or null when the underlying column is empty. Node ids come in two flavours, and every response says which is which; see Node numbering.

Rate limits

Public endpoints are limited per IP in a fixed window of one minute. The limit differs by endpoint because the work behind them differs.

EndpointRequests per minute per IP
/api/stats120
/api/nodes/:address60
/api/node/:id60
/api/distributions60
/api/deploy-quote60
/api/priceNot limited. The upstream quote is fetched at most once a minute for everyone.
/api/me120
/api/nodes/sync, /api/auth/nonce, /api/auth/verify20
/api/auth/logout30

Every answer from a limited endpoint carries the state of your window in headers, whether it succeeded or not:

Response headers
x-ratelimit-limit:     60
x-ratelimit-remaining: 57
x-ratelimit-reset:     1756041600     # unix seconds
retry-after:           23             # only on a 429
Counters are per server instance, so the effective allowance can be higher than the table suggests when several instances are running. Do not build anything that depends on the limit being exactly this number; treat it as the floor and back off on a 429.

Caching

Public reads are cacheable for a few seconds at the edge, which is why a figure can lag the chain slightly. Nothing derived from a session is ever cached by a shared cache.

EndpointCache-Control
/api/statspublic, max-age=0, s-maxage=10, stale-while-revalidate=30, plus a short in-process memo
/api/nodes/:address, /api/node/:idpublic, max-age=0, s-maxage=5, stale-while-revalidate=15
/api/distributionspublic, max-age=0, s-maxage=10, stale-while-revalidate=30
/api/deploy-quotepublic, max-age=0, s-maxage=30, stale-while-revalidate=90. Short, because it is the number a user is about to send money against.
/api/pricepublic, max-age=60, browser included, since a spot quote is the same for everyone
/api/cron/*no-store. A liveness reading is not a document.
Anything behind a sessionprivate, no-store

Operational endpoints

Three routes under /api/cron exist for the scheduler that keeps the protocol moving. Two of them do work and are gated by a secret; the third only reports and is open to anyone.

EndpointGateWhat it does
/api/cron/paymentsx-cron-keyOne pass of the payment pipeline: read new blocks for transfers to the payments wallet, then mint a node for each payment that checks out. Every step is idempotent, so calling it twice costs RPC and nothing else.
/api/cron/creditx-cron-keyOne credit pass over the nodes that are due. Held by an advisory lock, because two passes crediting the same nodes would pay twice.
/api/cron/healthNoneWhether the credit worker is alive and whether it can still pay. Read only, and public on purpose.

Both working routes answer to GET as well as POST, because several hosted schedulers only issue GET. There is no body either way. An overlapping run is not an error: the second caller gets a 200 with ran: false, because a job that runs every minute and occasionally takes longer than a minute is behaving normally.

/api/cron/health

Public because everything in it is already public. The contract’s balance, what it owes, whether it is solvent and whether payouts are switched on can all be read from the chain by anyone, and a node holder has a fair claim to see them without an operator’s key. What it deliberately leaves out is the identity of any key-holding account. The distributor’s balance appears as a number with no address attached, which says the float is running low without saying where the float lives.

200 application/json
{
  "lastTickAt": "…",  "secondsSinceLastTick": 0,
  "stale": false,     "staleAfterSec": 300,  "tickSec": 60,
  "distEnabled": true, "distMode": "treasury",
  "paused": false,
  "dueNodes": 0,      "scheduledNodes": 0,
  "distributorBalanceWei": "…",
  "contractBalanceWei": "…",
  "outstandingWei": "…",
  "isSolvent": true
}

Any reading that could not be taken is null, never a zero that would look measured. A worker that has never ticked reports stale: true with secondsSinceLastTick: null: absent and stale are both wrong, but they are different problems. isSolvent is the contract’s own answer to whether its balance covers everything it owes, and it can be checked directly with isSolvent(); see Factory interface.

The chain is the source of truth

This API serves the operator’s ledger. The parts of it that involve money are also on chain, and where the two disagree, the chain is right: node ownership through nodesOf(address), and the owner, balance, credited total and withdrawn total for one node through nodeInfo(id), which returns all five at once. Anything that matters for money can be verified without this API at all; see Factory interface.

Stability

  • Fields may be added. Ignore ones you do not recognise.
  • Existing fields will not change type or meaning without the change appearing in the changelog.
  • There is no version prefix in the path, and no plan to add one.
  • The endpoints under /api/admin are not public and answer 404 when no admin key is configured. The same is true of /api/cron/payments. Neither is part of this API’s surface, and neither is documented field by field.