[ REFERENCE ]
REST API
The endpoints the deck itself calls. Everything here is session-authenticated and scoped to one operator — for programmatic access from outside a browser, use the MCP server instead.
Conventions
Every endpoint answers with the same envelope, so a client never has to guess the shape of a failure.
// success
{ "success": true, "data": { … } }
// failure — data is always null, error is always human-readable
{ "success": false, "data": null, "error": "Sign in to the Command Deck first." }- Authentication — the
oddysey_sessioncookie: HMAC-signed,httpOnly, 12-hour life. Missing or invalid gives401. - Scope — every query is bounded by the session address. There is no cross-operator read anywhere in the API.
- Validation — bodies are parsed with schemas at the boundary. A failure returns
400with one readable line per bad field. - Errors — server-side detail is logged, never returned. Clients get a sentence they can show a person.
Deck
| ENDPOINT | PURPOSE |
|---|---|
GET /api/deck | Hydration: watches, proposals, ledger, and freshly priced positions in one round trip. Seeds a first-visit deck. |
GET /api/deck
{
"success": true,
"data": {
"watches": [ … ],
"proposals": [ … ],
"ledger": [ … ],
"positions": [
{ "symbol": "NVDA", "name": "NVIDIA stock token",
"tokens": 8.75, "priceUsd": 184.52, "change24hPct": -4.1 }
],
"market": {
"source": "finnhub", "live": true, "stale": false,
"asOf": 1755689400000
}
}
}Watches
| ENDPOINT | BODY | RETURNS |
|---|---|---|
GET /api/watches | — | Your watches. |
POST /api/watches | symbol, thresholdPct, action, note? | The new watch plus the ledger event recording that you filed it. |
PATCH /api/watches/[id] | — | Toggles active/paused. 404 if the watch is not on your deck. |
POST /api/watches/evaluate | — | Runs the sweep for your watches only, with a per-watch outcome. |
POST /api/watches/evaluate
{
"success": true,
"data": {
"owner": "0x…",
"evaluated": 3,
"triggered": 1,
"outcomes": [
{ "watchId": "…", "symbol": "NVDA", "status": "triggered",
"detail": "Staged accumulation on the NVDA drawdown" },
{ "watchId": "…", "symbol": "AAPL", "status": "held",
"detail": "1.20% vs -3%" },
{ "watchId": "…", "symbol": "TSLA", "status": "cooling" }
]
}
}Proposals
| ENDPOINT | BODY | RETURNS |
|---|---|---|
PATCH /api/proposals/[id] | { "decision": "approve" | "reject" } | The updated proposal and its ledger event. 409 when the proposal is no longer in a state you can change. |
Deck tokens
| ENDPOINT | BODY | RETURNS |
|---|---|---|
GET /api/tokens | — | Token summaries: id, label, created, last used, revoked. Never the token itself. |
POST /api/tokens | label? (≤60 chars) | The new token's plaintext — in this response and nowhere else, ever. |
DELETE /api/tokens | id | Revokes it. 404 if it is not yours or already revoked. |
Authentication
| ENDPOINT | PURPOSE |
|---|---|
GET /api/auth/nonce | Issues a single-use nonce for the SIWE message. |
POST /api/auth/verify | Verifies an EIP-4361 signature against the nonce cookie and the claimed address, then sets the session. |
POST /api/auth/demo | Mints a throwaway demo address and session. |
POST /api/auth/logout | Clears the session cookie. |
Verification is strict on all three counts: the nonce in the signed message must match the nonce cookie, the address line must match the claimed address, and the signature must verify. Any mismatch is 401.
Scheduled evaluation
| ENDPOINT | AUTH | PURPOSE |
|---|---|---|
POST /api/cron/evaluate | x-cron-secret header | The sweep across every operator with active watches. Called by the Worker's scheduled handler. |
Guarded by a shared secret rather than a session, because the caller is the Worker itself and not a browser. The comparison is constant-time. With CRON_SECRET unset the endpoint returns 503 and refuses to run at all rather than defaulting open — and the sweep caps at 25 operators per run, reporting how many were deferred instead of hiding them.
Status codes
| CODE | WHEN |
|---|---|
| 200 | Fine. |
| 400 | The body failed validation. The error names the fields. |
| 401 | No valid session, or a bad cron secret, or an unknown deck token. |
| 404 | The row exists for somebody, but not for you. |
| 409 | A legal object in an illegal state — approving something already decided. |
| 500 | Something broke. Detail is in the server logs, not the response. |
| 503 | A required secret is not configured. |