Modelo desconectado β€” el backend de inferencia no estΓ‘ accesible. Las solicitudes fallarΓ‘n hasta que se recupere.
OrΓ‘culo desconectado β€” el feed de tasa XMR/USD no estΓ‘ disponible. Las solicitudes fallarΓ‘n hasta que se recupere.
Monedero sin conexiΓ³n β€” el monedero de Monero no estΓ‘ disponible. Los depΓ³sitos estΓ‘n en pausa.
BΓΊsqueda web sin conexiΓ³n β€” el precio de bΓΊsqueda no estΓ‘ disponible. Las bΓΊsquedas fallarΓ‘n hasta que se restablezca.
PrΓ³ximamente β€” plurnk aΓΊn no estΓ‘ abierto; las cuentas, la financiaciΓ³n y la inferencia no estΓ‘n activas.
Servicio disponible actualmente solo para ciudadanos estadounidenses adultos sujetos a la jurisdicciΓ³n estadounidense.
El token de sesiΓ³n no coincidiΓ³. Recarga la pΓ‘gina e intΓ©ntalo de nuevo.

DepΓ³sito recibido: pΙ± DepΓ³sito acreditado: pΙ± DepΓ³sito liquidado: pΙ±

API

Los formularios, OpenAPI y MCP2 son interfaces pares sobre un solo plano de control. Los enlaces siguientes exponen sus contratos de mΓ‘quina.

Overview

Custodial-Monero control plane for plurnk's optional hosted LLM inference.

The REST/JSON contract for plurnk.web β€” a Tor-first, no-JS-first commerce and account control plane for Plurnk's hosted inference service. HTML forms, this OpenAPI surface, and the MCP 2026-07-28 server at /mcp are peer adapters over one shared capability layer; none proxies through another. /v1 remains the separately purchased, OpenAI-compatible inference usage API, and metered search remains under /api/v1/search/*.

Getting started

A REST client, end to end:

Password, TOTP, recovery, and authorization consent never enter MCP elicitation or tool arguments. The existing JSON auth endpoints remain available to deliberate REST consumers.

authorization-code flow with S256 PKCE for resource https://plurnk.ai/api/v1 and scope plurnk:control.

with the returned pct_ control token.

once. Store it on the host that will call /v1 or metered /search.

debits the account balance.

Auth

Three credentials have deliberately disjoint authority:

only credential accepted by password, TOTP, recovery-code, session, security-activity, account-deletion, and operator surfaces.

bound to either the /api/v1 or /mcp resource and scope plurnk:control. It can read account commerce state and manage pk_ usage keys. A token for one resource is rejected by the other.

inference and metered /api/v1/search/*. It cannot read or mutate the account, mint/revoke keys, authorize MCP, or subscribe to account events.

Key lifecycle

A pk_ key has no account-control scopes. It carries usage authority only: inference plus the independently toggleable Search capability. There is no way to promote it into a session or OAuth principal.

The controls are lifecycle, not privilege:

after that window.

re-enabled; a disabled key doesn't authenticate at all.

Sensitive account mutations require a browser session and their documented step-up proof. OAuth control tokens and usage keys are categorically refused.

The money flow

Custodial piconero, double-entry ledger. Amounts are strings of piconero (1 XMR = 1e12 piconero) to stay exact past 2^53.

and can include an exact optional amount_xmr; its response carries the canonical monero: payment URI. The service cannot spend the caller's external wallet. The scanner credits an observed payment after confirmations (pending β†’ credited β†’ settled).

balance (priced in piconero) β€” there is no public spend call; usage is reported by the plurnk endpoint and lands in the ledger.

Β§6). The hot wallet is watch-only β€” no spend key exists on this host, so no call can move funds out.

Event stream

Connect a WebSocket to /ws with either a same-origin browser session or an /api/v1-bound pct_ control token in the Authorization header. Query credentials and pk_ usage keys never authorize private events. Every connection gets a { "type": "hello" } greeting; an authenticated one then receives its OWN account's events as { type, at, data } JSON messages (amounts are decimal piconero strings):

target_kind/target_id, source_ref, block_height).

tier: credited = spendable, settled = fully confirmed).

amount_piconero, source_ref β€” the usage report's id).

The type vocabulary is append-only, like error_key β€” tolerate unknown types. Delivery is pull-only and best-effort notification (a missed event changes nothing in the ledger; reconcile with the REST reads). There are no outbound webhooks β€” the platform makes no outbound product requests.

CORS

/api/*, OAuth discovery/token/registration, protected-resource metadata, /mcp, and this spec's own URLs answer any origin where the protocol needs it. Never credentials mode: session cookies stay same-origin; cross-origin control uses OAuth bearer tokens. HTML pages remain outside the embedding surface.

Versioning

Within /api/v1, changes are additive only β€” new endpoints, new optional fields, new values on the append-only vocabularies (error_key, the event types). Build clients that tolerate unknown fields and unknown enum values. Anything removed or reshaped ships as /api/v2 served alongside /v1 for an announced overlap window; retired identifiers are never reused.

Moderation

Operator bans come in tiers (ban_type): timed (locked out until expiry, content stays), permanent (locked out + content hidden from public reads), erasure (irreversible removal, == self-deletion). A ban revokes the user's sessions + keys immediately.

Content negotiation

/signup, /signin, /signin/recover, /signout are content-negotiated:

to the form with ?e=ERROR_KEY)

Error shape

Every JSON error is { error_key: string, fields?: string[] }. The error_key is a stable machine-readable identifier (the full vocabulary is enumerated on the ErrorResponse schema below); the locale files provide human text. fields names the inputs at fault on validation errors.

Rate limiting

Report filing is per-IP-bucket rate-limited; when tripped, 429 with { error_key: "error_rate_limited" } and a Retry-After header. IPs are hashed with a daily-rotating salt (SPEC Β§2) β€” no raw IP is persisted.

Endpoints

auth

POST /api/v1/auth/signup

Create a new account

Creates the user, issues 8 single-use recovery codes (returned in the JSON path; flashed to session and retrieved separately in the form path), signs the new user in immediately.

Request body (required)
application/json
SignupRequest
application/x-www-form-urlencoded
SignupRequest
Responses
200 β€” Account created. JSON path returns the 8 single-use recovery codes inline so a scripted signup is a single round-trip; the same flash is consumed by the form path via the redirect β†’ /signup/codes β†’ /api/v1/auth/codes chain.

application/json β€” SignupSuccessResponse

303 β€” Account created (form path) β†’ redirect to /signup/codes
400 β€” Missing fields, mismatched passwords, or invalid username

application/json β€” ErrorResponse

409 β€” Username taken (or reserved)

application/json β€” ErrorResponse

POST /api/v1/auth/signin

Sign in to an existing account

Request body (required)
application/json
SigninRequest
application/x-www-form-urlencoded
SigninRequest
Responses
200 β€” Signed in (JSON path)

application/json β€” OkResponse

303 β€” Signed in (form path) β†’ redirect to /account
400 β€” Missing fields

application/json β€” ErrorResponse

401 β€” Wrong username or password

application/json β€” ErrorResponse

403 β€” Account banned

application/json β€” ErrorResponse

POST /api/v1/auth/recover

Recover account using a single-use recovery code

Consumes one of the 8 recovery codes issued at signup; sets a new password and signs the user in. Code is single-use; remaining codes stay valid.

Request body (required)
application/json
RecoverRequest
application/x-www-form-urlencoded
RecoverRequest
Responses
200 β€” Recovered + signed in

application/json β€” OkResponse

303 β€” Form-path success β†’ redirect to /account
400 β€” Missing fields or mismatched passwords

application/json β€” ErrorResponse

401 β€” Wrong username or code

application/json β€” ErrorResponse

403 β€” Account banned

application/json β€” ErrorResponse

POST /api/v1/auth/signin/totp

Complete signin with a TOTP code

Second step of a two-factor signin. POST /signin sets a partial session (pendingTotpUserId) when the user has TOTP enrolled; this endpoint takes the 6-digit code from the authenticator and promotes the partial session to a fully signed-in one. Recovery codes bypass this β€” /signin/recover clears totp_secret on success, so a user who lost their authenticator escapes via the recovery flow rather than this one.

Request body (required)
application/json
SigninTotpRequest
application/x-www-form-urlencoded
SigninTotpRequest
Responses
200 β€” Signed in

application/json β€” OkResponse

303 β€” Signed in (form path) β†’ 303 to /account
400 β€” Code missing

application/json β€” ErrorResponse

401 β€” No pending TOTP signin, or code invalid

application/json β€” ErrorResponse

403 β€” Account banned

application/json β€” ErrorResponse

POST /api/v1/auth/signout

Destroy the current session

Security: cookieAuth

Responses
200 β€” Signed out (JSON path)

application/json β€” OkResponse

303 β€” Signed out (form path) β†’ redirect to /

GET /api/v1/auth/codes

Retrieve the post-signup recovery codes (one-shot flash)

Returns the 8 recovery codes generated at signup, then clears them from the session. Subsequent calls return 404. Only useful in the narrow window between a successful /signup and the user closing the tab.

Security: cookieAuth

Responses
200 β€” Codes available

application/json β€” CodesResponse

404 β€” No codes in the session flash (already retrieved, or never signed up in this session)

application/json β€” ErrorResponse

GET /api/v1/auth/keys

List the authenticated user's active keys

Lists active (un-revoked) keys with metadata only β€” the plaintext key is never re-derivable. Use DELETE to revoke. Paginated, newest first, 25 per page.

Security: cookieAuth or controlTokenAuth

Parameters
page β€” query, string
string, pattern ^[1-9][0-9]*$
Responses
200 β€” A page of keys

application/json β€” TokenListResponse

401 β€” Not signed in

application/json β€” ErrorResponse

POST /api/v1/auth/keys

Issue an inference/search usage key

Generates a pk_<64-hex> usage key bound to the authenticated user. The plaintext key is returned exactly once β€” in this response (JSON path), or in the one-shot flash on the keys page (form path). Only its sha256 hash is persisted. Pass the key only to /v1 inference or metered /api/v1/search/*; it is categorically rejected as account-control or MCP authentication.

Security: cookieAuth or controlTokenAuth

Request body
application/json
KeyCreateRequest
application/x-www-form-urlencoded
KeyCreateFormRequest
Responses
201 β€” Token issued (JSON path)

application/json β€” KeyCreateResponse

303 β€” Form path β†’ "/account/keys" β€” success renders the plaintext exactly once via a session flash; failure carries `?e=error_key_label_required` / `?e=error_key_expiry_invalid`.
400 β€” Missing label or invalid expiry (error_key_label_required / error_key_expiry_invalid)

application/json β€” ErrorResponse

401 β€” Not signed in

application/json β€” ErrorResponse

DELETE /api/v1/auth/keys/{id}

Revoke a usage key

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Responses
200 β€” Token revoked

application/json β€” OkResponse

401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/auth/keys/{id}/delete

Revoke a usage key (no-JS form alias of DELETE)

Same effect as DELETE /api/v1/auth/keys/{id} β€” exposed as a POST so the no-JS keys page can revoke from a native form.

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Token revoked (JSON path)

application/json β€” OkResponse

303 β€” Form path β†’ "/account/keys" (failure carries ?e=error_not_found)
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/auth/keys/{id}/disable

Disable your own usage key (user axis)

Sets the USER disable axis on the caller's own key. A key is live only when all three axes (user/system/operator) are enabled, so this disables but cannot override a system/operator disable. Reversible via /enable.

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Token disabled (JSON path)

application/json β€” OkResponse

303 β€” Form path β†’ "/account/keys"
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/auth/keys/{id}/enable

Re-enable your own usage key (user axis)

Clears the USER disable axis on the caller's own key. The key becomes live only if the system and operator axes are also enabled.

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Token enabled (JSON path)

application/json β€” OkResponse

303 β€” Form path β†’ "/account/keys"
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/auth/keys/{id}/search/enable

Allow web search for your own key (Search capability)

Sets the Search capability flag on the caller's own key, permitting it to spend on the metered web-search path. Orthogonal to the disable axes and the money path - it never affects authentication or inference spend.

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Search allowed (JSON path)

application/json β€” OkResponse

303 β€” Form path -> "/account/keys"
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/auth/keys/{id}/search/disable

Disallow web search for your own key (Search capability)

Clears the Search capability flag on the caller's own key. The key keeps inference usage authority; it just can't reach the metered search path. Reversible via /search/enable.

Security: cookieAuth or controlTokenAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Search disallowed (JSON path)

application/json β€” OkResponse

303 β€” Form path -> "/account/keys"
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active key with that id belongs to the caller

application/json β€” ErrorResponse

POST /api/v1/search/authorize

Authorize-and-spend for one web search (metered)

A search-side filter pings this once per search with the user's usage key. Verifies the key is live and Search-enabled, then debits the endpoint-published search price from spendable balance (atomic, idempotent on request_id). Off (503) when the search rate lane is offline.

Security: usageKeyAuth

Request body (required)
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Authorized and charged (or an idempotent replay)

application/json β€” object

400 β€” Missing request_id

application/json β€” ErrorResponse

401 β€” Invalid or inactive key

application/json β€” ErrorResponse

402 β€” Insufficient spendable balance

application/json β€” ErrorResponse

403 β€” Key lacks the Search capability

application/json β€” ErrorResponse

503 β€” Search rate lane offline

application/json β€” ErrorResponse

POST /api/v1/search/void

Reverse a search charge the endpoint couldn't fulfil

Called when a charged search fails downstream (results undeliverable). Idempotently reverses the charge β€” credits the customer, debits the operator β€” scoped to the charge's owner. A re-void is a no-op (replayed).

Security: usageKeyAuth

Request body (required)
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Reversed (or an idempotent replay)

application/json β€” object

400 β€” Missing request_id

application/json β€” ErrorResponse

401 β€” Invalid or inactive key

application/json β€” ErrorResponse

404 β€” No matching charge for this account

application/json β€” ErrorResponse

operator

POST /api/v1/account/operator/keys/{id}/disable

Operator-disable any user's usage key (operator axis)

Sets the OPERATOR disable axis on any user's key (abuse intervention). Gated by operator session; non-operators get 404.

Security: cookieAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Token operator-disabled

application/json β€” OkResponse

404 β€” Not an operator, or no active key with that id

application/json β€” ErrorResponse

POST /api/v1/account/operator/keys/{id}/enable

Operator-enable any user's usage key (operator axis)

Clears the OPERATOR disable axis on any user's key. Gated by operator session; non-operators get 404.

Security: cookieAuth

Parameters
id β€” path, required, integer
integer
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Token operator-enabled

application/json β€” OkResponse

404 β€” Not an operator, or no active key with that id

application/json β€” ErrorResponse

GET /api/v1/account/operator/reports

The content-report triage queue (operator)

Open reports (SPEC Β§7), priority-ordered β€” subjects with more open reports float to the top, ties break newest-first. Each row carries the repeat-count signal. Paginated.

Security: cookieAuth

Parameters
page β€” query, string
string
Responses
200 β€” Open report queue

application/json β€” object

404 β€” Not an operator

application/json β€” ErrorResponse

GET /api/v1/account/operator/reports/{id}

A single report with evidence + pattern stats (operator)

Security: cookieAuth

Parameters
id β€” path, required, string
string
Responses
200 β€” The report, its evidence snapshot, and subject-pattern stats

application/json β€” object

404 β€” Not found / not operator

application/json β€” ErrorResponse

POST /api/v1/account/operator/reports/{id}/resolve

Resolve a report β€” record the decision, enforce the side-effect (operator)

Records the operator's triage decision on the report and applies its side-effect: dismiss/warn record only; ban bans the reported user. Enforcement runs before the close, so a self-ban or a decision that can't apply to the subject aborts without resolving. Idempotent β€” a second resolve 409s.

Security: cookieAuth

Parameters
id β€” path, required, string
string
Request body (required)
application/json
ResolveReportRequest
application/x-www-form-urlencoded
ResolveReportRequest
Responses
200 β€” Resolved

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/reports
400 β€” Invalid decision / reason missing or too long

application/json β€” ErrorResponse

404 β€” Not found / not operator

application/json β€” ErrorResponse

409 β€” Already resolved / cannot ban self

application/json β€” ErrorResponse

GET /api/v1/account/operator/support/tickets

List support tickets (operator)

Security: cookieAuth

Parameters
status β€” query, string
string
category β€” query, string
string
page β€” query, string
string
Responses
200 β€” Ticket list

application/json β€” object

404 β€” Not an operator

application/json β€” ErrorResponse

GET /api/v1/account/operator/support/tickets/{id}

A single support ticket (operator)

Security: cookieAuth

Parameters
id β€” path, required, string
string
Responses
200 β€” The ticket

application/json β€” object

404 β€” Not found / not operator

application/json β€” ErrorResponse

POST /api/v1/account/operator/support/tickets/{id}/resolve

Resolve a support ticket (operator)

Security: cookieAuth

Parameters
id β€” path, required, string
string
Request body
application/json
ResolveTicketRequest
application/x-www-form-urlencoded
ResolveTicketRequest
Responses
200 β€” Resolved

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/tickets
400 β€” Notes missing/too long

application/json β€” ErrorResponse

404 β€” Not found / not operator

application/json β€” ErrorResponse

409 β€” Already resolved

application/json β€” ErrorResponse

GET /api/v1/account/operator/moderation/actions

The moderation audit log (operator)

Security: cookieAuth

Parameters
target_kind β€” query, string
string
target_id β€” query, string
string
page β€” query, string
string
Responses
200 β€” Recent moderation actions

application/json β€” object

404 β€” Not an operator

application/json β€” ErrorResponse

POST /api/v1/account/operator/moderate

No-JS moderation console dispatch (ban/unban by body)

One body-keyed endpoint behind the operator moderation console's no-JS forms. The REST routes put the target user in the path, which a native form can't fill from a typed input; this dispatches on action (ban/unban by target_username) to the same lookup, mutation, and audit the REST routes use. JSON clients should prefer the REST routes; this exists for the no-JS console.

Security: cookieAuth

Request body (required)
application/json
ModerateRequest
application/x-www-form-urlencoded
ModerateRequest
Responses
200 β€” Action applied (JSON path)

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/moderation
400 β€” Invalid action/kind or reason missing

application/json β€” ErrorResponse

404 β€” Target not found / not operator

application/json β€” ErrorResponse

409 β€” Already in the requested state, or self-ban

application/json β€” ErrorResponse

POST /api/v1/account/operator/users/{user}/ban

Ban a user account (moderation)

Security: cookieAuth

Parameters
user β€” path, required, string
string
Request body
application/json
ModerationActionRequest
application/x-www-form-urlencoded
ModerationActionRequest
Responses
200 β€” Banned

application/json β€” OkResponse

303 β€” Form path: 303 redirect
400 β€” Reason required

application/json β€” ErrorResponse

404 β€” Not found / not operator

application/json β€” ErrorResponse

409 β€” Already banned, or banning yourself

application/json β€” ErrorResponse

POST /api/v1/account/operator/users/{user}/unban

Unban a user account (moderation)

Security: cookieAuth

Parameters
user β€” path, required, string
string
Request body
application/json
ModerationActionRequest
application/x-www-form-urlencoded
ModerationActionRequest
Responses
200 β€” Unbanned

application/json β€” OkResponse

303 β€” Form path: 303 redirect
400 β€” Reason required

application/json β€” ErrorResponse

404 β€” Not found / not operator

application/json β€” ErrorResponse

409 β€” Not banned

application/json β€” ErrorResponse

POST /api/v1/account/operator/janitor/sweep

Run the 30-day account janitor sweep (Β§spec/account-lifecycle)

Sweeps accounts older than the age window whose lifetime balance high-water never reached the reserve minimum (and that aren't the operator): each account's dust is forfeited to the house as an adjustment (conservation preserved) and its username is freed. This is the invocable surface β€” plurnk-build owns the schedule and calls this on its own timer. Idempotent in effect (swept accounts are tombstoned and drop out of the candidate set). No body.

Security: cookieAuth

Responses
200 β€” Sweep complete β€” returns the swept accounts and forfeited amounts

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/janitor
404 β€” Not operator

application/json β€” ErrorResponse

POST /api/v1/account/operator/deposits/{id}/credit

Credit an unattributed deposit to a user (Β§spec/account-lifecycle)

Attributes a deposit that arrived on our address outside any lease window to a username. Uses the scanner's own txid:addressIndex source_ref, so the chain-idempotency index makes a double-credit impossible even against a racing scan. Single-shot: a replay (or a row already resolved) yields 404. username must resolve to an existing account.

Security: cookieAuth

Parameters
id β€” path, required, string
string
Request body
application/json
DepositCreditRequest
application/x-www-form-urlencoded
DepositCreditRequest
Responses
200 β€” Credited

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/deposits
404 β€” Not found / already resolved / unknown user / not operator

application/json β€” ErrorResponse

POST /api/v1/account/operator/deposits/{id}/dismiss

Dismiss an unattributed deposit with a mandatory note (Β§spec/account-lifecycle)

Resolves an unattributed deposit without crediting anyone (e.g. returned out-of-band). The note is recorded on the resolution. Single-shot: a replay (or a row already resolved) yields 404.

Security: cookieAuth

Parameters
id β€” path, required, string
string
Request body
application/json
ModerationActionRequest
application/x-www-form-urlencoded
ModerationActionRequest
Responses
200 β€” Dismissed

application/json β€” OkResponse

303 β€” Form path: 303 to /account/operator/deposits
400 β€” Note required

application/json β€” ErrorResponse

404 β€” Not found / already resolved / not operator

application/json β€” ErrorResponse

account

GET /api/v1/account/deposit

The current deposit surface β€” the ACTIVE lease (a pure read)

Reads the active deposit lease (or a legacy permanent address on pre-lease accounts). Never allocates β€” POST /api/v1/account/deposit-intent to lease an address. 404 when nothing is live.

Security: cookieAuth or controlTokenAuth

Parameters
amount_xmr β€” query

Optional exact amount to encode in the returned Monero payment URI.

XmrAmount
Responses
200 β€” The active lease (expires_at/reserved_until null on a legacy address)

application/json β€” DepositAddressResponse

401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No active lease β€” POST /api/v1/account/deposit-intent first

application/json β€” ErrorResponse

GET /api/v1/account/ledger

The signed-in user's ledger entries (account statement)

Paginated, newest first. Each entry's amount_piconero is signed β€” debits (inference spend) negative, credits (deposits) positive. tier is the confirmation status computed on read against the chain height (CRYPTO Β§5): chain deposits climb pending β†’ credited β†’ settled; internal moves are final; null when the daemon is unreachable. Operator fee rows are not the user's and never appear.

Security: cookieAuth or controlTokenAuth

Parameters
page β€” query, string
string, pattern ^[1-9][0-9]*$
type β€” query, string
string
key β€” query, string
string
from β€” query, string
string
to β€” query, string
string
sort β€” query, string
string
dir β€” query, string
string
Responses
200 β€” A page of ledger entries

application/json β€” LedgerResponse

401 β€” Not signed in

application/json β€” ErrorResponse

POST /api/v1/account/deposit-intent

Lease a deposit address (active ~1 hour, then retired)

Deposit addresses are LEASES, not permanent identities. The response's address is yours until expires_at (~1 hour) β€” pay within the window. A deposit broadcast inside the window is credited to you even if it mines later (a quiet reserve outlasts the mempool's lifetime). Do NOT save or reuse the address: after the reserve it recycles, and a payment to a stale address lands in an operator queue instead of your balance. Calling again inside the active window returns the SAME lease. An optional exact XMR amount changes only the returned payment URI; this call never spends an external wallet.

Security: cookieAuth or controlTokenAuth

Request body
application/json
DepositIntentRequest
application/x-www-form-urlencoded
DepositIntentFormRequest
Responses
200 β€” The active lease

application/json β€” DepositAddressResponse

303 β€” Form path redirects to `/account`, preserving a normalized non-secret amount in the query when present.
400 β€” amount_xmr is not positive exact XMR with at most 12 decimal places

application/json β€” ErrorResponse

401 β€” Not signed in

application/json β€” ErrorResponse

503 β€” No address can be issued right now (pool empty + wallet unreachable) β€” try again shortly

application/json β€” ErrorResponse

GET /api/v1/account/ledger/summary

Exact signed pΙ± nets grouped by key, day, or entry type

The reporting engine's summary: signed piconero nets (credits positive, debits negative β€” an inference settle refund nets against its reserve) grouped by group = key | day | type (default type), over the same filter set as the statement (type, key, from, to). entries counts collapsed statement lines β€” turns, not raw double-entry rows. "How much did Key X spend last week?" is ?group=key&key=X&from=...&to=... β€” one call, exact to the piconero.

Security: cookieAuth or controlTokenAuth

Parameters
group β€” query, string
string
type β€” query, string
string
key β€” query, string
string
from β€” query, string
string
to β€” query, string
string
Responses
200 β€” The grouped summary

application/json β€” LedgerSummaryResponse

401 β€” Not signed in

application/json β€” ErrorResponse

GET /api/v1/account/turns/{holdId}

Analyze one metered turn β€” the charge record + the turn content (Β§spec/inference-link)

The Analyze view behind every inference line on the statement (its hold_id). hold is web's OWN ledger record for the turn β€” the authoritative money facts; the endpoint's copy of the charge is never served. turn is the content (messages in, emission out, usage), fetched on view from the endpoint over the private bank link; it is null when that link is down or the turn has aged out of the endpoint's ~30-day hot content window β€” the charge facts above it are permanent. Ownership is an existence-leak 404: another user's hold id reads as not-found.

Security: cookieAuth or controlTokenAuth

Parameters
holdId β€” path, required, string
string, pattern ^[1-9][0-9]*$
Responses
200 β€” The turn view

application/json β€” TurnViewResponse

401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” Unknown hold / not yours

application/json β€” ErrorResponse

POST /api/v1/account/password

Change the signed-in user's password

Requires current-password reauth + new password + confirmation. On success, every other session for this user is terminated; the caller's own session is preserved so they aren't signed out from the device they're rotating from. Usage keys are NOT automatically revoked β€” they're managed independently via /api/v1/auth/keys.

Security: cookieAuth

Request body (required)
application/json
ChangePasswordRequest
application/x-www-form-urlencoded
ChangePasswordRequest
Responses
200 β€” Password changed (JSON path)

application/json β€” OkResponse

303 β€” Password changed (form path) β†’ redirect to /account/security
400 β€” Missing or mismatched fields

application/json β€” ErrorResponse

401 β€” Not signed in, or current-password reauth failed

application/json β€” ErrorResponse

429 β€” Reauth throttled β€” too many password attempts for this account (Retry-After set; SECURITY Β§6)

application/json β€” ErrorResponse

GET /api/v1/account/sessions

List active sessions for the signed-in user

Returns one entry per non-expired session bound to this user. Each carries a short fingerprint (sha256 of the sid, truncated) for display and as the revoke target. The full sid is never exposed to the client. current: true marks the session this request rode in on.

Security: cookieAuth

Responses
200 β€” Session list

application/json β€” SessionListResponse

401 β€” Not signed in

application/json β€” ErrorResponse

POST /api/v1/account/sessions/revoke-others

Sign out everywhere else (revoke all sessions except the caller's)

Terminates every session for this user EXCEPT the caller's own β€” they aren't signed out from the device they're managing their security on.

Security: cookieAuth

Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Others revoked (JSON path)

application/json β€” OkResponse

303 β€” Others revoked (form path) β†’ 303 to "/account/security/sessions"
401 β€” Not signed in

application/json β€” ErrorResponse

POST /api/v1/account/sessions/{fingerprint}/delete

Revoke a session by fingerprint (no-JS form alias of DELETE)

Same effect as DELETE /api/v1/account/sessions/{fingerprint} β€” exposed as a POST so the no-JS sessions page can revoke from a native form. Revoking the caller's own current session destroys the cookie and (form path) redirects to "/".

Security: cookieAuth

Parameters
fingerprint β€” path, required, string
string
Request body
application/json
object
application/x-www-form-urlencoded
object
Responses
200 β€” Revoked (JSON path)

application/json β€” SessionRevokeResponse

303 β€” Revoked (form path) β†’ 303 to "/account/security/sessions" (or "/" on self-revoke)
401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No matching session for the caller

application/json β€” ErrorResponse

DELETE /api/v1/account/sessions/{fingerprint}

Revoke a specific session by fingerprint

Deletes the session identified by fingerprint. The user_id guard on the server-side query prevents cross-user revocation. If the user revokes their own current session, the response carries self: true and the session cookie is destroyed in the same response β€” the page redirects to /signin.

Security: cookieAuth

Parameters
fingerprint β€” path, required, string
string
Responses
200 β€” Revoked

application/json β€” SessionRevokeResponse

401 β€” Not signed in

application/json β€” ErrorResponse

404 β€” No matching session for the caller

application/json β€” ErrorResponse

GET /api/v1/account/activity

User-facing security activity log (paginated, newest first)

Lists auth-lifecycle events on this account so the user can notice unauthorized changes. Each event has a stable event_type enum, a UTC timestamp, a transport ("clearnet" | "onion"), and an optional detail blob with event-specific JSON (e.g. key name on key_issued). Per SPEC Β§2 no raw IP or User-Agent is ever logged. Paginated, newest first, 25 per page.

Security: cookieAuth

Parameters
page β€” query, string
string, pattern ^[1-9][0-9]*$
Responses
200 β€” A page of the activity log

application/json β€” AuthActivityResponse

401 β€” Not signed in

application/json β€” ErrorResponse

POST /api/v1/account/delete

Delete the signed-in account

Deletes the signed-in account after password reauth. Any remaining balance is FORFEITED to the house per the Terms (an adjustment pair β€” the response reports the exact forfeited_piconero; CRYPTO Β§9.5). Deletion is a tombstone (SPEC Β§3): the public identity is severed and the user's content is removed from the live site, while the rows stay intact internally. Sessions, OAuth control credentials, usage keys, and recovery codes are dropped and the live session is destroyed in the same response.

Security: cookieAuth

Request body (required)
application/json
DeleteAccountRequest
application/x-www-form-urlencoded
DeleteAccountRequest
Responses
200 β€” Account deleted (JSON path)

application/json β€” OkResponse

303 β€” Account deleted (form path) β†’ 303 to "/"
400 β€” Password missing

application/json β€” ErrorResponse

401 β€” Not signed in, or password reauth failed

application/json β€” ErrorResponse

429 β€” Reauth throttled β€” too many password attempts for this account (Retry-After set; SECURITY Β§6)

application/json β€” ErrorResponse

POST /api/v1/account/totp/enroll

Begin TOTP enrollment (step 1 of 2)

Generates a fresh base32 secret and returns it along with the otpauth:// URI an authenticator app reads. The secret is stashed in the session as pendingTotpSecret β€” it is NOT written to the user row until the follow-up confirm call succeeds, so an abandoned enrollment never strands the user.

Security: cookieAuth

Request body (required)
application/json
TotpEnrollRequest
application/x-www-form-urlencoded
TotpEnrollRequest
Responses
200 β€” Enrollment started (JSON path)

application/json β€” TotpEnrollResponse

303 β€” Enrollment started (form path) β†’ 303 to "/account/security" (pending state renders the secret)
400 β€” Password missing

application/json β€” ErrorResponse

401 β€” Not signed in, or password reauth failed

application/json β€” ErrorResponse

409 β€” Already enrolled

application/json β€” ErrorResponse

429 β€” Reauth throttled β€” too many password attempts for this account (Retry-After set; SECURITY Β§6)

application/json β€” ErrorResponse

POST /api/v1/account/totp/confirm

Confirm TOTP enrollment (step 2 of 2)

Verifies the code generated by the user's authenticator against the pending session secret. On success, promotes the pending secret to users.totp_secret and clears the pending slot.

Security: cookieAuth

Request body (required)
application/json
TotpCodeRequest
application/x-www-form-urlencoded
TotpCodeRequest
Responses
200 β€” Enrollment confirmed (JSON path)

application/json β€” OkResponse

303 β€” Enrollment confirmed (form path) β†’ 303 to "/account/security" (active state)
400 β€” Code missing

application/json β€” ErrorResponse

401 β€” Not signed in, or code invalid

application/json β€” ErrorResponse

409 β€” No pending enrollment (run /enroll first)

application/json β€” ErrorResponse

DELETE /api/v1/account/totp

Disable TOTP for the signed-in account

Requires BOTH password AND a current TOTP code. Disabling 2FA from an active session is the attacker's preferred next step; gating on the live authenticator ensures the attacker would need both the password and the device.

Security: cookieAuth

Request body (required)
application/json
TotpDisableRequest
Responses
200 β€” Disabled

application/json β€” OkResponse

400 β€” Password or code missing

application/json β€” ErrorResponse

401 β€” Not signed in, password reauth failed, or code invalid

application/json β€” ErrorResponse

409 β€” Not currently enrolled

application/json β€” ErrorResponse

429 β€” Reauth throttled β€” too many password attempts for this account (Retry-After set; SECURITY Β§6)

application/json β€” ErrorResponse

POST /api/v1/account/totp/disable

Disable TOTP (no-JS form alias of DELETE /api/v1/account/totp)

Same effect and same dual-credential gate (password AND a current TOTP code) as DELETE /api/v1/account/totp β€” exposed as a POST so the no-JS security page can reach it from a native form. JSON clients should prefer the DELETE verb.

Security: cookieAuth

Request body (required)
application/json
TotpDisableRequest
application/x-www-form-urlencoded
TotpDisableRequest
Responses
200 β€” Disabled (JSON path)

application/json β€” OkResponse

303 β€” Disabled (form path) β†’ 303 to "/account/security" (initial state)
400 β€” Password or code missing

application/json β€” ErrorResponse

401 β€” Not signed in, password reauth failed, or code invalid

application/json β€” ErrorResponse

409 β€” Not currently enrolled

application/json β€” ErrorResponse

429 β€” Reauth throttled β€” too many password attempts for this account (Retry-After set; SECURITY Β§6)

application/json β€” ErrorResponse

health

GET /api/v1/rates

The live pricing table as a plain GET (Β§spec/event-stream)

The same snapshot the /ws rates frame pushes β€” one entry per SERVICE (endpoint configuration) with the four token-category prices in exact piconero, plus marketplace metrics (context_tokens, tq, conformance_pct, latency_p50/p95_ms) when the endpoint has measured them. Agents poll this; browsers get the push twin. Fail-closed: 503 when no live snapshot exists β€” never a stale or fabricated price.

Responses
200 β€” The live snapshot

application/json β€” object

503 β€” No live snapshot (endpoint link down or no valid rates yet)

application/json β€” ErrorResponse

GET /api/v1/health

Public liveness probe β€” server + db only

Responses
200 β€” Liveness. Operational detail (wallet/daemon/pool/read-only state) is operator-only β€” see /api/v1/account/operator/health.

application/json β€” HealthResponse

GET /api/v1/account/operator/health

Operator-only operational detail (wallet, daemon, pool depth, read-only state)

Behind operator auth. Carries internal state deliberately kept off the public probe (e.g. pool depth an attacker might try to drain). Non-operators get 404 (existence-leak posture).

Security: cookieAuth

Responses
200 β€” Full subsystem detail.

application/json β€” OperatorHealthResponse

404 β€” Not an operator (existence-leak posture).

application/json β€” ErrorResponse

reports

POST /api/v1/reports

Report a user account (anonymous-capable; rate-limited)

Files a conduct report against a user account (SPEC Β§7). The server resolves the subject, captures an immutable evidence snapshot, and records the reporter context (salted-IP bucket β€” never the raw IP β€” transport, and the account id when signed in). Per-bucket rate-limited. Reporter identity is never exposed to the reported party.

Security: cookieAuth

Request body (required)
application/json
ReportRequest
application/x-www-form-urlencoded
ReportRequest
Responses
201 β€” Report filed (JSON path) β€” returns the report id

application/json β€” object

303 β€” Form path: 303 back to the subject with ?reported=1
400 β€” Invalid subject or category, or notes too long

application/json β€” ErrorResponse

404 β€” The reported subject does not exist

application/json β€” ErrorResponse

support

POST /api/v1/support/tickets

Submit a support ticket (anonymous-capable; rate-limited)

Request body
application/json
SupportTicketRequest
application/x-www-form-urlencoded
SupportTicketRequest
Responses
200 β€” Submitted

application/json β€” OkResponse

303 β€” Form path: 303 to /contact?submitted=1
400 β€” Validation failed

application/json β€” ErrorResponse

Authentication

cookieAuth

Type
apiKey
In
cookie
Name
pk.sid

HttpOnly session cookie issued by /signup / /signin / /signin/recover. Browser path.

controlTokenAuth

Type
oauth2

OAuth 2.1 authorization-code token with S256 PKCE, scope plurnk:control, and exact resource audience /api/v1. Control tokens begin pct_, expire after one hour, and are not usage credentials.

usageKeyAuth

Type
http
Scheme
bearer
Bearer format
pk_<hex>

Usage key issued by POST /api/v1/auth/keys. Pass as Authorization: Bearer pk_<64-hex> only to /v1 inference or /api/v1/search/*. It has no account-control, MCP, session, or event authority. It may have an optional expiry and can be disabled or revoked.

Schemas

DepositCreditRequest

Credit an unattributed deposit to a user. username is NOT marked required here β€” the handler owns the check so an unknown/blank user returns the existence-leak 404, not a generic validation error.

username β€” string
 

ModerationActionRequest

Operator moderation action. reason is mandatory for the audit log but is NOT marked required here β€” the handler owns that check so it returns the specific error_moderation_reason_required, not a generic validation error. related_ticket_id optionally links the action to a support ticket.

reason β€” string
 
related_ticket_id β€” string, pattern ^[1-9][0-9]*$
 
ban_type β€” string

On the ban route: timed | permanent | erasure (SECURITY Β§6.5). The handler validates β†’ error_ban_type_invalid.

ban_duration β€” string

On a timed ban: a duration modifier from PLURNK_WEB_BAN_DURATIONS.

ModerateRequest

No-JS console dispatch body. action is ban | unban (with target_username). The business-required fields are NOT marked required β€” the handler owns validation so it returns specific error_keys. Numeric patterns tolerate an empty value so a blank optional form field doesn't trip the validator.

action β€” string

One of: ban, unban

target_username β€” string
 
ban_type β€” string

On action=ban: timed (lockout, auto-expires) | permanent (lockout) | erasure (tombstone, irreversible). SECURITY Β§6.5. The handler validates β†’ error_ban_type_invalid.

ban_duration β€” string

On a timed ban: a SQLite duration modifier from PLURNK_WEB_BAN_DURATIONS (e.g. '7 days'). Ignored for permanent/erasure.

reason β€” string
 
related_ticket_id β€” string, pattern ^([1-9][0-9]*)?$
 

SupportTicketRequest

A support ticket submission. The handler validates category + body.

category β€” string
 
body β€” string
 
reply_token β€” string
 

ReportRequest

A content report (SPEC Β§7). The handler owns validation so it returns the specific error_keys (invalid subject/category, notes too long). The evidence snapshot, content hash, URL, and reporter context are captured server-side β€” never supplied by the client.

subject_kind β€” string

user (the only supported subject; handler-validated).

subject_id β€” string

The subject's id (form values are strings; handler-validated).

category β€” string

Enum: out_of_scope | spam | harassment | impersonation | copyright | payment_fraud | other (handler-validated).

notes β€” string

Free-form reporter note (handler caps at 4000 chars).

ResolveTicketRequest

Operator ticket resolution. The handler owns the resolution-notes checks.

resolution_notes β€” string
 

ResolveReportRequest

Operator report resolution (SPEC Β§7). The handler owns validation so it returns the specific error_keys; resolution is left loose here (not an enum) so an unsupported value yields error_report_resolution_invalid rather than the generic validator error.

resolution β€” string

One of: dismiss | warn | ban (handler-validated).

reason β€” string

Free-form reason

OkResponse

ok β€” required, boolean
 

SignupSuccessResponse

JSON-path signup response with the 8 single-use recovery codes inline. The codes flash is consumed in this response; subsequent calls to /api/v1/auth/codes return 404.

ok β€” required, boolean
 
codes β€” required, array

Eight 16-character hex single-use recovery codes.

Items: string, pattern ^[0-9a-f]{16}$

key β€” object

Present only when the request carried issue_key true β€” the first usage key, shown exactly once.

ErrorResponse

The shape of every JSON error: { error_key, fields? }. error_key is a stable identifier you can branch on; fields (validation errors) names the offending inputs. The enum below is the exhaustive list; the families a consumer hits most:

Auth & session

  • error_signin_required β€” not authenticated (401).
  • error_signin_bad_creds / error_password_incorrect β€” wrong credentials.
  • error_totp_required / error_totp_code_required / error_totp_invalid β€” 2FA step.
  • error_account_banned β€” the account is banned (permanent, or timed not yet expired).
  • error_rate_limited β€” per-IP bucket limit tripped (429; Retry-After).

Keys

  • error_key_label_required / error_key_expiry_invalid β€” bad key-creation input.

Validation

  • error_validation_failed β€” schema validation failed (fields names them).
  • error_not_found β€” the target doesn't exist (or you can't see it β€” no leak).

Operator

  • error_already_banned / error_not_banned / error_cannot_ban_self,

error_ban_type_invalid / error_ban_duration_invalid β€” ban actions.

error_key β€” required, string

Stable machine-readable identifier. The complete vocabulary (and English text) lives in src/locales/en.json.

One of: error_unknown, error_not_found, error_validation_failed, error_signin_required, error_key_label_required, error_key_expiry_invalid, error_signin_missing_fields, error_signin_bad_creds, error_signup_missing_fields, error_signup_username_invalid, error_signup_username_taken, error_terms_required, error_american_required, error_password_mismatch, error_recover_missing_fields, error_recover_bad_code, error_account_banned, error_codes_unavailable, error_password_required, error_password_incorrect, error_deposit_address_unavailable, error_deposit_amount_invalid, error_rates_unavailable, error_totp_required, error_totp_code_required, error_totp_invalid, error_totp_already_enrolled, error_totp_not_enrolled, error_totp_no_pending, error_rate_limited, error_csrf, error_support_category_invalid, error_support_body_required, error_support_body_too_long, error_support_reply_token_too_long, error_support_resolution_notes_required, error_support_resolution_notes_too_long, error_report_subject_invalid, error_report_category_invalid, error_report_notes_too_long, error_report_resolution_invalid, error_report_resolution_reason_required, error_report_resolution_reason_too_long, error_report_already_resolved, error_support_already_resolved, error_moderation_reason_required, error_moderation_reason_too_long, error_already_banned, error_not_banned, error_cannot_ban_self, error_ban_type_invalid, error_ban_duration_invalid, error_payment_required, error_search_unavailable, error_search_not_enabled, error_search_charge_not_found, error_inference_cap_exceeded, error_invalid_key, error_amount_required

fields β€” array

For validation errors, the input names at fault.

Items: string

SlugResponse

slug β€” required, string
 

SignupRequest

username β€” required, string, pattern ^[A-Za-z0-9_-]{1,32}$
 
issue_key β€” boolean

JSON path only — also mint a first usage key inline (returned once as key), so a deliberate REST client can go zero→usage in one call. The HTML form path never sends this.

accept_terms

Affirmative Terms-of-Service + Privacy-Policy acceptance β€” REQUIRED to create an account (enforced by the handler with error_terms_required so it owns the error_key; not schema-required). JSON clients send true; the HTML form sends "on".

american

Affirmative jurisdiction-eligibility representation (the service is only available to adult American citizens subject to American jurisdiction) β€” REQUIRED to create an account (handler- enforced with error_american_required). JSON clients send true; the HTML form sends "on".

password β€” required, string, format password
 
password_confirm β€” required, string, format password
 

SigninRequest

username β€” required, string
 
password β€” required, string, format password
 

RecoverRequest

username β€” required, string
 
code β€” required, string

One of the 8 single-use recovery codes issued at signup.

password β€” required, string, format password
 
password_confirm β€” required, string, format password
 

CodesResponse

codes β€” required, array

Eight 16-character hex single-use recovery codes.

Items: string, pattern ^[0-9a-f]{16}$

AccountResponse

username β€” required, string
 
has_totp β€” required, boolean

True if the user has an active TOTP enrollment.

balance β€” object, nullable

Balance in piconero (exact decimal strings, Β§13). null when the daemon is unreachable. spendable = internal funds + chain deposits at β‰₯ credited confs.

XmrAmount

Positive exact XMR amount with no more than 12 decimal places. The shared capability layer owns this semantic validation so every adapter returns error_deposit_amount_invalid rather than an adapter-specific schema error.

string

DepositIntentFormRequest

amount_xmr

XmrAmount

_csrf β€” required, string

Same-origin double-submit token rendered into the native form.

DepositAddressResponse

address β€” required, string

The leased Monero subaddress for self-deposit.

expires_at β€” required, string, nullable

Lease expiry (ISO-8601); null on a legacy permanent address.

reserved_until β€” required, string, nullable

Until when a slow-to-mine payment still credits you; null on legacy.

amount_xmr β€” required

Normalized requested amount, or null when the intent is amount-free.

payment_uri β€” required, string, pattern ^monero:

Canonical wallet handoff URI; includes tx_amount exactly when amount_xmr is present.

TurnViewResponse

hold β€” required, object

Web's authoritative charge record for the turn (the ledger's hold).

LedgerResponse

entries β€” required, array

Items: object

page β€” required, integer
 
page_size β€” required, integer
 
total β€” required, integer
 
page_count β€” required, integer
 
filter β€” object

The normalized filter/sort echoed back (server-whitelisted).

LedgerSummaryResponse

group β€” required, string

One of: key, day, type

rows β€” required, array

Items: object

filter β€” required, object
 

ChangePasswordRequest

current_password β€” required, string, format password
 
password β€” required, string, format password
 
password_confirm β€” required, string, format password
 

SessionSummary

fingerprint β€” required, string

Short base64url-encoded sha256 of the sid (12 chars). Stable per session, non-reversible.

created_at β€” required, string
 
last_seen β€” required, string
 
current β€” required, boolean

True if this is the session the request rode in on.

SessionListResponse

sessions β€” required, array

Items: SessionSummary

AuthEvent

id β€” required, integer
 
event_type β€” required, string

One of: signup, signin_password, signin_totp, signout, recover_used, password_changed, totp_enrolled, totp_disabled, key_issued, key_revoked, session_revoked, sessions_revoked_others, control_authorized

occurred_at β€” required, string
 
transport β€” required, string

One of: clearnet, onion,

detail β€” object, nullable

Event-specific JSON; shape varies per event_type.

AuthActivityResponse

events β€” required, array

Items: AuthEvent

page β€” required, integer
 
page_size β€” required, integer
 
total β€” required, integer
 
page_count β€” required, integer
 

SessionRevokeResponse

ok β€” required, boolean
 
self β€” required, boolean

True if the caller revoked their own current session. Page should redirect to /signin.

DeleteAccountRequest

password β€” required, string, format password

The current account password. Required as a reauth even on an active session β€” destructive actions don't ride a stolen cookie.

SigninTotpRequest

code β€” required, string, pattern ^[0-9]{6}$
 

TotpEnrollRequest

password β€” required, string, format password
 

TotpEnrollResponse

secret β€” required, string, pattern ^[A-Z2-7]+$

Base32-encoded TOTP shared secret. Shown to the user once, here, for entry into an authenticator app. The server has already stashed this in the session as pendingTotpSecret β€” it is NOT written to the user row until /confirm succeeds.

otpauth_url β€” required, string

otpauth:// URI containing the secret + issuer + algorithm, for the authenticator app to enroll directly.

TotpCodeRequest

code β€” required, string, pattern ^[0-9]{6}$
 

TotpDisableRequest

password β€” required, string, format password
 
code β€” required, string, pattern ^[0-9]{6}$
 

KeyCreateRequest

name β€” string

Required human-readable label for the key.

expires_in_days β€” integer

Optional hard expiry in days from now (positive integer). Omit for a key that never expires. The handler validates β†’ error_key_expiry_invalid.

search β€” boolean

Whether this usage key may also authorize metered web search.

KeyCreateFormRequest

name β€” string

Required human-readable label for the key.

expires_in_days β€” string

Optional expiry in days (form values are strings). The handler validates β†’ error_key_expiry_invalid.

search β€” string

Optional no-JS form representation of the Search capability.

One of: on, off

KeyCreateResponse

id β€” required, integer
 
key β€” required, string, pattern ^pk_[0-9a-f]{64}$

The plaintext key, returned exactly once. Pass it only to /v1 inference or metered /api/v1/search/*.

name β€” required, string
 
created_at β€” required, string
 

TokenSummary

id β€” required, integer
 
prefix β€” required, string
 
name β€” required, string
 
expires_at β€” required, string, nullable
 
created_at β€” required, string
 
last_used_at β€” required, string, nullable
 
user_disabled_at β€” required, string, nullable
 
system_disabled_at β€” required, string, nullable
 
operator_disabled_at β€” required, string, nullable
 
search_enabled β€” required, integer

One of: 0, 1

TokenListResponse

keys β€” required, array

Items: TokenSummary

page β€” required, integer
 
page_size β€” required, integer
 
total β€” required, integer
 
page_count β€” required, integer
 

HealthResponse

server β€” required, string

Status string for the express process

db β€” required, string

Status string for sqlite

timestamp β€” string

ISO-8601 probe time

OperatorHealthResponse

server β€” required, string

Status string for the express process

db β€” required, string

Status string for sqlite

daemon β€” required, string

Status string for monerod

wallet β€” required, string

Status string for monero-wallet-rpc

writable β€” required, boolean

False = the platform is in maintenance read-only (PLURNK_WEB_FREEZE); every authenticated mutation returns 503.

read_only_reason β€” required, string,null

null when writable; otherwise frozen | wallet_unreachable | daemon_unreachable | wallet_sync_behind (the latter three are reported for visibility but no longer gate globally).

pool_depth β€” required, integer

Unassigned subaddresses remaining in the pool (operator runway gauge).