Skip to main content

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/config and bounded POST /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.
The interactive OpenAPI document is at /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>, and transaction:<txHash>
  • protocol.config, vault.state, staking.state, and system.status
An initial subscription captures a stream boundary, fetches an authoritative snapshot, then tails from that boundary. This at-least-once handoff can deliberately overlap an event already reflected in the snapshot; clients deduplicate the stable 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_COUNT retries, bounded by API_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_HOPS must match the deployed Railway proxy chain; the engine separately rate-limits signed identities in shared Redis.
  • x-request-id and W3C traceparent flow 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.
  • /readyz fails closed when engine, indexer, live RPC, API credential store, admission Redis, realtime Redis, or the shared Ponder-pump freshness heartbeat is unavailable. /livez reports 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.
Railway may add API replicas without sticky sessions. WebSocket reconnect/resume makes ordinary rolling drains safe; the Redis stream is shared, deduplicated by event ID, and the Ponder pump uses a renewable single-leader lease so two API replicas cannot publish duplicate canonical feeds. Sources: apps/api/src/app.ts, apps/api/src/routes, and apps/api/src/realtime.
Last modified on July 23, 2026