Status: canonical API index (Phase 2, October 2026).
This is the entry point for API consumers. Detailed per-service documents are linked below; when they disagree with this index, the service document wins for its own routes and this index is updated in the same change.
| Service | Default local base URL | Public target | Detailed docs |
|---|---|---|---|
| Controller (market data/oracle) | http://localhost:3000 |
https://oracle.suban.org |
suban-controller/docs/API.md |
| Data API (Horizon-compatible) | http://localhost:4000 |
https://horizon.suban.org |
../Suban-api/README.md |
| Data platform (indexed history) | http://localhost:3002 |
https://data.suban.org (target; not yet provisioned) |
../services/event-indexer/README.md |
| Bridge relayer (metrics/health only) | http://localhost:9464 |
internal worker | ../docs/OBSERVABILITY.md |
| RPC | http://localhost:8000 |
https://rpc.suban.org |
../../suban-rpc/README.md |
All HTTP APIs return JSON unless noted. Timestamps are ISO-8601 UTC strings. Endpoints marked authenticated require a shared secret and are not part of the public developer surface.
- Public HTTP routes are versioned under
/api/v1(data API and controller). The data platform uses unversioned resource paths (/events,/transactions,/operations,/accounts,/ledgers) and carriesschema_version: "2.0"in every data response. - The controller price payload carries
schema_version(1.0today) andmethod_version(weighted_average_v1). Breaking shape changes bumpschema_version; methodology changes bumpmethod_version. - Adding a field is backward-compatible. Removing/renaming a field is not.
- The data platform (
/events,/transactions,/operations,/accounts,/ledgers) returnsschema_version: "2.0"and afreshnessobject on every data response. Breaking shape changes bumpschema_version; additions do not. - Error responses use
{ "error": "message" }; controller price errors may addassetandavailable_assets. - Unknown assets return
404; known but unconfigured assets return503. Do not treat503as a zero price.
| Surface | Auth | Limits |
|---|---|---|
| Data API public read routes | None | Per-IP rate limit at the edge and in the service |
| Data platform public read routes | x-api-key or Authorization: Bearer <key> when INDEXER_REQUIRE_API_KEY=true |
Per-key minute/day quotas; 429 + Retry-After |
| Data platform admin routes | INDEXER_ADMIN_API_KEY |
Private network; 503 if unset |
| Controller control routes | x-api-key: <key> or Authorization: Bearer <key> |
Per-IP limit, default 30/min |
| Controller oracle publisher | ORACLE_ADMIN_SECRET (server-side only) |
Opt-in; not an HTTP route |
RPC admin API (:8001) |
Private network only | Do not expose |
RPC public JSON-RPC (:8000) |
None | See RPC docs |
Metrics /metrics |
None by default | Should be scraped from a private network |
If CONTROLLER_ADMIN_API_KEY is unset, control routes return 503 rather than
falling open. If the key is wrong, they return 401/403; repeated failures are
rate-limited.
Base: http://localhost:3000. Every price route accepts
?asset=PI|USDC|USDT|OUSD and defaults to PI.
Returns the asset registry with per-asset source counts, trust state, oracle address and last committed price.
{
"schema_version": "1.0",
"assets": [
{
"id": "PI",
"symbol": "PI",
"name": "Pi Network",
"enabled": true,
"oracle_asset": null,
"min_sources_required": 1,
"sources": [{ "name": "mexc", "status": "active" }],
"trust": {
"state": "HEALTHY",
"since": "2026-10-20T12:00:00.000Z",
"reason": "sources and confidence within bounds",
"healthy_streak": 3
},
"last_committed_price": 0.3125
}
]
}Returns the aggregated price with provenance, freshness, confidence and trust.
{
"schema_version": "1.0",
"method_version": "weighted_average_v1",
"symbol": "PI",
"price_usd": 0.3125,
"timestamp": "2026-10-20T12:00:00.000Z",
"age_ms": 0,
"is_fresh": true,
"sources_used": 4,
"total_sources": 4,
"aggregation_method": "weighted_average",
"confidence_score": 0.94,
"cache_hit": false,
"source_prices": {
"mexc": {
"price": 0.312,
"weight": 3,
"timestamp": "2026-10-20T11:59:58.000Z",
"age_ms": 2000,
"is_fresh": true
}
},
"trust": {
"state": "HEALTHY",
"since": "2026-10-20T11:00:00.000Z",
"reason": "sources and confidence within bounds",
"healthy_streak": 12
}
}Failure behavior:
| Case | Status | Body |
|---|---|---|
| Unknown asset | 404 |
{ "error": "Unknown asset", "asset": "DOGE", "available_assets": ["PI","USDC","USDT","OUSD"] } |
| Registered but no sources (e.g. oUSD without feed) | 503 |
{ "error": "Asset is not configured", "asset": "OUSD" } |
| All sources fail / no usable observation | 503 |
{ "error": "No price available", "asset": "PI" } |
| Breaker active | 200 |
last committed price with aggregation_method: "circuit_breaker_fallback" and is_fresh: false |
| Breaker active with no committed reference | 503 |
{ "error": "No price available", "asset": "PI" } |
Never infer a price from a 503 or from a missing source_prices entry. A breaker
fallback is explicitly stale (is_fresh: false) and must not be treated as a
fresh observation.
Query: hours (1–720, default 24), limit (1–10000, default 1000). Returns
observations newest first plus window stats. Breaker observations are stored for
audit but excluded from public stats.
All three paths return the same twap_volatility_v1 calculation from persisted
observations. Query: asset, window (60s-30d; 5m, 24h, 30d or
seconds), and optional as_of for replay. coverage_ratio and warnings
report partial history; an empty window returns 503, never a fabricated
value. Volatility is null with fewer than two returns.
Returns versioned price/TWAP/risk methods and explicit gaps. In particular,
price_quality.liquidity_aware is false; no pool-depth input exists yet.
GET /api/v1/risk/methods publishes formulas, defaults and limitations for
lending_risk_v1.
POST /api/v1/risk/lending accepts:
{
"collateral": [{ "asset": "PI", "amount": 100 }],
"debt": [{ "asset": "USDC", "amount": 5 }],
"liquidation_threshold": 0.8,
"target_health_factor": 1.5
}Prices come from the live asset aggregators. Unknown assets return 404,
unconfigured assets, missing prices and stale prices return 503 without
partial results. A fresh price whose trust state is not HEALTHY is returned
with data_quality.status: "degraded".
The response contains health_factor, collateral_value_usd, debt_value_usd,
max_borrow_usd and liquidation_price_usd_per_collateral_unit (single
collateral only).
GET /api/v1/risk/lending/indexed/:account is an opt-in alternative that reads
the latest indexed lending-pool snapshot from the data platform. Configure
DATA_PLATFORM_URL (and optionally DATA_PLATFORM_API_KEY). Required query
parameters are collateral_decimals and debt_decimals; optional parameters
are collateral_asset (default PI), debt_asset (default USDC) and the
same threshold/target fields. The response labels the source as
indexed_contract_events, includes the source ledger/timestamp, and returns
503 when the data platform is not configured or a price is stale/missing.
Indexed positions can lag the chain; they are not live contract storage.
POST /api/v1/quote/cpmm accepts raw integer token units:
{
"reserve_in": "1000000",
"reserve_out": "1000000",
"amount_in": "1000",
"fee_bps": 30
}The response is versioned as quote_v1 with data_quality.status: "preview"
and includes amount_out_raw, fee_amount_raw, spot/execution price and
price_impact_bps. Reserves are caller-supplied; this is not an execution-time
quote.
GET /api/v1/quote/cpmm/:contractId?token_in=...&amount_in=... uses the latest
enriched pool state from the data platform. It returns the source ledger in
data_quality.as_of and refuses pools without token/reserve/fee metadata. It is
still a preview over indexed state, not a live get_quote simulation.
GET /api/v1/quote/cpmm/:contractId/onchain?token_in=...&amount_in=... calls the
deployed contract's get_quote view through Soroban simulation and compares the
result with the same deterministic formula. It requires
QUOTE_SIMULATION_SOURCE_ACCOUNT (an existing account used only as the
simulation source; nothing is signed or submitted). The response is
data_quality.status: "simulated" and includes on_chain, parity and the
source ledger. A mismatch is returned as status: "mismatch" with a warning.
GET /api/v1/route/cpmm?token_in=...&token_out=...&amount_in=...[&max_hops=2]
discovers the best CPMM route over indexed pools only. It considers pools
that have enriched token/reserve/fee state (at most the first 1,000 pool
summaries), excludes stableswap hops, and returns 404 when no real indexed
route exists. max_hops is 1–3. Every hop
reports its pool, reserves, fee and per-hop integer quote; sources reports each
hop's indexer ledger and observation time. Amounts are raw token units. The
route is a preview over indexed state and can lag the chain.
Suban/sdk/scripts/quote-parity.mjs is the reproducible deployed-parity check:
it reads a pool's tokens/reserves/fee, simulates get_quote in both directions
and exits non-zero on any mismatch. See docs/PHASE4-QUOTE-PREVIEW.md.
Returns the current trust state and transition history. ?limit= bounds history
(1–100, default 50).
States: HEALTHY, DEGRADED, BREAKER, RECOVERY.
Without asset, returns PI sources plus an asset summary for backward
compatibility. With asset, returns only that asset's sources.
Top-level fields describe PI (backward compatible) and assets lists every
asset's trust, source count and last committed price. status is unhealthy
when the PI breaker is active, degraded when PI is not healthy or fewer than
half of its sources are active, otherwise healthy.
| Route | Method | Body | Purpose |
|---|---|---|---|
/api/v1/price/commit |
POST | { "price": number, "asset"?: string } |
Set the breaker reference price |
/api/v1/circuit-breaker/reset |
POST | { "asset"?: string } |
Clear the breaker for one asset |
Prometheus text exposition. Key series:
suban_price_usd{symbol},suban_price_confidence{symbol},suban_price_age_ms{symbol}suban_source_up{asset,source},suban_source_staleness_ms{asset,source}suban_price_aggregations_total{asset,result}suban_oracle_push_total{asset,result}suban_oracle_trust_state{asset,state}suban_http_requests_total{method,route,status}- Node.js process metrics
| Route | Notes |
|---|---|
GET /data/pi-price |
Legacy PI price shape |
GET /data/mainnet-supply, /data/top-accounts, /data/accounts/distribution |
Pi analytics |
GET /data/data/unlocknext30d, /data/data/unlockfull, /data/data/nodemap |
Unlock/node analytics |
GET /api/v1/chain/stats, /chain/ledgers, /chain/accounts, /chain/transactions |
Local Horizon chain data |
GET /api/contracts, /api/contracts/:id, /api/contracts/:id/state |
Contract discovery (derived from recent effects) |
GET /api/escrow/:id, /api/gateway/events |
Escrow viewer backend |
GET /realtime/, /realtime/snapshot |
SSE live transactions/trades/orders |
GET /horizon/* |
Transparent proxy to local Horizon |
These are compatibility/analytics surfaces; they are not guaranteed to be stable developer contracts until documented in a future API version.
Base: http://localhost:4000.
| Route | Purpose |
|---|---|
GET /api/v1/summary |
Latest ledger/transaction/operation summary |
GET /api/v1/accounts/:accountId |
Account details |
GET /api/v1/accounts/:accountId/transactions |
Account transactions |
GET /api/v1/transactions/:hash |
Transaction by hash |
GET /api/v1/ledgers |
Recent ledgers |
GET /api/v1/assets |
Assets known to Horizon |
ALL /horizon/* |
Raw Horizon proxy (`X-Cache: HIT |
GET /health |
Liveness |
GET /metrics |
Prometheus metrics (suban_api_*) |
| Arc routes | /arc/health, /arc/block-number, POST /arc/rpc |
Rate limit: default 100 requests/60s per IP; /metrics is exempt. Upstream
failures return { "error": "Horizon API error", "message": "..." } with the
upstream status or 500.
Base: http://localhost:3002. This is the historical/indexed API. It answers
from SQLite and never calls Horizon or Soroban RPC in a query handler.
| Route | Purpose |
|---|---|
GET / |
Service metadata and endpoint list |
GET /status |
Counts, checkpoints, freshness, Horizon state |
GET /stats |
Counts and per-contract event totals |
GET /contracts |
Contract metadata registry (network/address/interface/verification) |
GET /contracts/:contractId |
One registry entry |
GET /contracts/:contractId/events |
Events for one contract |
GET /events |
Events (contract, type, from, to, ledger_from, ledger_to, limit, offset) |
GET /events/contract/:id |
Events by contract (legacy shape, freshness-wrapped) |
GET /events/type/:type |
Events by type (legacy shape, freshness-wrapped) |
GET /events/ledger/:ledger |
Events at a ledger (legacy shape, freshness-wrapped) |
GET /transactions |
Transactions (account, ledger, from, to, successful, limit, offset) |
GET /transactions/:hash |
Indexed transaction; 404 if not indexed |
GET /operations |
Operations (account, tx_hash, ledger, type, from, to, limit, offset) |
GET /accounts/:accountId |
Indexed account or activity stub |
GET /accounts/:id/transactions |
Transactions involving the account |
GET /accounts/:id/operations |
Operations involving the account |
GET /financial/pools |
Pool activity, latest reserves and derived LP positions |
GET /financial/pools/:contractId |
Pool state, volume, LP positions and recent events |
GET /financial/pools/:contractId/trades |
Swap events with token direction and post-trade reserves |
GET /financial/pools/:contractId/liquidity |
Add/remove-liquidity events with share totals |
GET /financial/pools/:contractId/lp-positions |
Net LP shares derived from add/remove events |
GET /financial/pools/:contractId/valuation |
Opt-in USD reserves/volume; refuses stale or degraded prices |
GET /financial/lending/events |
Deposit/withdraw/borrow/repay/collateral/liquidation/accrual events |
GET /financial/lending/accounts/:accountId |
Latest indexed lending position snapshot |
GET /financial/lending/summary |
Lending event counts and latest event |
GET /financial/accounts/:accountId/activity |
Bounded recent account activity |
GET /financial/summary |
Pool/lending counts, latest event and coverage flags |
GET /ledgers |
Recent indexed ledgers |
GET /ledgers/:sequence |
One indexed ledger |
GET /health |
200 healthy, 503 degraded/stale |
GET /metrics |
Prometheus metrics (suban_indexer_*) |
Financial routes use enriched pool/lending events. coverage.lp_positions is
true and reflects netted internal add/remove-share events; it is not a
transferable LP-token balance. coverage.usd_volume is true only when
INDEXER_PRICE_API_URL, INDEXER_TOKEN_ASSETS and INDEXER_TOKEN_DECIMALS are
all configured. The valuation route returns 503 for stale, non-HEALTHY or
unmapped prices and never infers a USD value from raw amounts alone. Legacy
events remain readable but report missing direction/reserves/LP state.
All data responses include schema_version: "2.0" and freshness:
{
"schema_version": "2.0",
"freshness": {
"as_of": "2026-10-21T11:47:52.494Z",
"latest_indexed_ledger": 29329977,
"latest_observed_ledger": 29329977,
"lag_ledgers": 0,
"lag_seconds": 3,
"last_success_at": "2026-10-21T11:47:48.619Z",
"stale": false,
"stale_after_seconds": 120
},
"data": []
}Pagination is limit/offset (limit max 1000, default 100). Time filters
accept ISO-8601 or Unix seconds. stale is true when ledger lag exceeds
INDEXER_MAX_LAG_LEDGERS (default 10), when the last success is older than
INDEXER_STALE_AFTER_SECONDS (default 120), or before the first success.
Consumers must check freshness.stale; do not assume real-time delivery.
Admin routes (/admin/replay, /admin/checkpoints, /admin/contracts,
/admin/webhooks, /admin/api-keys/usage) require
INDEXER_ADMIN_API_KEY. A replay reproduces state from a checkpoint; writes are
idempotent.
See DATA-PLATFORM.md for the schema and replay contract.
No public API. Internal endpoints:
| Route | Purpose |
|---|---|
GET http://<relayer>:9464/health |
Liveness |
GET http://<relayer>:9464/metrics |
Prometheus metrics (suban_relayer_*) |
Do not expose port 9464 to the public internet.
Base: http://localhost:8000 (JSON-RPC 2.0). Admin API: :8001, internal only.
Supported methods include getHealth, getNetwork, getLatestLedger,
getLedgers, getLedgerEntries, getEvents, getTransaction,
getTransactions, sendTransaction, simulateTransaction, getFeeStats and
getVersionInfo. Params are always an object, never an array.
Compatibility limitation: getLedgerEntries is served by the Go path and is
supported. The internal/preflight tests are excluded from CI because their
fixture targets protocol 25 while the vendored host is protocol 23; this affects
preflight test coverage, not getLedgerEntries. See
../../suban-rpc/COMPATIBILITY.md.
429: back off; respectRetry-Afterwhere provided.5xxfrom a read route: retry with exponential backoff and jitter; do not retry non-idempotent writes automatically.503on a price route: the asset is not configured or has no usable sources. Treat it as "no price", never as zero or a peg.401/403on control routes: fix credentials; do not retry aggressively.- Log the response
schema_versionand anyrequest_id/trace header you add at the edge.
- ARCHITECTURE.md — service responsibilities and data flow.
- DATA-PLATFORM.md — indexed schema, freshness, replay and webhooks.
- PRODUCTS.md — what each API is for.
- SECURITY.md — auth, keys and threat model.
- OBSERVABILITY.md — metrics and dashboards.
- DEFERRED-OWNER-ACTIONS.md — owner follow-ups.
- DEVELOPER.md — contract addresses and SDK usage.