Skip to content

Proto schema

The canonical schema for flint.spot.v1. Synced from the flint-protos submodule; regenerate SDKs and docs after bumping that submodule.

proto
// Code generated by `make bundle-proto`; DO NOT EDIT.
// Services are grouped first so the RPC surface is easy to scan.

syntax = "proto3";

package flint.spot.v1;

// -- Services --------------------------------------------------------

// -- AuthService (auth/service.proto) --------------------------------------------------------

// Issues session tokens. Keypair-auth tokens from Authenticate last 6 hours;
// passwordless organization tokens from VerifyLoginCode last 7 days.
// All RPCs are unauthenticated.
service AuthService {
  // Get a nonce to sign. Idempotent per pubkey within the nonce TTL;
  // calling twice returns distinct nonces but only the most recent is
  // accepted by Authenticate.
  rpc Challenge(ChallengeRequest) returns (ChallengeResponse);

  // Exchange a signed nonce for a 6-hour keypair session token. UNAUTHENTICATED if the
  // pubkey has no outstanding nonce, the nonce has expired, the signature
  // is invalid, or the pubkey is not registered as a quoting authority.
  rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse);

  // Invalidate a token before its natural expiration.
  rpc Revoke(RevokeRequest) returns (RevokeResponse);

  // Send a 6-digit passwordless login code to an organization user's email.
  // Requests are limited to one per email per minute.
  rpc RequestLoginCode(RequestLoginCodeRequest) returns (RequestLoginCodeResponse);

  // Exchange a passwordless login code for a 7-day bearer session token.
  rpc VerifyLoginCode(VerifyLoginCodeRequest) returns (VerifyLoginCodeResponse);
}

// -- MarketDataService (market_data/service.proto) --------------------------------------------------------

// Public market data.
service MarketDataService {
  // Subscribe to a multiplexed stream of book events (L1/L2/L3) and
  // StatusEvents. The pair set is fixed at stream start; to change
  // subscriptions open a new stream.
  rpc Subscribe(SubscribeRequest) returns (stream MarketDataEvent);

  // Subscribe to fills on a separate stream. Optional per-pair filter;
  // empty = fills from every pair.
  rpc SubscribeFills(SubscribeFillsRequest) returns (stream FillEvent);

  // Subscribe to trades executed through external DEX aggregators
  // (Jupiter, OKX, DFlow, Titan, ...) for a single requested pair. The
  // server taps a shared upstream transaction feed only while at least one
  // subscriber is connected; with no subscribers, nothing is captured.
  rpc SubscribeExternalFills(SubscribeExternalFillsRequest)
      returns (stream ExternalFillEvent);

  // Subscribe to per-venue bid/ask for a single requested pair, derived by
  // polling each external aggregator's quote API at a fixed notional every
  // N ms. Polling for a pair runs only while at least one subscriber is
  // connected. Only pairs quoted in the global (USD) numeraire are supported.
  rpc SubscribeExternalQuotes(SubscribeExternalQuotesRequest)
      returns (stream ExternalVenueQuoteEvent);

  // One-shot book snapshot. Convenient for cold-start reconciliation
  // without opening a streaming RPC.
  rpc GetBook(GetBookRequest) returns (GetBookResponse);

  // Return the catalog of discovered pairs + their spot metadata.
  rpc ListPairs(ListPairsRequest) returns (ListPairsResponse);

  // Stream a consolidated snapshot of every registered market
  rpc SubscribeMarketSnapshots(SubscribeMarketSnapshotsRequest)
      returns (stream MarketsSnapshotEvent);
}

// -- MakerService (maker/service.proto) --------------------------------------------------------

// Per-maker queries + streaming. Every RPC requires organization-oriented
// auth: either a session token via `authorization: Bearer <token>` metadata
// whose maker_id exists in the organizations table (see AuthService), or an
// `x-api-key` organization API key. The resolved maker_id scopes every response
// except GetDecodedTransaction, which authenticates the caller but returns the
// whole (multi-party) transaction rather than one maker's slice of it.
service MakerService {
  // Stream balance updates as the on-chain accounts change.
  rpc SubscribeBalance(SubscribeBalanceRequest) returns (stream MakerBalanceEvent);

  // One-shot snapshot of current balances - useful on cold start before
  // opening the stream.
  rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);

  // Stream a consolidated cross-market snapshot enriched with the
  // authenticated maker's own best bid / ask + depth level per pair.
  rpc SubscribeMarketSnapshots(SubscribeMakerMarketSnapshotsRequest)
      returns (stream MakerMarketsSnapshotEvent);

  // 1h / 24h / 30d volume + fill count broken down by pair, scoped to
  // the authenticated maker.
  rpc GetVolumeBreakdown(GetMakerVolumeBreakdownRequest)
      returns (GetMakerVolumeBreakdownResponse);

  // Time-bucketed volume series scoped to the authenticated maker.
  rpc GetVolumeSeries(GetMakerVolumeSeriesRequest)
      returns (GetMakerVolumeSeriesResponse);

  // Time-bucketed historical NAV (USD net asset value) scoped to the
  // authenticated maker.
  rpc GetNavHistory(GetNavHistoryRequest)
      returns (GetNavHistoryResponse);

  // Stream every fill where the authenticated maker provided the
  // liquidity. The taker (counterparty) is the transaction signer; this
  // stream does not include fills where the maker was the taker.
  rpc SubscribeFills(SubscribeMakerFillsRequest) returns (stream MakerFillEvent);

  // Historical fills where the authenticated maker provided liquidity.
  // Same row shape as SubscribeFills, ordered per the request's `order_by`.
  // Capped at 1000 rows - use GetMarkoutSummary for anything spanning the
  // whole window rather than summing these.
  rpc GetFills(GetMakerFillsRequest) returns (GetMakerFillsResponse);

  // Markout over the whole window: totals, 24h buckets, per-market rows and
  // the bps distribution. Aggregated in the warehouse, so no row cap.
  rpc GetMarkoutSummary(GetMakerMarkoutSummaryRequest)
      returns (GetMakerMarkoutSummaryResponse);

  // Rolling 1s / 1m / 5m activity rates scoped to the authenticated maker:
  // fill rate, TX submission outcomes, plus the venue-wide oracle update
  // rate for cross-reference.
  rpc GetStats(GetMakerStatsRequest) returns (GetMakerStatsResponse);

  // Historical on-chain account actions by the authenticated maker:
  // collateral deposits / withdrawals, maker-account creation, and market
  // joins. Trading fills are excluded - use GetFills. Ordered newest-first.
  rpc GetActivity(GetMakerActivityRequest) returns (GetMakerActivityResponse);

  // -- Transaction explorer ----------------------------------------------
  // Signature-first read of a single DECODED transaction, backing the in-house
  // explorer. Returns only transactions the indexer decoded into the `events`
  // table (Flint-program txs) - NOT arbitrary Solana transactions; unseen
  // signatures return NOT_FOUND and the client falls back to an external
  // explorer. Authenticated like the rest of the service, but NOT maker-scoped:
  // it returns every decoded event in the transaction (a tx is multi-party),
  // not just the caller's.
  rpc GetDecodedTransaction(GetDecodedTransactionRequest)
      returns (GetDecodedTransactionResponse);

  // -- Quoting activity --------------------------------------------------
  // On-chain quote-strategy updates (UpdateQuotingParams) by the authenticated
  // maker - the high-frequency complement to GetActivity. Strategy detail
  // (orderlist levels / oracle offsets / linear params) rides as JSON in
  // QuoteUpdate.payload_json; the strategy enum says how to read it. Scoped to
  // the maker, so a competitor can't read another maker's strategy params.

  // Live stream of the maker's quote updates. Optional market / quoter filters
  // narrow within the maker (both empty = all of the maker's quoting).
  rpc SubscribeQuotes(SubscribeQuotesRequest) returns (stream QuoteUpdate);

  // Recent quote updates by the maker over a time window, newest first.
  rpc ListRecentQuotes(ListRecentQuotesRequest) returns (ListRecentQuotesResponse);

  // One of the maker's quote updates in full, by on-chain identity
  // (signature + instruction index + leg index). NOT_FOUND if there is no such
  // quote for this maker.
  rpc GetQuote(GetQuoteRequest) returns (GetQuoteResponse);

  // -- Quoter liveness ---------------------------------------------------
  // Connection telemetry for the maker's quoting fleet (formerly
  // MakerAdminService), read from the quoter_sessions tape.

  // One row per of the maker's quoting authorities (quoting keypairs) with
  // currently-open sessions, aggregated across server instances, optionally
  // restricted to one authority. Beyond connection state each row carries the
  // fee-payer SOL balance, recent round-trip percentiles, and a short
  // bucketed history of latency / quote rate / transaction outcomes - the
  // whole quoter-detail page in one call.
  rpc ListQuotingAuthorities(ListQuotingAuthoritiesRequest)
      returns (ListQuotingAuthoritiesResponse);

  // The maker's currently-open quoter sessions, optionally restricted to one
  // quoting authority.
  rpc ListQuoterSessions(ListQuoterSessionsRequest)
      returns (ListQuoterSessionsResponse);

  // -- SDK access --------------------------------------------------------

  // Read-only Cargo-registry credentials for pulling the private Rust SDK
  // crate. The first call mints a named token for the maker under a dedicated
  // read-only registry account; later calls return the same credentials.
  // Demo sessions are rejected with PERMISSION_DENIED.
  rpc GetSdkCredentials(GetSdkCredentialsRequest)
      returns (GetSdkCredentialsResponse);
}

// -- StatsService (stats/service.proto) --------------------------------------------------------

// Public (unauthenticated) aggregate stats about the exchange.
service StatsService {
  rpc GetSummary(GetSummaryRequest) returns (GetSummaryResponse);

  // 1h / 24h / 30d volume + fill count broken down by pair.
  rpc GetVolumeBreakdown(GetVolumeBreakdownRequest) returns (GetVolumeBreakdownResponse);

  rpc GetVolumeSeries(GetVolumeSeriesRequest) returns (GetVolumeSeriesResponse);

  rpc GetAssetTvls(GetAssetTvlsRequest) returns (GetAssetTvlsResponse);
}

// -- NodeService (node/service.proto) --------------------------------------------------------

// Public (unauthenticated) identity of the flint-server node handling the
// connection: build, deploy target, and how close it sits to leaders.
service NodeService {
  rpc GetNodeInfo(GetNodeInfoRequest) returns (GetNodeInfoResponse);

  // Every flint-server endpoint the deployment publishes, so a client can
  // discover the full set from any one node and pick the closest.
  rpc ListNodes(ListNodesRequest) returns (ListNodesResponse);
}

// -- TxService (tx/service.proto) --------------------------------------------------------

// Transaction support: chain-tip feeds (blockhash, slot), transaction
// submission, and per-tx lifecycle status.
//
// Every RPC on this service requires a keypair-auth session token via
// `authorization: Bearer <token>` metadata. The token's maker_id is used
// for rate limiting on the public streams and to scope which signatures
// show up on the status stream.
service TxService {
  // Forward a transaction to upstream submission paths. Returns
  // the parsed signature immediately; landing/confirmation are reported
  // asynchronously on `SubscribeTxStatus`.
  rpc SubmitTx(SubmitTxRequest) returns (SubmitTxResponse);

  rpc SubscribeBlockhash(SubscribeBlockhashRequest) returns (stream BlockhashEvent);
  rpc SubscribeSlots(SubscribeSlotsRequest) returns (stream SlotEvent);
  rpc SubscribeTxStatus(SubscribeTxStatusRequest) returns (stream TxStatusEvent);

  // Stream the upcoming leader schedule with each leader's TPU socket, so a
  // client can send directly at the validator producing the block. On connect
  // the server replays its whole lookahead window before pushing new entries.
  rpc SubscribeLeaders(SubscribeLeadersRequest) returns (stream LeaderEvent);
}

// -- SpreadProtectService (spread_protect/service.proto) --------------------------------------------------------

// SpreadProtect: an advisory signal for Solana network-topology risk -
// conditions that make it harder to reprice in time and easier to get picked
// off. v0 covers bad leaders only. It is an optimization layer: the on-chain
// price band and decay are the safety floor, so if SpreadProtect is down the
// maker falls back to those and is fine.
//
// The server owns the policy (a manually-maintained bad-leader list, the
// lookahead, and the widen size), turns the leader schedule plus the list into
// widen windows, and pushes them to the maker ahead of time. The signal is
// advisory: the maker may apply it, scale it, or ignore it. Nothing is
// auto-applied at the venue level.
//
// Requires a keypair-auth session token via `authorization: Bearer <token>`.
service SpreadProtectService {
  // Stream SpreadProtect state resets and upcoming widen windows. On connect
  // the server sends a reset event followed by any currently relevant windows,
  // so a fresh or restarted client is never blind until the next scheduled push.
  rpc SubscribeSpreadProtect(SubscribeSpreadProtectRequest) returns (stream SpreadProtectEvent);
}

// -- HistoricalService (historical/service.proto) --------------------------------------------------------

// Historical (ClickHouse-backed) queries. Every RPC requires a session
// token via `authorization: Bearer <token>` metadata.
//
// Hard constraints enforced by the server:
//   - Maximum time window: 30 days.
//   - `GetFills.limit` capped at 1000.
//   - `GetCandles` returns at most 10_000 rows per call.
//
// If the backing ClickHouse is not configured, every RPC returns
// FAILED_PRECONDITION.
service HistoricalService {
  rpc GetFills(GetFillsRequest) returns (GetFillsResponse);
  rpc GetCandles(GetCandlesRequest) returns (GetCandlesResponse);
}

// -- EscalationService (escalation/service.proto) --------------------------------------------------------

// Out-of-band escalation path for makers to reach the FLINT on-call team.
// Requires the same organization-oriented auth as MakerService: either a
// session token via `authorization: Bearer <token>` metadata whose maker_id
// exists in the organizations table (see AuthService), or an `x-api-key`
// organization API key. The resolved maker_id is attached to every page so
// the team knows who is escalating.
service EscalationService {
  // Page the on-call team with a free-text message. Returns once the page
  // has been accepted and dispatched to the team's alerting channel; the
  // response confirms receipt only, not that anyone has acted on it.
  // UNAUTHENTICATED if the caller is not a recognized maker;
  // INVALID_ARGUMENT if the message is empty.
  rpc PageTheTeam(PageTheTeamRequest) returns (PageTheTeamResponse);
}

// -- Messages, events, and shared types ------------------------------

// -- Shared types (common.proto) --------------------------------------------------------

// Shared primitives used across every service: ids, solana-level crypto
// primitives, decimal wire format, enums, book level structs, and metadata.
// Every message in this package has `package flint.spot.v1;` so that the
// generated Rust module stays flat (`proto::<Type>`) regardless of which .proto
// a type was declared in.

// -- Ids -------------------------------------------------------------

message SpotId {
  uint64 id = 1;
}

message MakerId {
  uint64 id = 1;
}

message Pair {
  reserved 1, 2;
  optional uint64 base_id = 3;
  string base = 4;
  optional uint64 quote_id = 5;
  string quote = 6;
  string label = 7;
}

// -- Solana-level primitives -----------------------------------------
//
// Pubkeys and transaction signatures are carried as base58 strings on every
// message in this API.

message Blockhash {
  bytes hash = 1;
}

// -- Decimal ---------------------------------------------------------

// Human-readable decimal string (e.g. "155.14"). The client must parse
// with a decimal library that matches rust_decimal semantics.
message Decimal {
  string value = 1;
}

// -- Timestamp -------------------------------------------------------

// Unix microseconds since epoch. The single canonical timestamp unit
// across the entire API - events, responses, range queries. Field value
// `0` means "unspecified" (used on optional bounds in range queries and
// on absent event timestamps).
message Timestamp {
  uint64 micros = 1;
}

// -- Enums -----------------------------------------------------------

enum FeedLevel {
  FEED_LEVEL_UNSPECIFIED = 0;
  FEED_LEVEL_L1 = 1;
  FEED_LEVEL_L2 = 2;
  FEED_LEVEL_L3 = 3;
}

enum Side {
  SIDE_UNSPECIFIED = 0;
  SIDE_BUY = 1;
  SIDE_SELL = 2;
}

enum HealthState {
  HEALTH_STATE_UNSPECIFIED = 0;
  HEALTH_STATE_HEALTHY = 1;
  HEALTH_STATE_DEGRADED = 2;
  HEALTH_STATE_HALTED = 3;
}

// -- Book levels -----------------------------------------------------

message L2Level {
  Decimal price = 1;
  Decimal size = 2;
}

message L3Entry {
  MakerId maker_id = 1;
  Decimal price = 2;
  Decimal size = 3;
}

// -- Subscription descriptor -----------------------------------------

message PairSubscription {
  Pair pair = 1;
  FeedLevel level = 2;
  // When true, the server emits a full snapshot on every book update and
  // never sends deltas. When false (default), one snapshot on subscribe
  // followed by incremental deltas.
  bool snapshot_only = 3;
}

// -- Per-event metadata ----------------------------------------------

message PairMetadata {
  uint64 slot = 1;
  Timestamp ts = 2;
  uint64 update_id = 3;
}

// -- Reference / catalog ---------------------------------------------

message SpotMetadata {
  SpotId id = 1;
  string name = 2;
  // Base58 mint address.
  string mint = 3;
  // Base58 program id.
  string program_id = 4;
  uint32 decimals = 5;
  uint32 atoms_per_lot = 6;
  // Base58 SPL Token / Token-2022 program that owns the mint.
  string token_program = 7;
  // Token logo URL. Empty when unknown - sourced from Jupiter's verified
  // token set, so it needs the pricing store enabled.
  string icon = 8;
}

// -- Geo -------------------------------------------------------------

// A geographic coordinate. Optional everywhere it appears: the source may be
// unavailable (no GeoIP database loaded, a private/unresolvable IP, or an
// unknown server region), in which case the field is simply absent.
message LatLng {
  double lat = 1;
  double lng = 2;
}

// -- Health / status (cross-cutting) ---------------------------------

message StatusEvent {
  HealthState state = 1;
  // Absent = global scope, present = per-pair.
  Pair pair = 2;
  string reason = 3;
  Timestamp ts = 4;
}

// -- Auth messages (auth/messages.proto) --------------------------------------------------------

// -- AuthService messages --------------------------------------------
//
// Session-token flow:
//   1. Client calls `AuthService.Challenge(pubkey)` -> server returns a
//      single-use nonce bound to that pubkey with a short TTL.
//   2. Client signs AUTH_DOMAIN_PREFIX || nonce with the pubkey's keypair.
//   3. Client calls `AuthService.Authenticate(pubkey, signature)` -> server
//      verifies the signature covers the outstanding nonce, maps pubkey to
//      maker_id via MakerRegistry, and returns a bearer `session_token`.
//   4. Client attaches `authorization: Bearer <session_token>` metadata on
//      TxService or MakerService. Passwordless sessions are accepted by
//      MakerService only, not TxService.

message ChallengeRequest {
  // Base58 pubkey (quoting authority) that will sign the challenge.
  string pubkey = 1;
}

message ChallengeResponse {
  // Single-use random nonce. Client signs AUTH_DOMAIN_PREFIX || nonce.
  bytes nonce = 1;
  // Absolute expiration. Server rejects Authenticate after this moment.
  Timestamp expires_at = 2;
}

message AuthenticateRequest {
  // Base58 pubkey.
  string pubkey = 1;
  // Base58 signature over AUTH_DOMAIN_PREFIX || nonce from the outstanding Challenge.
  string signature = 2;
  // Maker to authenticate as. Only needed when `pubkey` is a quoting
  // authority for more than one maker (a shared quoting key); the server then
  // scopes the session to exactly this maker. When omitted, the server
  // resolves the maker the pubkey maps to; if the pubkey is a quoting
  // authority for more than one maker it defaults to the lowest maker id and
  // logs a warning, so set this for a deterministic maker.
  MakerId maker_id = 3;
}

message AuthenticateResponse {
  // Opaque bearer token. Clients pass this back as
  // `authorization: Bearer <session_token>` metadata on all authenticated RPCs.
  string session_token = 1;
  // Resolved maker identity the token speaks for.
  MakerId maker_id = 2;
  // Absolute session expiration. Clients should re-auth before this
  // moment; server will start returning UNAUTHENTICATED once it passes.
  Timestamp expires_at = 3;
}

message RevokeRequest {
  string session_token = 1;
}

message RevokeResponse {}

message RequestLoginCodeRequest {
  string email = 1;
}

message RequestLoginCodeResponse {}

message VerifyLoginCodeRequest {
  string email = 1;
  string code = 2;
}

message VerifyLoginCodeResponse {
  // Opaque bearer token. Clients pass this back as
  // `authorization: Bearer <session_token>` metadata on MarketDataService.
  string session_token = 1;
  // Resolved maker identity associated with the user's organization.
  MakerId maker_id = 2;
  // Absolute session expiration.
  Timestamp expires_at = 3;
  string organization_name = 4;
}

// -- Market data events (market_data/events.proto) --------------------------------------------------------

// -- Book events -----------------------------------------------------

message L1Event {
  Pair pair = 1;
  PairMetadata metadata = 2;
  L2Level bid = 3;
  L2Level ask = 4;
}

message L2SnapshotEvent {
  Pair pair = 1;
  PairMetadata metadata = 2;
  repeated L2Level bids = 3;
  repeated L2Level asks = 4;
}

message L2UpdateEvent {
  Pair pair = 1;
  PairMetadata metadata = 2;
  repeated L2Level bids = 3;
  repeated L2Level asks = 4;
}

message L3SnapshotEvent {
  Pair pair = 1;
  PairMetadata metadata = 2;
  repeated L3Entry bids = 3;
  repeated L3Entry asks = 4;
}

message L3UpdateEvent {
  Pair pair = 1;
  PairMetadata metadata = 2;
  repeated L3Entry bids = 3;
  repeated L3Entry asks = 4;
}

// -- Cross-market snapshot -------------------------------------------

// `slot` is per-market: different entries may carry different slots when
// only a subset of markets updated in the most recent envelope.
message MarketSnapshot {
  Pair pair = 1;
  uint64 slot = 2;
  L2Level best_bid = 3;
  L2Level best_ask = 4;
  repeated L2Level bid_depth_levels = 5;
  repeated L2Level ask_depth_levels = 6;
  Decimal last_price = 7;
}

message MarketsSnapshotEvent {
  uint64 slot = 1;
  Timestamp ts = 2;
  repeated MarketSnapshot markets = 3;
}

// -- Fill events -----------------------------------------------------

message FillEvent {
  Pair pair = 1;
  Side side = 2;
  Decimal price = 3;
  Decimal size = 4;
  uint64 slot = 5;
  Timestamp ts = 6;
  // Base58 signature of the transaction the fill was settled in.
  string signature = 7;
}

// -- External aggregator fills ---------------------------------------

// A trade observed on an external Solana DEX aggregator (Jupiter, OKX,
// DFlow, Titan, ...), normalized onto a Flint pair. `price` is quote per
// base and `size` is in base units, both derived from the swap's token
// balance deltas; `side` is the taker's direction relative to the base.
message ExternalFillEvent {
  Pair pair = 1;
  Side side = 2;
  Decimal price = 3;
  Decimal size = 4;
  uint64 slot = 5;
  Timestamp ts = 6;
  // Base58 signature of the transaction the swap settled in.
  string signature = 7;
  // Aggregator the swap routed through ("jupiter", "okx", "dflow", "titan").
  string source = 8;
}

// -- External venue quotes -------------------------------------------

// A venue's bid/ask for a pair at a fixed notional, obtained by polling the
// venue's quote API. Both sides are quoted at the same base size (the amount
// of base received for the ask-side notional), so `bid.size == ask.size`;
// under spread/fees the realized bid notional is <= the ask notional. `price`
// is quote per base. The quote side is the global (USD) numeraire.
message ExternalVenueQuoteEvent {
  Pair pair = 1;
  string venue = 2;
  // SELL base -> quote (price a taker receives selling base).
  L2Level bid = 3;
  // BUY base with quote (price a taker pays buying base).
  L2Level ask = 4;
  // Server receipt time of the poll that produced this quote.
  Timestamp received_ts = 5;
  // Underlying AMM/DEX labels each side's order routes through, in route
  // order - e.g. Jupiter's `routePlan[].swapInfo.label`. The buy and sell
  // legs are quoted separately and can route differently. Empty if the venue
  // doesn't report a route.
  repeated string ask_route = 6;
  repeated string bid_route = 7;
}

// -- Multiplexed stream envelope -------------------------------------
//
// MarketDataService.Subscribe returns a single stream carrying every
// per-pair book event plus cross-cutting StatusEvents. Fills are not
// carried here - use `MarketDataService.SubscribeFills`. Clients
// discriminate via `kind`.

message MarketDataEvent {
  oneof kind {
    L1Event l1 = 1;
    L2SnapshotEvent l2_snapshot = 2;
    L2UpdateEvent l2_update = 3;
    L3SnapshotEvent l3_snapshot = 4;
    L3UpdateEvent l3_update = 5;
    StatusEvent status = 6;
  }
}

// -- Market data messages (market_data/messages.proto) --------------------------------------------------------

// -- MarketDataService request / response ----------------------------

message SubscribeRequest {
  // Pairs to subscribe to with their desired feed level.
  repeated PairSubscription pairs = 1;
}

// Separate stream so book subscribers don't have to pay to deserialize
// fills and vice versa.
message SubscribeFillsRequest {
  // Per-pair filter. Empty = fills from every pair.
  repeated Pair pairs = 1;
}

// External aggregator fills are streamed per-pair: the server captures a
// single shared upstream transaction feed across all aggregators and fans
// out only the trades matching each subscriber's requested pair.
message SubscribeExternalFillsRequest {
  // Required. The pair to stream external fills for.
  Pair pair = 1;
}

// Per-venue bid/ask poll feed. The server polls each configured venue's quote
// API for this pair while at least one subscriber is connected. The pair's
// quote side must be the global (USD) numeraire.
message SubscribeExternalQuotesRequest {
  // Required. The pair to stream external venue quotes for.
  Pair pair = 1;
}

message GetBookRequest {
  Pair pair = 1;
  FeedLevel level = 2;
}

message GetBookResponse {
  oneof snapshot {
    L2SnapshotEvent l2 = 1;
    L3SnapshotEvent l3 = 2;
    L1Event l1 = 3;
  }
}

message ListPairsRequest {}

message SubscribeMarketSnapshotsRequest {}

message ListedPair {
  Pair pair = 1;
  SpotMetadata base = 2;
  // Last landed fill price for this pair. Absent until the server observes a fill.
  Decimal last_price = 3;
  SpotMetadata quote = 4;
  // Taker fee rate for this pair, in hundredths of a basis point:
  // 1_000_000 == 100%, so 100 == 1bp == 0.01%.
  uint32 fee_hbps = 5;
}

message ListPairsResponse {
  repeated ListedPair pairs = 1;
}

// -- Maker events (maker/events.proto) --------------------------------------------------------

// Streaming form of a per-maker balance observation. Emitted whenever the
// underlying on-chain account updates. Always scoped to the authenticated
// maker - servers filter by the token's maker_id before fan-out, so a
// client never sees another maker's balances.
message MakerBalanceEvent {
  MakerId maker_id = 1;
  SpotId spot_id = 2;
  // Token units (e.g. "1.5" SOL); server scales by SpotMetadata.decimals.
  Decimal balance = 3;
  uint64 slot = 4;
  Timestamp ts = 5;
  Decimal notional = 6;
  Decimal soft_max_balance = 7;
}

// -- Per-maker market snapshot ---------------------------------------

// The authenticated maker's resting quote on one side of a pair plus
// its 0-indexed L2 depth level (0 = top of book). Aggregates across the
// maker's individual orders at their best price for that side.
message MakerQuote {
  Decimal price = 1;
  Decimal size = 2;
  uint32 level = 3;
}

// Per-pair view that augments `MarketSnapshot` with the authenticated
// maker's own best bid / best ask + depth level.
message MakerMarketSnapshot {
  Pair pair = 1;
  uint64 slot = 2;
  L2Level best_bid = 3;
  L2Level best_ask = 4;
  repeated L2Level bid_depth_levels = 5;
  repeated L2Level ask_depth_levels = 6;
  Decimal last_price = 7;
  // Absent when the maker has no order on this side of this pair.
  MakerQuote maker_bid = 8;
  MakerQuote maker_ask = 9;
}

message MakerMarketsSnapshotEvent {
  uint64 slot = 1;
  Timestamp ts = 2;
  repeated MakerMarketSnapshot markets = 3;
}

// -- Per-maker fill --------------------------------------------------

// Fill where the authenticated maker provided the liquidity. The `side`
// here is the maker's side (BUY = maker bought, SELL = maker sold) -
// the inverse of `FillEvent.side` on the public market-data stream,
// which carries the taker's side.
message MakerFillEvent {
  Pair pair = 1;
  MakerId maker_id = 2;
  Side side = 3;
  Decimal price = 4;
  Decimal size = 5;
  uint64 slot = 6;
  Timestamp ts = 7;
  // Base58 signature of the transaction the fill was settled in.
  string signature = 8;
  // Quote-denominated markout reference prices captured from the pricing
  // store at fixed horizons after the fill (0s/1s/5s/15s/30s). Populated only
  // by GetFills (the historical query joins `fill_markouts`); always unset on
  // the SubscribeFills stream, where markouts don't exist yet. A horizon is
  // unset when no price was captured (store too stale, or low-timeframe
  // horizons disabled).
  Decimal ref_price_0s = 9;
  Decimal ref_price_1s = 10;
  Decimal ref_price_5s = 11;
  Decimal ref_price_15s = 12;
  Decimal ref_price_30s = 13;
  Decimal notional_usd = 14;
}

// -- Per-maker on-chain activity -------------------------------------

// A single on-chain account action performed by the authenticated maker:
// collateral deposit / withdrawal, maker-account creation, or joining a
// spot market. Trading fills are served separately by GetFills /
// SubscribeFills and are not included here.

// Selects which `MakerActivity` actions GetActivity returns; `UNSPECIFIED` is
// ignored. Each value maps to one action case, except `DEPOSIT_WITHDRAW` which
// covers both `deposit` and `withdraw`.
enum MakerActivityType {
  MAKER_ACTIVITY_TYPE_UNSPECIFIED = 0;
  MAKER_ACTIVITY_TYPE_DEPOSIT_WITHDRAW = 1;
  MAKER_ACTIVITY_TYPE_ADD_MARKET = 2;
  MAKER_ACTIVITY_TYPE_MANAGE_MAKER = 3;
  MAKER_ACTIVITY_TYPE_SET_CROSS_SPREAD = 4;
  MAKER_ACTIVITY_TYPE_SET_QUOTING_PARAMS = 5;
}

message MakerActivity {
  uint64 slot = 1;
  Timestamp ts = 2;
  // Base58 transaction signature the action was emitted in.
  string signature = 3;
  // 6 / create_maker retired: maker creation is archived but not surfaced here.
  reserved 6;
  reserved "create_maker";
  oneof action {
    DepositAction deposit = 4;
    WithdrawAction withdraw = 5;
    AddMarketAction add_market = 7;
    ManageMakerAction manage_maker = 8;
    CrossSpreadAction set_cross_spread = 9;
    QuotingParamsAction set_quoting_params = 10;
  }
}

// Collateral deposited into a spot balance.
message DepositAction {
  SpotId spot_id = 1;
  // Token units deposited (server scales by SpotMetadata.decimals).
  Decimal amount = 2;
  // Spot balance after the deposit, token units.
  Decimal balance_after = 3;
}

// Collateral withdrawn from a spot balance.
message WithdrawAction {
  SpotId spot_id = 1;
  // Token units withdrawn (server scales by SpotMetadata.decimals).
  Decimal amount = 2;
  // Spot balance after the withdrawal, token units.
  Decimal balance_after = 3;
}

// The maker joined a spot market by adding a micro-book, enabling it to
// quote there. `book_index` / `soft_max_balance` are absent on actions
// indexed before those fields were recorded.
message AddMarketAction {
  SpotId spot_id = 1;
  optional uint32 book_index = 2;
  // Soft balance cap configured for the book, token units.
  Decimal soft_max_balance = 3;
}

// A change to a maker's authorities via the on-chain ManageMaker instruction:
// adding/removing a quoting authority, or rotating the admin / withdraw
// authority. The program emits no event for these - the server recovers them
// from the instruction data.
message ManageMakerAction {
  enum Kind {
    KIND_UNSPECIFIED = 0;
    ADD_QUOTER = 1;
    REMOVE_QUOTER = 2;
    SET_ADMIN = 3;
    SET_WITHDRAW_AUTHORITY = 4;
  }
  Kind kind = 1;
  // The authority pubkey added, removed, or set (base58).
  string authority = 2;
}

// A maker changed a cross-market spread on one of its micro books, via the
// on-chain UpdateQuotingParams instruction (which emits no event). The spread
// is applied to the fair price when the `market_spot` book quotes against
// `cross_spot`.
//
// Reflects the cross-spread change as *submitted* in the instruction. The
// program applies a quoting-params update only when its order sequence is
// fresh, so in the rare case a stale (out-of-order) refresh carries a
// cross-spread change, this may report a change the chain skipped.
message CrossSpreadAction {
  // The micro book's own market.
  SpotId market_spot = 1;
  // The cross leg the spread applies to.
  SpotId cross_spot = 2;
  // Raw on-chain spread (oracle units); absent means the cross spread was
  // cleared (no cross spread).
  optional uint32 spread = 3;
}

// A maker changed quoting params (enable flag and/or soft cap) on a micro book,
// via UpdateQuotingParams (no CPI event). Both fields are sent on every update,
// so each value is reported rather than just what changed. Recovered only for
// params-only updates (`quoting` unset, no cross-spread); reflects the submitted
// values, which a stale order sequence may mean the chain skipped.
message QuotingParamsAction {
  SpotId spot_id = 1;
  // Whether the book is enabled for quoting (`false` = paused).
  bool enabled = 2;
  // Soft balance cap, token units; absent means no cap.
  Decimal soft_max_balance = 3;
}

// -- Stats messages (stats/messages.proto) --------------------------------------------------------

// -- Rolling-window enums --------------------------------------------

enum StatsWindow {
  STATS_WINDOW_UNSPECIFIED = 0;
  STATS_WINDOW_1H = 1;
  STATS_WINDOW_24H = 2;
  STATS_WINDOW_30D = 3;
}

// -- Shared building blocks ------------------------------------------

message WindowStats {
  StatsWindow window = 1;
  Decimal volume_base = 2;
  Decimal volume_quote = 3;
  uint64 fill_count = 4;
  Decimal volume_usd = 5;
}

// Cross-venue totals are USD: base and quote assets differ per pair and do
// not add.
message CrossVenueWindowStats {
  reserved 2;

  StatsWindow window = 1;
  uint64 fill_count = 3;
  // Valued when each fill was ingested, so price moves never restate it.
  // Excludes fills that were ingested unpriced.
  Decimal volume_usd = 4;
}

message PairVolume {
  Pair pair = 1;
  WindowStats one_hour = 2;
  WindowStats twenty_four_hour = 3;
  WindowStats thirty_day = 4;
  Decimal last_price = 5;
}

// -- GetSummary ------------------------------------------------------

message GetSummaryRequest {}

message GetSummaryResponse {
  reserved 6, 7;

  uint32 active_pairs = 1;
  uint32 active_makers = 2;

  CrossVenueWindowStats one_hour = 3;
  CrossVenueWindowStats twenty_four_hour = 4;
  CrossVenueWindowStats thirty_day = 5;

  uint64 fill_count_all_time = 8;
  Decimal volume_usd_all_time = 14;

  // Distinct taker accounts in the last 24h.
  uint32 unique_traders_24h = 9;

  // USDC-quoted trades only.
  LargestTrade largest_trade_24h = 10;

  UpdateRate update_rate = 11;
  UpdateRate oracle_update_rate = 12;

  // Sampled periodically, not real-time.
  Decimal total_nav = 13;
}

message LargestTrade {
  Pair pair = 1;
  Decimal size = 2;
  Decimal price = 3;
  Timestamp ts = 4;
  Decimal notional = 5;
}

// -- GetVolumeBreakdown ----------------------------------------------

message GetVolumeBreakdownRequest {
  // Empty = every pair with fills in the last 30d.
  repeated Pair pairs = 1;
}

message GetVolumeBreakdownResponse {
  // Ordered by 24h volume descending.
  repeated PairVolume pairs = 1;
}

// -- GetVolumeSeries -------------------------------------------------

message GetVolumeSeriesRequest {
  // Absent = cross-venue aggregate.
  Pair pair = 1;
  // Sets horizon and bucket size: 1H -> 60 x 1m, 24H -> 288 x 5m,
  // 30D -> 720 x 1h.
  StatsWindow window = 2;
}

message VolumeBucket {
  // Bucket start (inclusive).
  Timestamp ts = 1;
  // Per-pair only; absent on a cross-venue request, where `volume_usd`
  // carries the total.
  Decimal volume_base = 2;
  Decimal volume_quote = 3;
  uint64 fill_count = 4;
  Decimal volume_usd = 5;
}

message GetVolumeSeriesResponse {
  // Ordered oldest-first.
  repeated VolumeBucket buckets = 1;
  uint32 bucket_seconds = 2;
}

// -- GetAssetTvls ----------------------------------------------------

message GetAssetTvlsRequest {}

message AssetTvl {
  SpotMetadata asset = 1;
  // Token units, not atoms.
  Decimal total_balance = 2;
  uint32 maker_count = 3;
}

message GetAssetTvlsResponse {
  repeated AssetTvl assets = 1;
}

// -- UpdateRate ------------------------------------------------------

message UpdateRate {
  double per_second_1s = 1;
  double per_second_1m = 2;
  double per_second_5m = 3;
}

// -- Node messages (node/messages.proto) --------------------------------------------------------

// -- GetNodeInfo -----------------------------------------------------

message GetNodeInfoRequest {}

message GetNodeInfoResponse {
  // Server build identity.
  string version = 1;
  string git_commit = 2;
  // Schema this node was built against, so a client can detect skew
  // against its own generated stubs.
  string proto_hash = 3;
  string proto_rev = 4;

  // Solana cluster this node is pointed at (e.g. "mainnet-beta").
  string cluster = 5;
  // Deploy region owning this node. Matches the `region` reported on
  // maker_admin streams.
  string region = 6;
  // Base58 program id of the on-chain venue this node serves.
  string program_id = 7;
  // Process start time. Absolute, so it needs no fetch-time anchor to
  // interpret; uptime is `now - started_at`.
  Timestamp started_at = 8;

  LeaderProximity leader_proximity = 9;

  // Per-process id of this flint-server instance. Same id space as
  // `QuoterSession.server_instance`.
  string server_instance = 10;

  // Approximate location of `region`. Absent for unmapped regions (e.g.
  // local/dev); treat the precision as illustrative.
  optional LatLng region_lat_lng = 11;

  // This node only - whether it is keeping up and able to serve. Venue and
  // per-pair health are a separate axis, carried on `StatusEvent`.
  HealthState health = 12;
  // Why `health` is not HEALTH_STATE_HEALTHY. Empty when it is.
  string health_reason = 13;
}

// How close this node sits to the block leaders it sends to, measured from
// observed round trips. A client weighing a direct TPU send off
// `TxService.SubscribeLeaders` against the extra hop through `SubmitTx` can
// compare its own leader distance to this.
message LeaderProximity {
  // Leaders included in the measurement. `0` means no sample yet, in which
  // case the distance fields are meaningless.
  uint32 measured = 1;
  uint32 mean_distance_micros = 2;
  uint32 median_distance_micros = 3;
  uint32 p90_distance_micros = 4;
}

// -- ListNodes -------------------------------------------------------

message ListNodesRequest {
  // Empty = every cluster the deployment serves. Set to list only the
  // endpoints on one cluster (e.g. "mainnet-beta").
  string cluster = 1;
}

// One dialable flint-server endpoint.
message NodeEndpoint {
  // gRPC endpoint to dial, host:port or a full URL.
  string endpoint = 1;
  // Deploy region. Same id space as `GetNodeInfoResponse.region`.
  string region = 2;
  // Approximate location of `region`. Absent for unmapped regions (e.g.
  // local/dev); treat the precision as illustrative.
  optional LatLng region_lat_lng = 3;
  // Solana cluster this endpoint serves.
  string cluster = 4;
  // True for the endpoint serving this request.
  bool current = 5;
}

message ListNodesResponse {
  // Every endpoint the deployment publishes, including `current`. Carries no
  // health: an entry means "published", not "up right now". A client that
  // needs liveness calls GetNodeInfo on the endpoint it picked.
  repeated NodeEndpoint nodes = 1;
}

// -- Maker messages (maker/messages.proto) --------------------------------------------------------

// -- MakerService request / response ---------------------------------

message SubscribeBalanceRequest {
  // Optional per-spot filter. Empty = every spot the authenticated maker
  // has a balance in (base + quote of every pair they quote on).
  repeated SpotId spot_ids = 1;
}

message GetBalanceRequest {
  // Optional per-spot filter; empty = all spots.
  repeated SpotId spot_ids = 1;
}

message GetBalanceResponse {
  repeated MakerBalanceEvent balances = 1;
}

message SubscribeMakerMarketSnapshotsRequest {}

// Per-maker volume breakdown. The authenticated maker_id is taken from
// the bearer token; clients only optionally narrow the pair set.
message GetMakerVolumeBreakdownRequest {
  // Optional pair filter. Empty = every pair the maker has fills in
  // across the largest window (30d).
  repeated Pair pairs = 1;
}

message GetMakerVolumeBreakdownResponse {
  // Ordered by 24h volume descending.
  repeated PairVolume pairs = 1;
}

message SubscribeMakerFillsRequest {
  // Optional per-pair filter. Empty = every pair the authenticated maker
  // gets a fill on.
  repeated Pair pairs = 1;
}

// SEC_<n> rather than <n>S: prefix-stripping codegen (prost) would turn
// MARKOUT_HORIZON_0S into the invalid identifier `0S`. SEC_0 / SEC_1 are only
// populated when the server runs with MARKOUT_LOW_TIMEFRAME_ENABLE, off in
// mainnet.
enum MarkoutHorizon {
  MARKOUT_HORIZON_UNSPECIFIED = 0;  // server default: 5s
  MARKOUT_HORIZON_SEC_0 = 1;
  MARKOUT_HORIZON_SEC_1 = 2;
  MARKOUT_HORIZON_SEC_5 = 3;
  MARKOUT_HORIZON_SEC_15 = 4;
  MARKOUT_HORIZON_SEC_30 = 5;
}

enum MakerFillOrder {
  MAKER_FILL_ORDER_UNSPECIFIED = 0;  // same as TIME_DESC
  MAKER_FILL_ORDER_TIME_DESC = 1;
  // Largest absolute USD markout first; fills without one sort last. A biased
  // slice of the window's extremes, not a sample - derive nothing aggregate
  // from it, use GetMarkoutSummary.
  MAKER_FILL_ORDER_ABS_MARKOUT_DESC = 2;
}

message GetMakerFillsRequest {
  // Optional per-pair filter. Empty = every pair the authenticated maker
  // got a fill on.
  repeated Pair pairs = 1;
  // Inclusive lower bound. Unset / zero-micros = server retention window.
  Timestamp start = 2;
  // Exclusive upper bound. Unset / zero-micros = now.
  Timestamp end = 3;
  // Max rows to return. Zero = server default (50). Hard cap 1000.
  uint32 limit = 4;
  // Ordering for the returned page. Unset = TIME_DESC.
  MakerFillOrder order_by = 5;
  // Horizon ABS_MARKOUT_DESC ranks on. Ignored for TIME_DESC.
  MarkoutHorizon markout_horizon = 6;
}

message GetMakerFillsResponse {
  // Ordered per the request's `order_by`; newest-first by default.
  repeated MakerFillEvent fills = 1;
}

// -- GetMarkoutSummary -----------------------------------------------

// Markout over the whole window. GetFills is capped at 1000 rows, so totals
// summed from it describe only a busy maker's most recent slice; these have
// no such cap.
message GetMakerMarkoutSummaryRequest {
  repeated Pair pairs = 1;
  // Inclusive lower bound. Unset / zero-micros = server retention window.
  Timestamp start = 2;
  // Exclusive upper bound. Unset / zero-micros = now.
  Timestamp end = 3;
  MarkoutHorizon horizon = 4;
}

// A rolling 24h window from `effective_start`, not a calendar day - no
// timezone is involved.
message MarkoutBucket {
  uint32 index = 1;
  double net = 2;
  double adverse = 3;
  double favorable = 4;
}

message MarkoutMarketRow {
  Pair pair = 1;
  double net = 2;
  double adverse = 3;
  double favorable = 4;
  double volume_usd = 5;
  uint64 fills = 6;
  uint64 fills_with_markout = 7;
  optional double avg_bps = 8;
}

// Edges travel with the data so clients render labels from the response
// rather than hardcoding the bucket list. Unset = unbounded.
message MarkoutHistogramBin {
  optional double min = 1;
  optional double max = 2;
  uint64 count = 3;
  double volume_usd = 4;
  double net = 5;
}

message GetMakerMarkoutSummaryResponse {
  double net = 1;
  // Absolute USD of the negative-markout fills only (adverse selection).
  double adverse = 2;
  double favorable = 3;
  // At-ingest USD notional over every priced fill.
  double volume_usd = 4;
  // Every fill in the window, including those behind no USD figure.
  uint64 fills = 5;
  // Carrying both a USD notional and a reference price - the only fills
  // behind net / adverse / favorable.
  uint64 fills_with_markout = 6;
  // Ingested without a USD notional, not reconstructible later. Non-zero
  // means every USD total here is a lower bound.
  uint64 fills_unpriced = 7;
  uint64 fills_missing_ref = 8;
  optional double avg_bps = 9;
  // Unset, not 0, when no fill carried a markout - matching avg_bps.
  optional double avg_per_fill = 10;
  repeated MarkoutBucket buckets = 11;
  // Descending by `adverse`.
  repeated MarkoutMarketRow markets = 12;
  // Ascending by bps, edges contiguous.
  repeated MarkoutHistogramBin histogram = 13;
  // Window actually queried. A longer range comes back clamped to the
  // server's 30d retention rather than rejected, so label the range from
  // these rather than from the request.
  Timestamp effective_start = 14;
  Timestamp effective_end = 15;
}

// Per-maker volume series. The authenticated maker_id is taken from the
// bearer token. Window selects both horizon and bucket width identically
// to `StatsService.GetVolumeSeries`.
message GetMakerVolumeSeriesRequest {
  // Optional pair filter. Absent = aggregate across every pair the maker
  // has fills in.
  Pair pair = 1;
  StatsWindow window = 2;
}

message GetMakerVolumeSeriesResponse {
  // Ordered oldest-first.
  repeated VolumeBucket buckets = 1;
  uint32 bucket_seconds = 2;
}

// -- GetNavHistory ----------------------------------------------

// Per-maker historical NAV (net asset value, USD). The authenticated
// maker_id is taken from the bearer token. Window selects both horizon and
// bucket width identically to `GetVolumeSeries`.
message GetNavHistoryRequest {
  StatsWindow window = 1;
}

message NavPoint {
  Timestamp ts = 1;
  // Total USD value of the maker's holdings in this bucket. Held assets with
  // no available price are valued at 0; `unpriced_assets` flags when that
  // makes the total a partial valuation.
  Decimal nav_usd = 2;
  // Freshness of the prices the NAV was computed from. Lets clients spot a
  // valuation taken against carried-forward (stale) prices.
  Timestamp price_as_of = 3;
  // Held assets that were priced into `nav_usd`.
  uint32 priced_assets = 4;
  // Held assets with no price, counted as 0 in `nav_usd`. Non-zero means the
  // total understates true NAV.
  uint32 unpriced_assets = 5;
}

message GetNavHistoryResponse {
  // Ordered oldest-first.
  repeated NavPoint points = 1;
  uint32 bucket_seconds = 2;
}

// -- GetStats --------------------------------------------------------

message GetMakerStatsRequest {}

message GetMakerStatsResponse {
  reserved 2, 3;

  // Echoed from the bearer token so the client can sanity-check the scope.
  MakerId maker_id = 1;

  // Match events where this maker provided liquidity.
  UpdateRate fills = 4;

  // Transactions accepted from this maker by the server's TxService
  // (counts every TX whose initial outcome was `Submitted`).
  UpdateRate transactions_sent = 5;
  // Transactions confirmed on-chain (TxOutcome::Landed).
  UpdateRate transactions_landed = 6;
  // Transactions that failed - rejected pre-submit, expired before landing,
  // or landed with an error.
  UpdateRate transactions_failed = 7;

  // Venue-wide oracle-fair update rate. Same value every maker observes -
  // included so MMs can compare their request rate against oracle volatility.
  UpdateRate oracle_updates = 8;
}

// -- GetActivity -----------------------------------------------------

// Per-maker on-chain account-action history. The authenticated maker_id is
// taken from the bearer token / API key. Covers deposits, withdrawals,
// maker-account creation, and market joins - not trading fills (see GetFills).
message GetMakerActivityRequest {
  // Inclusive lower bound. Unset / zero-micros = server retention window.
  Timestamp start = 1;
  // Exclusive upper bound. Unset / zero-micros = now.
  Timestamp end = 2;
  // Max rows to return. Zero = server default (50). Hard cap 1000.
  uint32 limit = 3;
  // Optional action-type filter. Empty = all types; unrecognized / UNSPECIFIED
  // entries are ignored, and a filter matching no known type yields no rows.
  repeated MakerActivityType types = 4;
}

message GetMakerActivityResponse {
  // Ordered newest-first.
  repeated MakerActivity activities = 1;
}

// -- GetDecodedTransaction (in-house explorer) -----------------------
//
// Signature-first read backing the in-house transaction explorer. Returns only
// transactions the indexer DECODED into the `events` table (Flint-program txs),
// not arbitrary Solana transactions. Unlike the other MakerService RPCs it is
// authenticated for access control but NOT maker-scoped: a transaction is
// inherently multi-party (a fill has a maker and a taker/counterparty), so the
// response carries every decoded event in the transaction regardless of which
// maker they belong to. Assembled from the already-decoded `events`
// (bloom-indexed on signature) plus tx-level fields from `raw_transactions`.
// Signatures the indexer never saw (non-Flint txs) return NOT_FOUND - the
// client then falls back to an external explorer link.

// Landing status of a transaction. Everything the indexer archived landed on
// chain; `FAILED` means it landed but reverted (recovered from archived tx
// meta). Rows indexed before tx-meta capture landed report `CONFIRMED` with an
// unset `fee_lamports`.
enum TransactionStatus {
  TRANSACTION_STATUS_UNSPECIFIED = 0;
  TRANSACTION_STATUS_CONFIRMED = 1;
  TRANSACTION_STATUS_FAILED = 2;
}

// The kind of decoded program event a `DecodedEvent` carries. Mirrors the
// indexer's `EventKind` - CPI-event-backed kinds plus synthetic kinds the
// indexer recovers from instruction data. Tells the client how to render the
// row and which dashboard page to deep-link to (e.g. MATCH -> the maker page for
// the market; QUOTING_PARAMS_UPDATE -> quoter detail).
enum DecodedEventKind {
  DECODED_EVENT_KIND_UNSPECIFIED = 0;
  // Event-backed (1:1 with the on-chain SweetSpotEvent):
  DECODED_EVENT_KIND_MATCH = 1;
  DECODED_EVENT_KIND_TRADE_SETTLEMENT = 2;
  DECODED_EVENT_KIND_CREATE_SPOT = 3;
  DECODED_EVENT_KIND_CREATE_MAKER = 4;
  DECODED_EVENT_KIND_ADD_MICRO_BOOK = 5;
  DECODED_EVENT_KIND_DEPOSIT_WITHDRAW = 6;
  DECODED_EVENT_KIND_CREATE_GLOBAL_ADMIN = 7;
  DECODED_EVENT_KIND_SWAP = 8;
  DECODED_EVENT_KIND_CROSS_CANCEL = 9;
  DECODED_EVENT_KIND_CROSS_FILL = 10;
  DECODED_EVENT_KIND_WITHDRAW_FEES = 11;
  DECODED_EVENT_KIND_SET_GLOBAL_ADMIN = 12;
  // Synthetic (recovered from instruction data, no CPI event):
  DECODED_EVENT_KIND_MANAGE_MAKER = 13;
  DECODED_EVENT_KIND_CROSS_SPREAD_UPDATE = 14;
  DECODED_EVENT_KIND_QUOTING_PARAMS_UPDATE = 15;
}

// One decoded Flint-program event within a transaction - a single row of the
// indexer's `events` table. Optional fields are unset when the column is null
// for that event kind (e.g. a CreateSpot carries no side/price/size). Full
// type-specific detail rides as JSON in `payload_json`; `kind` says how to read
// it. The `pair` / `maker_id` / `counterparty` fields are what make each row
// deep-linkable in the dashboard.
message DecodedEvent {
  DecodedEventKind kind = 1;
  // Trading pair the event touches; unset for venue-global events.
  Pair pair = 2;
  // The maker whose liquidity/account the event concerns; unset when not
  // applicable. Deep-links to the maker page.
  MakerId maker_id = 3;
  // The other party, meaning depends on kind: the taker's signer pubkey for a
  // MATCH, the opposing maker for a CROSS_FILL, the mint for a CREATE_SPOT
  // (base58). Unset when not applicable.
  optional string counterparty = 4;
  // Taker side for fills; SIDE_UNSPECIFIED when the event has no side.
  Side side = 5;
  // Price / size of the fill. `Decimal` is a message, so these carry field
  // presence: they are left UNSET (absent on the wire -> null client-side, not a
  // zero-value) for non-trading event kinds like CREATE_SPOT / CREATE_MAKER /
  // ADD_MICRO_BOOK / WITHDRAW_FEES, mirroring the indexer's `Option<Decimal>`.
  // No `optional` keyword is needed - that only adds presence to bare scalars.
  Decimal price = 6;
  Decimal size = 7;
  // On-chain order id (formatted) when the event references one.
  optional string order_id = 8;
  // Position within the transaction, used to order rows and address the row
  // uniquely (mirrors GetQuote's addressing).
  uint32 instruction_index = 9;
  uint32 event_index = 10;
  // Signing quoting-authority pubkey (base58) for quote-bearing events;
  // populated once the quote-tape join lands (stats iteration), unset until
  // then. Deep-links to quoter detail.
  optional string quoter = 11;
  // Raw decoded SweetSpotEvent as JSON, for type-specific detail rendering.
  string payload_json = 12;
}

// Address a transaction by its base58 signature.
message GetDecodedTransactionRequest {
  string signature = 1;
}

message GetDecodedTransactionResponse {
  // Base58 signature (echoed).
  string signature = 1;
  uint64 slot = 2;
  // Chain (block) time of the transaction.
  Timestamp block_time = 3;
  TransactionStatus status = 4;
  // Transaction fee in lamports. Unset when tx meta was not archived for this
  // transaction (rows indexed before tx-meta capture landed).
  optional uint64 fee_lamports = 5;
  // Decoded Flint-program events, ordered by (instruction_index, event_index).
  repeated DecodedEvent events = 6;
}

// -- SDK access ------------------------------------------------------

message GetSdkCredentialsRequest {}

message GetSdkCredentialsResponse {
  // Plaintext registry token (read-only pull scope). Stable: every call
  // returns the same token, so clients never need to store it.
  string token = 1;
  // Server-assigned token name, e.g. "sdk-maker-12-mainnet".
  string token_name = 2;
  // Cargo sparse-index URL for `[registries.<registry_name>]` in
  // .cargo/config.toml.
  string index_url = 3;
  // Registry name for Cargo.toml / .cargo/config.toml, e.g. "kellnr".
  string registry_name = 4;
  // When the token was first minted.
  Timestamp created_at = 5;
}

// -- Maker admin messages (maker_admin/messages.proto) --------------------------------------------------------

// Aggregate of one quoting authority's currently-open sessions.
message QuotingAuthority {
  // Quoting keypair pubkey (base58) that authenticated the sessions.
  string quoter = 1;
  // Number of open session streams across all instances.
  uint32 open_sessions = 2;
  // Most recent heartbeat seen for any of this authority's sessions.
  Timestamp last_seen = 3;
  // Best (lowest) live round-trip across this authority's sessions, micros.
  // Zero when no session has completed a heartbeat round trip yet.
  uint64 min_rtt_micros = 4;
  // Distinct flint-server regions this authority is connected to.
  repeated string regions = 5;
  // Distinct client SDK build strings observed.
  repeated string sdks = 6;
  // Most recent time a quote (SubmitTx) from this authority was received by the
  // server. Absent if none seen recently.
  optional Timestamp last_submitted_at = 7;
  // Most recent time a tx from this authority's keypair confirmed on-chain.
  // Absent if none landed recently. The gap from `last_submitted_at` shows
  // whether submitted quotes are actually landing.
  optional Timestamp last_landed_at = 8;

  // Lamport balance of the quoting keypair - the account paying this
  // authority's transaction fees, so a falling balance is a quoting outage in
  // waiting. Lamports, not SOL: the balance is an exact integer on chain and
  // stays one here (divide by 1e9 to display). Absent when the balance has
  // not been read yet, or when the RPC lookup for it failed - absent means
  // "unknown", never "zero".
  optional uint64 sol_lamports = 9;
  // When `sol_lamports` was read. The balance is polled rather than streamed,
  // so this is how stale the number is. Absent whenever `sol_lamports` is.
  optional Timestamp sol_balance_as_of = 10;

  // Heartbeat round-trip distribution across this authority's sessions over
  // the last 5 minutes, micros. `min_rtt_micros` is the best live round trip;
  // these are the recent shape of it, and p99 is where a quoter actually
  // feels the tail. Both zero when `rtt_samples` is zero.
  uint64 p50_rtt_micros = 11;
  uint64 p99_rtt_micros = 12;
  // Round trips the two percentiles were computed from. `0` means no session
  // completed a heartbeat in the window and the percentiles are meaningless.
  uint32 rtt_samples = 13;

  // Recent history behind the point-in-time fields above: latency, quote
  // rate, and transaction outcomes in fixed buckets on one shared x axis, so
  // a latency spike can be read against the quoting and landing alongside it.
  // One bucket type rather than three parallel series so the three charts are
  // always sampled over identical boundaries.
  //
  // Ordered oldest-first, with every bucket in the window present including
  // the empty ones, so a client can plot the array directly without
  // reconstructing gaps. Buckets are aligned to wall-clock multiples of
  // `ListQuotingAuthoritiesResponse.bucket_seconds`, which makes the newest
  // one partial. Always populated - an empty entry means the authority was
  // silent for that bucket, never that the series was omitted.
  //
  // Keyed on the authority (the keypair), not on a session: sessions come and
  // go mid-window, and a quoter that reconnects has not stopped quoting.
  repeated QuoterSeriesBucket series = 14;
}

// One currently-open session stream.
message QuoterSession {
  // Per-stream id minted by the server at connect.
  string session_id = 1;
  // Quoting keypair pubkey (base58).
  string quoter = 2;
  Timestamp connected_at = 3;
  // Most recent heartbeat round trip for this session.
  Timestamp last_seen = 4;
  // Peer IP as seen by the server (proxy-forwarded first hop when present).
  string ip = 5;
  string sdk = 6;
  string proto_rev = 7;
  // Latest heartbeat round-trip time, micros (0 before the first round trip).
  uint64 rtt_micros = 8;
  // Latest estimated client clock drift, micros (signed; 0 if unknown).
  int64 drift_micros = 9;
  // flint-server deploy region that owns this stream.
  string region = 10;
  // Per-process id of the flint-server instance that owns this stream.
  string server_instance = 11;
  // Approximate location of the connecting quoter, derived from `ip`. Absent
  // until a GeoIP city database is wired in; treat precision as illustrative.
  optional LatLng session_lat_lng = 12;
  // Approximate location of the flint-server `region`. Absent for unmapped
  // regions (e.g. local/dev).
  optional LatLng server_lat_lng = 13;
  // Most recent time this session's quoter submitted a quote (SubmitTx) to the
  // server. Per quoter key, so all of a quoter's sessions share the value.
  optional Timestamp last_submitted_at = 14;
  // Most recent time a tx from this session's quoter keypair confirmed
  // on-chain. Per quoter key. The gap from `last_submitted_at` shows whether
  // submitted quotes are landing.
  optional Timestamp last_landed_at = 15;
}

message ListQuotingAuthoritiesRequest {
  // Optional: restrict to a single quoting authority (pubkey), for the
  // quoter-detail page. Empty returns every one of the caller's authorities.
  // Same filter semantics as `ListQuoterSessionsRequest.quoter`.
  string quoter = 1;
}

message ListQuotingAuthoritiesResponse {
  // Ordered by open-session count, descending.
  repeated QuotingAuthority authorities = 1;

  // Window and resolution of the `series` carried on every authority above.
  // One window per response rather than per row, so every authority's charts
  // share an x axis and are directly comparable.
  //
  // Bucket width, echoed rather than assumed. Currently a fixed 5 minutes at
  // 30-second buckets (10 buckets); read the width from here and the horizon
  // from `series_start`/`series_end` rather than hard-coding either, so a
  // later change of resolution is not a client break.
  uint32 bucket_seconds = 2;
  // `series_start` inclusive, `series_end` exclusive.
  Timestamp series_start = 3;
  Timestamp series_end = 4;
}

message ListQuoterSessionsRequest {
  // Optional: restrict to a single quoting authority (pubkey). Empty returns
  // all of the caller's open sessions.
  string quoter = 1;
}

message ListQuoterSessionsResponse {
  // Ordered most-recently-seen first.
  repeated QuoterSession sessions = 1;
}

// One bucket of a quoting authority's activity.
message QuoterSeriesBucket {
  // Bucket start (inclusive).
  Timestamp ts = 1;

  // -- Latency -------------------------------------------------------
  // Heartbeat round trips completed within the bucket, across all of the
  // authority's sessions. `0` means the authority was idle or disconnected
  // for the bucket and the two percentiles below are meaningless - that is a
  // gap in the latency line, not a drop to zero.
  uint32 rtt_samples = 2;
  uint64 p50_rtt_micros = 3;
  uint64 p99_rtt_micros = 4;

  // -- Quotes --------------------------------------------------------
  // Quote updates the server received from this authority in the bucket,
  // counted the same way `SubscribeQuotes` emits them (one per quoted leg).
  uint64 quotes = 5;

  // -- Transactions --------------------------------------------------
  // Bucketed by when the outcome was observed, not by when the transaction
  // was submitted. A tx sent near the end of one bucket usually lands in the
  // next, so `transactions_landed` in a bucket does not partition
  // `transactions_sent` in the same bucket - the two lines are rates to
  // compare, not a breakdown to sum.
  //
  // Transactions accepted from this authority by TxService (initial outcome
  // `Submitted`).
  uint64 transactions_sent = 6;
  // Confirmed on-chain.
  uint64 transactions_landed = 7;
  // Rejected pre-submit, expired before landing, or landed with an error.
  uint64 transactions_failed = 8;

  // -- Landing latency -----------------------------------------------
  // Submit-to-land time for this authority's transactions, micros: from when
  // TxService accepted the tx to when it was confirmed on-chain. Bucketed by
  // when the landing was observed, matching `transactions_landed`, so a
  // sample here belongs to a tx that may have been sent in an earlier bucket.
  //
  // Landings the two percentiles were computed from. `0` means nothing landed
  // in the bucket and the percentiles are meaningless - a gap in the line,
  // not a drop to zero. Counts only landings whose submit time is known, so
  // it can trail `transactions_landed`; it never exceeds it.
  uint32 land_samples = 9;
  uint64 p50_land_micros = 10;
  uint64 p99_land_micros = 11;
}

// -- Tx events (tx/events.proto) --------------------------------------------------------

// -- Chain-tip data --------------------------------------------------

message BlockhashEvent {
  Blockhash blockhash = 1;
  // Server's current priority-fee recommendation (microlamports per CU)
  // sampled from recent landed txs. Zero = no recommendation yet.
  uint64 recommended_cu_price = 2;
  Timestamp ts = 3;
  // Block height at which this blockhash expires (Solana
  // lastValidBlockHeight). A tx using it can no longer land once the chain's
  // block height passes this value. Lets clients compute the exact remaining
  // validity of each broadcast hash and pick one matching a desired lifetime.
  // Zero before the server's height clock has warmed up.
  uint64 last_valid_block_height = 4;
}

message SlotEvent {
  uint64 slot = 1;
  Timestamp ts = 2;
}

// One upcoming leader and the TPU socket to reach it on, so a client can
// send a tx straight at the validator that will produce the block instead of
// paying the extra hop through `SubmitTx`.
//
// Direct send is a latency optimization, not a replacement for `SubmitTx`:
// leaders throttle QUIC connections from unstaked peers, so a client with no
// stake-weighted path should treat these sends as best-effort and keep
// `SubmitTx` as the path it relies on for landing.
//
// Events are idempotent and keyed by `start_slot`. On connect the server
// replays every leader currently inside its lookahead window, then pushes
// each new one as the schedule advances, so a fresh or restarted client is
// never blind. Each event self-expires once the chain passes `end_slot`;
// there are no cancel messages. Contact info can change mid-epoch (a
// validator restarting on a new address), in which case the server re-sends
// the same `start_slot` with updated fields - last write wins.
message LeaderEvent {
  // Inclusive first slot of this leader's window.
  uint64 start_slot = 1;
  // Inclusive last slot of this leader's window. Solana assigns leaders in
  // runs of 4 consecutive slots, so this is normally `start_slot + 3`; it is
  // sent explicitly rather than assumed so consecutive runs by the same
  // validator can be coalesced into one event.
  uint64 end_slot = 2;
  // TPU address as advertised in the validator's gossip contact info.
  // Dotted-quad IPv4 today; typed as a string so an IPv6 leader does not
  // require a wire change.
  string tpu_ip = 3;
  // QUIC TPU port - the one to send to. Zero when the leader advertises no
  // QUIC TPU socket, in which case the client must not attempt a direct send
  // and should fall back to `SubmitTx` for that window.
  uint32 tpu_quic_port = 4;
  // Server send time, for client-side latency attribution. Not policy.
  Timestamp ts = 5;
}

// -- Per-tx lifecycle ------------------------------------------------

// Server has accepted the tx for submission (pre-confirmation).
message TxAckEvent {
  // Base58 transaction signature.
  string signature = 1;
  Timestamp ts = 2;
}

// Tx observed on-chain at Processed commitment with err == None.
// Not Solana "confirmed" or "finalized" commitment - reserves that name
// for a future supermajority-stake signal.
message TxLandedEvent {
  // Base58 transaction signature.
  string signature = 1;
  uint64 slot = 2;
  Timestamp ts = 3;
}

// Tx landed but reverted, or timed out before landing.
message TxFailedEvent {
  // Base58 transaction signature.
  string signature = 1;
  string reason = 2;
  Timestamp ts = 3;
}

// Multiplexed tx-status stream envelope.
message TxStatusEvent {
  oneof kind {
    TxAckEvent ack = 1;
    TxLandedEvent landed = 2;
    TxFailedEvent failed = 3;
  }
}

// -- Tx messages (tx/messages.proto) --------------------------------------------------------

// -- TxService request / response ------------------------------------

// Submit a fully signed Solana transaction. The server forwards the tx
// to its upstream RPC/jito paths; lifecycle events (ack/landed/failed)
// arrive on `SubscribeTxStatus`.
message SubmitTxRequest {
  // Serialized signed transaction bytes - legacy or v0. The server does
  // not modify the tx; fee payer, recent blockhash, compute budget, and
  // priority fee are all the client's choices.
  bytes transaction = 1;
  // Client wall-clock immediately before this request went out. Purely
  // informational: the server never validates it, never rejects on it, and
  // never reflects it back - it only feeds server-side latency telemetry for
  // the inbound leg. Omit (or leave zero) if the client has no useful clock.
  Timestamp client_sent_at = 2;
}

message SubmitTxResponse {
  // Parsed base58 signature from the submitted tx. Use this to correlate with
  // subsequent TxAckEvent / TxLandedEvent / TxFailedEvent received
  // on `SubscribeTxStatus`.
  string signature = 1;
  // Server wall-clock timestamp the submission was accepted for queueing.
  Timestamp ts = 2;
  // Server wall-clock immediately before this response was written.
  //
  // `server_replied_at - ts` is the server's own handling time. Both stamps
  // come off the same clock, so that difference is free of cross-machine
  // skew; so is the client's own send-to-receive round trip, measured on the
  // client's clock. Subtracting one from the other leaves network time. The
  // client is the only place both halves exist, so the client does that math
  // - the server publishes no end-to-end number.
  Timestamp server_replied_at = 3;
}

// Retrieve the set of fee-payer pubkeys the server will pay tx fees for.
// Clients may use any returned pubkey as the `feePayer` of a submitted tx;
// the server then covers the SOL cost on landing.
message GetSponsoredPayersRequest {}

message GetSponsoredPayersResponse {
  // Base58 fee-payer pubkeys.
  repeated string payers = 1;
}

message SubscribeBlockhashRequest {}

message SubscribeSlotsRequest {}

// The server owns the lookahead depth. Kept as an empty message rather than
// no-arg so a future client-chosen lookahead or filter is an added field, not
// a new RPC.
message SubscribeLeadersRequest {}

// Server streams status transitions for any tx submitted under the
// authenticated maker's session. No client-side filter - the maker_id
// on the session token scopes the stream.
message SubscribeTxStatusRequest {}

// -- Spread protect events (spread_protect/events.proto) --------------------------------------------------------

// SpreadProtect stream event. The server can reset the client's local advisory
// state, then send idempotent widen windows that layer on top of the current
// default multiplier.
message SpreadProtectEvent {
  oneof kind {
    ResetState reset_state = 1;
    WidenWindow widen_window = 2;
  }
}

// Replace the client's local SpreadProtect state.
message ResetState {
  // Baseline multiplier to apply when no widen window is active.
  Decimal default_spread_mult = 1;
}

// An advisory request to widen the maker's own spread for a range of slots
// where network topology makes it harder to reprice in time (v0: bad leaders).
//
// The window is a slot range and self-expires at `end_slot`. There are no
// cancel messages and no separate validity field; messages are idempotent. If
// one is lost the maker simply does not widen for that window and the on-chain
// price band covers it.
//
// `spread_mult` is a multiplier on the maker's *own* spread, not an absolute
// bps value and not a score. Because the maker's spread already encodes pair
// volatility, the multiplier scales protection by vol for free and stays
// pair-agnostic, so the server never needs to know vol.
//
// Future versions add fields to this same message shape (e.g. a priority-fee
// decay slope or geo-latency term) without changing the consumer interface.
message WidenWindow {
  // Inclusive first slot of the window.
  uint64 start_slot = 1;
  // Inclusive last slot of the window.
  uint64 end_slot = 2;
  // >= 1.0. Multiplier the maker MAY apply to its own spread for the window.
  // Decimal string (e.g. "1.5") so the value is exact on the wire.
  Decimal spread_mult = 3;
  // Server send time, for client-side latency/attribution. Not policy.
  Timestamp ts = 4;
}

// -- Spread protect messages (spread_protect/messages.proto) --------------------------------------------------------

// Subscribe to the SpreadProtect widen-window feed. No filters: the policy is
// server-side and the same windows apply to every maker.
message SubscribeSpreadProtectRequest {}

// -- Historical messages (historical/messages.proto) --------------------------------------------------------

// -- Historical primitives -------------------------------------------

enum CandleInterval {
  CANDLE_INTERVAL_UNSPECIFIED = 0;
  CANDLE_INTERVAL_1M = 1;
  CANDLE_INTERVAL_5M = 2;
  CANDLE_INTERVAL_15M = 3;
  CANDLE_INTERVAL_30M = 4;
  CANDLE_INTERVAL_1H = 5;
  CANDLE_INTERVAL_4H = 6;
  CANDLE_INTERVAL_1D = 7;
}

message Candle {
  // Candle open time.
  Timestamp ts = 1;
  Decimal open = 2;
  Decimal high = 3;
  Decimal low = 4;
  Decimal close = 5;
  // Base-asset volume over the interval.
  Decimal volume = 6;
}

// -- GetFills --------------------------------------------------------

message GetFillsRequest {
  Pair pair = 1;
  // Inclusive lower bound. Unset / zero-micros = unbounded (server uses
  // its retention window).
  Timestamp start = 2;
  // Exclusive upper bound. Unset / zero-micros = now.
  Timestamp end = 3;
  // Max rows to return. Zero = server default (50). Hard cap 1000.
  uint32 limit = 4;
}

message GetFillsResponse {
  // Ordered newest-first.
  repeated FillEvent fills = 1;
}

// -- GetCandles ------------------------------------------------------

message GetCandlesRequest {
  Pair pair = 1;
  CandleInterval interval = 2;
  // Inclusive lower bound.
  Timestamp start = 3;
  // Exclusive upper bound. Unset / zero-micros = now.
  Timestamp end = 4;
}

message GetCandlesResponse {
  // Ordered oldest-first. Hard cap of 10_000 candles per response.
  repeated Candle candles = 1;
}

// -- Escalation messages (escalation/messages.proto) --------------------------------------------------------

// -- EscalationService messages --------------------------------------
//
// Out-of-band alarm path for makers. When something looks wrong on the
// venue (stuck fills, balance drift, a suspected outage) a maker can page
// the on-call team directly rather than going through email/support. The
// page is fanned out to the team's alerting channel; this RPC confirms only
// that the page was accepted and dispatched, not that anyone has responded.
//
// This is the high-urgency path - it wakes the on-call. Low-urgency matters
// belong in Slack / Telegram, not here.

message PageTheTeamRequest {
  // Free-text description of the problem. Required; the server rejects an
  // empty or whitespace-only message with INVALID_ARGUMENT. Keep it short -
  // it is delivered verbatim into the on-call alert.
  string message = 1;
}

message PageTheTeamResponse {
  // Server-minted id for this page, echoed in the on-call alert so the maker
  // and the team can refer to the same incident.
  string page_id = 1;
  // When the server accepted and dispatched the page.
  Timestamp received_at = 2;
}

// -- Quote messages (quote/messages.proto) --------------------------------------------------------

// The on-chain update kind a `QuoteUpdate` carries. Tells the client how to
// interpret `payload_json`. `None` (params-only) updates are never emitted as
// quotes, so there is no variant for them; UNSPECIFIED is the proto3 default
// and should not appear on a real quote.
enum QuoteUpdateKind {
  QUOTE_UPDATE_KIND_UNSPECIFIED = 0;
  // Explicit price levels (place/cancel deltas) - `payload_json`: {bids, asks}.
  QUOTE_UPDATE_KIND_ORDERLIST = 1;
  // Ladders of offsets from the oracle - `payload_json`: {risk, buy_orders, sell_orders}.
  QUOTE_UPDATE_KIND_ORACLE_OFFSET = 2;
  // Parametric distribution around the fair - `payload_json`: {risk, client_order_id, ...}.
  QUOTE_UPDATE_KIND_LINEAR_DISTRIBUTION = 3;
  // Fast-path oracle-offset fair refresh - `payload_json`: {last_oracle_slot, oracle_sequence, buy_fair, sell_fair}.
  QUOTE_UPDATE_KIND_ORACLE_FAIR = 4;
  // Fast-path linear-distribution fair refresh - `payload_json`: {last_oracle_slot, oracle_sequence, buy_start_price, ...}.
  QUOTE_UPDATE_KIND_LINEAR_FAIR = 5;
}

// A single on-chain quote update (`UpdateQuotingParams` carrying a concrete
// strategy, or a fast-path fair refresh) for one maker on one market. Emitted
// live on the quote stream and returned by the history query in the same shape.
//
// Strategy detail is heterogeneous - orderlist levels, oracle offsets, or
// linear-distribution params - so it is carried as JSON in `payload_json`
// rather than a fixed schema. `strategy` says how to interpret it.
message QuoteUpdate {
  // Maker's quoting spot; the market is this spot vs the global numeraire.
  uint32 spot_id = 1;
  // Block (chain) time of the quote.
  Timestamp block_time = 2;
  uint64 slot = 3;
  // Transaction signature (base58) that carried the update.
  string signature = 4;
  // Index of the instruction within the tx, and of the param-leg within the
  // instruction (one update can touch several spots).
  uint32 instruction_index = 5;
  uint32 leg_index = 6;
  uint32 maker_id = 7;
  // Signing quoting-authority pubkey (base58).
  string quoter = 8;
  // Which quote-update shape this record carries.
  QuoteUpdateKind strategy = 9;
  // The book's enable flag carried by this update. For fast-fair refreshes,
  // this is always true because the instruction does not carry enable state.
  bool enabled = 10;
  // Soft inventory cap in atoms; max-uint64 is the no-cap sentinel. Fast-fair
  // refreshes use the sentinel because the instruction does not carry cap state.
  uint64 soft_max_balance = 11;
  // Strategy-specific detail as JSON (levels / offsets / risk params).
  string payload_json = 12;
}

// Live quote subscription filters, applied within the authenticated maker.
// Each repeated field is OR-within and the dimensions are AND-across; an empty
// field is a wildcard on that dimension.
message SubscribeQuotesRequest {
  reserved 1; // was maker_ids - the RPC is scoped to the authenticated maker
  // Markets (quoting spots) to include; empty = all of the maker's markets.
  repeated uint32 spot_ids = 2;
  // Signing quoter pubkeys (base58) to include; empty = all of the maker's quoters.
  repeated string quoters = 3;
}

// Historical quote query. Same filter semantics as the stream, over a time
// window, newest first; scoped to the authenticated maker.
message ListRecentQuotesRequest {
  reserved 1; // was maker_ids - the RPC is scoped to the authenticated maker
  repeated uint32 spot_ids = 2;
  repeated string quoters = 3;
  // Inclusive lower/upper bounds on block time. `0`/absent = unbounded.
  optional Timestamp start = 4;
  optional Timestamp end = 5;
  // Max rows to return. Server clamps to a sane ceiling; 0 = server default.
  uint32 limit = 6;
}

message ListRecentQuotesResponse {
  // Newest first.
  repeated QuoteUpdate quotes = 1;
}

// Exact on-chain identity of a single quote update. All three fields are
// required to address a unique row: one transaction can carry several quote
// instructions, each touching several spots (legs).
message GetQuoteRequest {
  string signature = 1;
  uint32 instruction_index = 2;
  uint32 leg_index = 3;
}

message GetQuoteResponse {
  QuoteUpdate quote = 1;
}

Built on Solana