Read API and streams

The /api/dusk/v1 routes serve live snapshots, event history and change streams, each tied to one deployment and stamped with its freshness.

Updated Nov 9, 2026

The native routes under /api/dusk/v1 are how the app reads live state without touching RPC in the browser. Every response is bound to one deployment identity, says how fresh it is, and never invents a value.

The envelope

Successful protocol-data responses carry a dusk-deployment.v2 envelope. This excerpt shows selected fields; complete responses also include the genesis hash, ProgramData addresses, upgrade authorities, delegate identity, raw IDL hashes, executable hashes and deploymentIdentitySha256.

{
  "success": true,
  "deployment": {
    "schemaVersion": "dusk-deployment.v2",
    "network": "devnet",
    "programId": "2rYBVfGubUVbHrLeut6Dn4A9ShM695dGSPCErdsGo16Y",
    "programDataSlot": "517844520",
    "leverageDelegateProgramDataSlot": "517825628",
    "idlSha256": "65de3da39b3f57495f47f331a66b26b620f24756858404b2281f5e6268bbbe58",
    "commitment": "confirmed",
    "sourceSlot": 517844521,
    "observedAt": "2026-11-02T11:59:01.970Z"
  },
  "data": {}
}

The active projection revision is devnet-2026-11-02-governable-tranches. It carries the deployed tranche interface and starts after the Dusk and leverage-delegate deployments are both complete.

Revision-bearing data contracts expose the active revision in their provenance or coverage metadata; it is separate from the envelope's buildRevision, which identifies the API build. /health and /status remain operational endpoints that can explain an unavailable deployment envelope.

Clients configure the identity they expect and reject anything else; a mismatch is 409 protocol_identity_mismatch. Range reads that cannot meet the requested consistency return 503 freshness_unavailable with metadata and no decision-bearing data; commitment is never silently downgraded.

Deployment envelope timestamps are ISO 8601 strings. Data timestamps and expiry fields follow each payload's schema; an expiry is never renewed by delivery or a cache hit.

Freshness states: ready (bounds met, no known gap), catching_up (a backfill is running), reconciling (canonical pointers may still change), stale (your max_lag_slots bound was exceeded). confirmed is the app's default commitment; finalized is required for irreversible accounting and durable keeper deduplication.

Routes

Identity and configuration

RoutePurpose
GET /configThe indexed market configuration and build-pinned deployment envelope; minimumSourceSlot asks for an envelope at or after a slot
GET /health, /status, /metrics, /provenanceOperational endpoints

Markets

RoutePurpose
GET /marketsIndexed markets
GET /markets/state and /markets/state/:marketCached live snapshots with source slot and expiry
GET /markets/:market and /markets/:market/eventsMarket detail and its finalized events (events, since, until, limit, offset)
GET /history/quotes/:marketSampled program spot-price candles: since (required), until, side (base or quote), resolutionSeconds (60, 300, 900, 3600, 14400, 86400), at most 2,000 buckets
GET /prices/:mintPrice history for a mint: market, at, maxAgeSeconds, limit, offset
GET /virtual-book/:market/streamDepth snapshots as server-sent events
GET /markets/:market/operatorMarket operator, optional manager and the single pending bundle from one confirmed bank

Quote candles are decimal strings in NAD, already normalized for mint decimals: divide by 10⁹ and nothing else. They are program previews, not trade candles; empty buckets are not filled and historyRangeComplete is always false.

Owners

RoutePurpose
GET /owners/:owner/accounts/:kindThe complete set of borrow, leverage, yield or referral-accrual accounts, as base64 SDK bytes from one confirmed bank; at most 500
GET /owners/:owner/leverage-valuations/:addressAn exact simulated full-close receipt for one leverage position, with verification slot
GET /owners/:owner/portfolio-snapshotsPortfolio value history: since, until, sampleSeconds
GET /owners/:owner/yield-claimsRecorded yield claims: market, since, until
GET /owners/:owner/yieldClaimable yield for every LP held and every unsettled hLP order escrow
GET /owners/:owner/referral-partnerReferral terms and per-market accruals, net of current transfer fees
GET /liquidationsLiquidatable borrow positions with the program's previewed flash and emergency terms

History and analytics

RoutePurpose
GET /history/eventsFinalized canonical events with cursors; version=2 adds HlpOpened, HlpClosed and YieldClaimed; category=activity&owner= gives a wallet's receipts
GET /history/ordersOrder history
GET /analytics/activityActivity aggregates with volumes split into spot, credit and margin: since, until, market
GET /analytics/yield-ratesRecorded claimable srLP swap-fee and interest rates over a window of one hour to ninety days: since, until, market

Spot, credit and margin volumes overlap by definition and must not be summed. Yield rates are APR components of realized, claimable yield, not a total APY.

Payload streams

RoutePurpose
GET /payload-snapshot and GET /payloadsA full initial snapshot, then complete revisions as dusk-payload events. kind=wallet&owner= publishes positions, valuations and orders together; kind=statistics&range=24h or all publishes activity aggregates and collateral exposure

Wallet payloads publish atomically to the consumer: a position whose close simulation was rejected appears as unavailable with a reason, never as a zero. Captures pace at two seconds for wallets and five for statistics.

Change notifications

GET /changes is a server-sent event stream of hints, not data:

{
  "schemaVersion": "dusk-read-change.v1",
  "streamId": "…",
  "sequence": 12,
  "kind": "change",
  "sourceSlot": 502849270
}

The first frame is a resync; heartbeats every two seconds have slot 0; change bursts coalesce to the highest slot in a 250 ms window. A frame tells you to re-read through your normal API or RPC boundary; it carries no reserves, prices or balances.

Reconnects get a new resync, and missed frames are not replayed. Each API process allows 128 open streams and closes a backpressured one rather than queueing.

The same hints are available as gRPC-Web: omnipair.stream.StreamService/StreamDuskChanges, whose DuskChange.envelope_json carries the identical frame. The app itself subscribes over Socket.IO at the API's /socket.io path; GET /changes stays for clients that predate it. The legacy StreamSwapsUpdates RPC cannot supply Dusk prices and stays for V1 consumers only.

Consumer rules

For an app:

  1. Request confirmed with a max_lag_slots that suits the view.
  2. Show degraded or reconciling state; never treat cached data as current.
  3. Treat the wallet's own confirmation as authoritative for an action you just sent; the indexer may trail it.
  4. Invalidate cached pages when the identity or reconciliation generation changes.

For a keeper:

  1. Use the indexer only to discover and prioritise.
  2. Require the full identity in every response.
  3. Re-fetch all decision accounts from RPC immediately before execution.
  4. Rebuild and simulate against the pinned IDL before signing.

The indexer never authorises a liquidation, an order execution, a bid or an arbitrage on its own.

Canonical history

Normal endpoints expose only rows referenced by canonical events. Ingestion stores an alternate blockhash first, reconciles the winning block through RPC, rolls back affected projections in one transaction, then advances the canonical pointer.

A finalized canonical row is never replaced; conflicting finalized evidence marks the service unavailable and pages an operator. Pagination cursors encode the identity, commitment, the canonical ordering tuple and the reconciliation generation, so a stale cursor is 409 cursor_invalidated rather than a silent gap.