Sitowise
API

Authentication

Everything worth reading is public and needs no credentials. A session exists only for the endpoints that act on behalf of one wallet, and it is created by signing a message, not by a password.

Public, no credentials

Node ownership is public data. The same list can be read off the contract with nodesOf(address), so putting it behind a key would protect nothing and only make the protocol harder to verify.

EndpointNeeds
GET /api/statsNothing
GET /api/nodes/:addressNothing
GET /api/node/:idNothing
GET /api/distributionsNothing
GET /api/deploy-quoteNothing
GET /api/priceNothing
GET /api/cron/healthNothing

/api/deploy-quote is unauthenticated because a wallet needs the payment address and the exact amount before it has signed anything, and /api/cron/health because everything it reports is already readable from the chain. Both are covered on the API overview.

Session endpoints

A session is needed for the routes that act for a specific wallet. There are two, and they exist for the dashboard rather than for general use.

EndpointPurpose
GET /api/meThe signed-in wallet, its nodes, totals, and any node the chain shows that the ledger has not recorded
POST /api/nodes/syncRegister a node that was just minted, from its transaction hash or id

There is no session route for withdrawing, because withdrawing does not go through this API at all. See Withdrawing.

Signing in

Two calls, and one wallet signature between them.

Step 1: get a nonce and the exact message to sign
POST /api/auth/nonce      // GET works too and returns the same thing
-> { "nonce": "…", "message": "…" }

The nonce is also set in an httpOnly cookie. The signature is verified against that copy, so a nonce captured from the response body is useless in another browser. The message field is the exact string to pass to personal_sign; rebuilding it yourself risks drifting from what the server verifies.

Step 2: send the signature
POST /api/auth/verify
{ "address": "0x…", "signature": "0x…" }

-> { "address": "0x…" }  plus a session cookie

The address in the body is not a claim of identity on its own. It is accepted only because the signature over this browser’s nonce recovers to it, and the nonce cookie is deleted as soon as it has been used, so a captured signature cannot be replayed. Every route after this one reads the address from the cookie, never from a body.

Signing in is not a transaction. It costs no gas, appears nowhere on chain, and grants no permission to move funds. It proves one thing: that you control the private key for that address.

Signing out
POST /api/auth/logout
-> { "ok": true }        // clears the session and any half-finished sign-in

Signing out always succeeds. Ending a session that had already lapsed is not an error.

  • httpOnly, so page scripts cannot read it.
  • Signed with a server secret. The value is the address, the expiry, and an HMAC over the two, so it cannot be forged or edited.
  • SameSite=Lax, so another site cannot cause your browser to use it, and Secure in production.
  • Expiring, seven days from sign-in. When it lapses, sign in again.
  • Stateless. Nothing is stored server side, so there is no session table holding a list of who is signed in.

Requests carrying a session are answered with cache-control: private, no-store, so nothing about one wallet can be served to another from a shared cache.

What a session cannot do

Operator keys

Two header secrets exist alongside the wallet session. Neither is part of the public API, neither is documented field by field, and no wallet signature grants either one: they are server configuration, not something a user can hold.

HeaderGatesWhen it is not configured
x-admin-keyEverything under /api/admin404, so an unconfigured admin surface does not advertise that it exists
x-cron-key/api/cron/payments and /api/cron/credit, the two scheduled passes that mint nodes and credit balances/api/cron/payments answers 404, for the same reason as the admin surface. /api/cron/credit fails closed with a 401 instead: a route that spends real ETH must refuse everyone rather than open up when a variable is missing.

/api/cron/payments also accepts the key as authorization: Bearer, because some schedulers can only set that header. /api/cron/credit reads x-cron-key only. Both keys are compared in constant time, and a wrong key is answered with 401 and no detail. /api/cron/health takes no key at all; it only reports.

Status codes you will see

StatusMeaning on a session route
200Signed in, request served
400The body was malformed: not JSON, not an object, a missing field, or an id or hash that does not parse
401Not signed in, the session expired, the sign-in nonce had already been used or lapsed, or the signature did not recover to the address given. On /api/me this is a normal answer for a visitor who has not connected.
403On /api/nodes/sync: the node exists but belongs to a different wallet
404On /api/nodes/sync: no such node, or its mint transaction has not confirmed yet
409On /api/nodes/sync: the chain no longer shows that node as yours, so nothing was recorded
429Rate limited. Sign-in and /api/nodes/sync are limited more tightly than reads: 20 requests per minute per IP, against 120 for /api/me.

The full table, including what each error means and whether retrying helps, is on Errors.