Public API and realtime gateway
tayho-api is the only application-facing backend. Two signerless Elysia replicas compose three
authorities without duplicating their canonical state:
Ponder owns on-chain projections. The engine owns automatic intents until settlement. Live RPC
reads provide short-lived quotes, unsigned simulations, and liquidation risk. Redis Streams is a
bounded, rebuildable delivery log and cross-replica backplane; it is never an authority. Responses
identify their source with x-tayho-source: onchain|engine|live.
API-key projects, hashed credentials, scopes, restrictions, quotas, and operator audit events live
in the API control tables. The API is the only runtime reader of those credentials; the internal
CLI is the only management interface. Shared admission counters live in Redis and fail closed.
REST contract
The 47 REST endpoints are grouped by responsibility:- Protocol:
GET /v1/configand boundedPOST /v1/batch. - Markets: list/detail, funding, stats, price, oracle, liquidity, events, trades, candles, and live
quote under
/v1/markets. - Positions: detail, event history, settlement history, and live risk under
/v1/positions. - Oracle: canonical funding attestations at
GET /v1/oracle/attestations. - Trader portfolio: positions, sessions, portfolio, balances, exposure, PnL, activity, orders,
trades, funding, settlements, vault activity, rewards, and transfers under
/v1/traders. - Protocol accounting: vault state/events/claims and staking state/events.
- Correlation: indexed order outcome and transaction event bundle.
- Trading: signerless
POST /v1/trades/simulate. - Automatic intents: create, batch create, signed account list, capability-scoped status, and signed
cancellation under
/v1/intents. - Immediate actions: capability-scoped session, add-margin, vault, and staking submission/status
under
/v1/actions.
/openapi; the machine contract is /openapi/json. Every
body, path, and query is validated at the Elysia boundary. JSON is stream-bounded to 512 KiB before
parsing. Decimal identifiers enforce the exact Solidity uint48, uint64, or uint256 maximum.
POST /v1/batch accepts at most 20 unique, explicitly allowlisted on-chain GET paths; it cannot
reach intent, live-RPC, health, metrics, or arbitrary upstream URLs.
WebSocket contract
GET /v1/ws upgrades to tayho.realtime.v1 after a short-lived single-use ticket is issued through
POST /v1/realtime/tickets. It carries server events only; orders and cancellations always use REST
so acknowledgement and retry semantics remain explicit. The public AsyncAPI contract is in the
realtime reference.
Clients send subscribe, unsubscribe, and ping. The server sends hello, snapshot, event,
subscribed, unsubscribed, heartbeat, pong, resync_required, and error. Channels are
strictly allowlisted and canonicalized:
market.{state|price|trades|funding|liquidity|oracle}:<marketId>trader.{positions|portfolio|balances|activity|orders|trades|funding|settlements|sessions|vault|rewards|transfers}:<address>intent:<orderHash>,action:<actionHash>, andtransaction:<txHash>protocol.config,vault.state,staking.state, andsystem.status
eventId and apply domain sequence/version fields
monotonically. Reconnects send resumeFrom per channel. If a replay exceeds the bounded replay
limit, the server sends resync_required and a fresh snapshot. Intent and action channels require
the receipt capability returned at intake; capabilities and encrypted intent terms are not realtime
stream payloads.
Each API replica maintains one blocking stream reader and fans out locally, avoiding one Redis
connection per browser. Fan-out uses an inverted channel-to-connection index, so each event visits
only interested sockets rather than scanning every client. Concurrent public subscriptions to the
same channel coalesce their authoritative snapshot request; capability-protected intent snapshots
remain isolated.
Failure and security boundaries
- Writes are forwarded exactly once. The API never automatically retries POSTs; stable action/order hashes and required idempotency keys converge deliberate caller retries.
- Idempotent reads use at most
API_READ_RETRY_COUNTretries, bounded byAPI_UPSTREAM_TIMEOUT_MS. Live RPC reads use a configured fallback pool and never hold a key. - The private engine requires
ENGINE_INTERNAL_TOKEN, mounted only into API and engine. Intent status snapshots preserve receipt capabilities end to end. - Redis contains derived indexes, public lifecycle projections, and bounded replay data only. Loss degrades realtime delivery; PostgreSQL, Ponder, and chain remain canonical and rebuild it.
- CORS is an exact allowlist. Cloudflare proxies the API domain and enforces coarse WAF/IP limits.
The API also enforces shared Redis-backed per-client REST, WebSocket-connect, and WebSocket-message
budgets across both replicas.
API_TRUSTED_PROXY_HOPSmust match the deployed Railway proxy chain; the engine separately rate-limits signed identities in shared Redis. x-request-idand W3Ctraceparentflow through request, upstream, job, transaction, and receipt paths. Elysia OpenTelemetry exports standard spans; Prometheus covers REST, upstreams, WebSocket, slow consumers, and the indexer event pump./readyzfails closed when engine, indexer, live RPC, API credential store, admission Redis, realtime Redis, or the shared Ponder-pump freshness heartbeat is unavailable./livezreports process health only. API receives its restricted credential-control database role, but no keeper coordination/indexer database role, signer, transaction journal, or intent encryption key.
apps/api/src/app.ts,
apps/api/src/routes, and
apps/api/src/realtime.