BTC/USD Pool

Live state for the SUBFROST BTC/USD pool — reserves, LP supply, the pool's own price, executed trades and OHLC candles — served from a dedicated indexer.

The pool is a Curve CryptoSwap (v2) port running on Alkanes. It trades frUSD (4:1776) against frBTC (32:0), both 8-decimal, behind the self-upgradeable proxy 4:1778, which holds all pool state and is itself the frBTCUSD LP token.

Endpoint

POST https://mainnet.subfrost.io/v4/<your-api-key>/btcusd

A separate path from the main /v4/<your-api-key> endpoint, backed by its own indexer, so its height advances independently — call indexheight to see how current it is.

The ABI is protobuf

Every view takes a protobuf-encoded request and returns a protobuf-encoded response, following the same convention as alkanes-rs and the rest of the Alkanes view stack. There is no JSON on the wire and no JSON envelope inside the result. Turning these messages into JSON — or into your own types — is your code's job, which is the point: you decode straight into generated structs with no lossy string round-trip in between.

Calls go through metashrew_view:

{
  "jsonrpc": "2.0",
  "method": "metashrew_view",
  "params": ["<view_name>", "<request_hex>", "latest"],
  "id": 1
}
  • view_name — one of the views below, e.g. getprice
  • request_hex0x + hex of the serialized protobuf request. Views taking no arguments accept "0x".
  • latest — the height tag.

The result is a 0x-prefixed hex string: hex-decode it, then parse the bytes as the matching protobuf response.

An empty or truncated request is not an error. Protobuf has no required fields, so a malformed body decodes to a default message and the view answers as if you asked about nothing — found: false. There is no error string to check. Always read found (or ok), never the absence of an error.

Generating a client

Save the schema as cryptoswap.proto and generate:

# Python
protoc -I. --python_out=. cryptoswap.proto

# TypeScript (ts-proto)
protoc -I. --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=. cryptoswap.proto
import json, requests
import cryptoswap_pb2 as pb

ENDPOINT = "https://mainnet.subfrost.io/v4/<your-api-key>/btcusd"

def view(name: str, req=None) -> bytes:
    payload = req.SerializeToString() if req is not None else b""
    r = requests.post(ENDPOINT, json={
        "jsonrpc": "2.0", "method": "metashrew_view",
        "params": [name, "0x" + payload.hex(), "latest"], "id": 1,
    }).json()
    if "error" in r:
        raise RuntimeError(f"{name}: {r['error']['message']}")
    return bytes.fromhex(r["result"][2:])

pool = pb.AlkaneId(block=4, tx=1778)

resp = pb.GetPriceResponse()
resp.ParseFromString(view("getprice", pb.GetPriceRequest(pool=pool)))

print(resp.marginal_price_q)                              # '15585063275356'
print(int(resp.price_q_scale) // int(resp.marginal_price_q))  # 64164  (USD per BTC)
import {
  AlkaneId, GetPriceRequest, GetPriceResponse,
} from "./cryptoswap";

const ENDPOINT = "https://mainnet.subfrost.io/v4/<your-api-key>/btcusd";

async function view(name: string, req?: Uint8Array): Promise<Uint8Array> {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: "metashrew_view",
      params: [name, "0x" + Buffer.from(req ?? new Uint8Array()).toString("hex"), "latest"],
      id: 1,
    }),
  });
  const { result, error } = await res.json();
  if (error) throw new Error(`${name}: ${error.message}`);
  return Uint8Array.from(Buffer.from(result.slice(2), "hex"));
}

const pool = AlkaneId.create({ block: 4, tx: 1778 });
const bytes = await view("getprice", GetPriceRequest.encode({ pool }).finish());
const price = GetPriceResponse.decode(bytes);

const usdPerBtc = BigInt(price.priceQScale) / BigInt(price.marginalPriceQ!); // 64164n

Reading prices

price_q is token1 per token0, in RAW token units, scaled by price_q_scale (1e18). It is NOT a USD figure, and the direction is not the one most people expect: token0 is frUSD and token1 is frBTC, so price_q is frBTC per frUSD. USD per BTC is its reciprocal.

Token slots are ordered by alkane id, not by role — token0 is whichever of the pair has the lower (block, tx). For this pool that is frUSD 4:1776, with frBTC 32:0 as token1. Read token0/token1 off the response rather than assuming.

For a pair whose tokens share a decimal count (as here — both 8-decimal):

usd_per_btc = price_q_scale / marginal_price_q
            = 1e18 / 15585063275356
            ≈ 64164

For a pair with differing decimals, convert the raw ratio to display units by multiplying by 10 ** (decimals0 - decimals1) first.

Every amount and price is a DECIMAL STRING, not an integer field. The pool's state does not fit protobuf's integer types: total_supply is currently 23113653069174808444 — past u64 — and D scales with reserves. Parse with a bignum (int in Python, BigInt in TS), never a float.

Two prices, and which to use

FieldMeaningAvailable
executed_price_qPrice of the pool's most recent actual tradeOnly after the pool has traded
marginal_price_qThe pool's own current price, from its price_scaleFrom the pool's first block
oracle_price_qThe pool's internal EMA of recent trade pricesFrom the pool's first block

Prefer marginal_price_q for a quote and executed_price_q for "what did someone actually pay". The executed fields are optional and absent until the pool has been traded against — a freshly seeded pool has liquidity and a price but no trades, which is exactly its state today. All three share the same token1-per-token0 convention, so they are directly comparable.

Views

ViewRequestResponse
pingPingRequestPingResponse
indexheightIndexHeightRequestIndexHeightResponse
getblocksummaryGetBlockSummaryRequestGetBlockSummaryResponse
getrawvalueGetRawValueRequestGetRawValueResponse
getpoolsGetPoolsRequestGetPoolsResponse
getpoolGetPoolRequestGetPoolResponse
getpoolstateGetPoolStateRequestGetPoolStateResponse
getreservesGetReservesRequestGetReservesResponse
getswapsGetSwapsRequestGetSwapsResponse
getpriceGetPriceRequestGetPriceResponse
getcandlesGetCandlesRequestGetCandlesResponse
getcandleatGetCandleAtRequestGetCandleAtResponse

Notes that are not obvious from the schema:

  • getpool returns the pool record plus its latest state, so it saves a round trip over getpool + getpoolstate. state is absent until the pool has written state.
  • getpoolstate carries both the contract's raw slots (price_scale, price_oracle, last_prices — in the CONTRACT's coin order, between decimal-normalised balances) and the re-oriented *_price_q fields. Use the _q fields unless you are reproducing contract internals; the raw slots are not in the same orientation, and for this pool the two orders are opposite.
  • getcandles only supports 3600 and 86400 second buckets. An unsupported width returns ok: false with supported_buckets populated rather than an error. Candles are built from executed trades, so a pool with no trades has no candles even though it has a price.
  • getrawvalue reads any single storage slot of a tracked pool — key: "/price_scale" — returning the raw little-endian bytes the contract wrote. Use it to reach state the typed views do not model.
  • getswaps and getpools paginate with page/limit. Omitting limit returns everything (capped at 20000).
  • ann is A * n^n * A_MULTIPLIER (CryptoSwap amplification), not a StableSwap A. Fees are over 1e10: mid_fee 20000000 is 0.2% (20 bps), out_fee 80000000 is 0.8%, and admin_fee 5000000000 is 50% — of the swap fee, not of the trade. The charged fee slides between mid_fee (balanced) and out_fee (imbalanced).

Schema

syntax = "proto3";
package alspo.cryptoswap;

message AlkaneId {
  uint32 block = 1;
  uint64 tx = 2;
}

message PingRequest {}
message PingResponse { string message = 1; }

message IndexHeightRequest {}
message IndexHeightResponse { optional uint32 height = 1; }

message GetBlockSummaryRequest { uint32 height = 1; }
message GetBlockSummaryResponse {
  bool found = 1;
  uint32 height = 2;
  string blockhash = 3;              // big-endian hex
  uint32 tx_count = 4;
  uint32 swap_count = 5;
  uint32 new_pool_count = 6;
  uint32 add_liquidity_count = 7;
  uint32 remove_liquidity_count = 8;
  uint32 claim_fees_count = 9;
  uint32 admin_count = 10;
}

message GetRawValueRequest { AlkaneId pool = 1; bytes key = 2; }
message GetRawValueResponse {
  bool found = 1;
  bytes txid = 2;                    // big-endian
  bytes value = 3;                   // raw little-endian, as written
}

message Pool {
  AlkaneId pool = 1;
  AlkaneId token0 = 2;               // ordered by (block, tx)
  AlkaneId token1 = 3;
  uint32 first_seen_height = 4;
  uint64 first_seen_ts = 5;
  bool is_canonical = 6;
  bool from_init = 7;
  bool coin0_is_token0 = 8;          // false here: contract coin order is reversed
  string precision_coin0 = 9;
  string precision_coin1 = 10;
  string ann = 11;
  string gamma = 12;
  string mid_fee = 13;
  string out_fee = 14;
  string admin_fee = 15;
  string swap_count = 16;
  string volume0 = 17;
  string volume1 = 18;
}

message PoolState {
  uint32 height = 1;
  uint64 timestamp = 2;
  string reserve0 = 3;               // module token0/token1 orientation
  string reserve1 = 4;
  string total_supply = 5;
  string price_scale = 6;            // raw contract slots, CONTRACT coin order
  string price_oracle = 7;
  string last_prices = 8;
  uint64 last_prices_height = 9;
  optional string marginal_price_q = 10;  // re-oriented; use these
  optional string oracle_price_q = 11;
  optional string last_price_q = 12;
  string d = 13;
  string virtual_price = 14;
  string xcp_profit = 15;
  bool is_killed = 16;
  bool ramping = 17;
  uint64 future_ag_height = 18;
}

message GetPoolsRequest {
  optional bool canonical = 1;
  optional uint32 page = 2;
  optional uint32 limit = 3;
}
message GetPoolsResponse {
  bool ok = 1;
  uint32 page = 2;
  uint32 limit = 3;
  uint32 total = 4;
  bool has_more = 5;
  string price_q_scale = 6;
  repeated Pool pools = 7;
}

message GetPoolRequest { AlkaneId pool = 1; }
message GetPoolResponse {
  bool found = 1;
  Pool pool = 2;
  PoolState state = 3;
  string price_q_scale = 4;
}

message GetPoolStateRequest { AlkaneId pool = 1; }
message GetPoolStateResponse {
  bool found = 1;
  AlkaneId pool = 2;
  AlkaneId token0 = 3;
  AlkaneId token1 = 4;
  PoolState state = 5;
  string price_q_scale = 6;
}

message GetReservesRequest { AlkaneId pool = 1; }
message GetReservesResponse {
  bool found = 1;
  AlkaneId pool = 2;
  AlkaneId token0 = 3;
  AlkaneId token1 = 4;
  uint32 height = 5;
  uint64 timestamp = 6;
  string reserve0 = 7;
  string reserve1 = 8;
  string total_supply = 9;
}

message Swap {
  uint64 timestamp = 1;
  uint32 height = 2;
  bytes txid = 3;                    // big-endian
  uint32 vout = 4;
  AlkaneId token_in = 5;
  string amount_in = 6;
  AlkaneId token_out = 7;
  string amount_out = 8;
  string price_q = 9;
  bool zero_for_one = 10;
}

message GetSwapsRequest {
  AlkaneId pool = 1;
  optional uint32 page = 2;
  optional uint32 limit = 3;
}
message GetSwapsResponse {
  bool ok = 1;
  AlkaneId pool = 2;
  uint32 page = 3;
  uint32 limit = 4;
  uint32 total = 5;
  bool has_more = 6;
  string price_q_scale = 7;
  repeated Swap swaps = 8;
}

message GetPriceRequest { AlkaneId pool = 1; }
message GetPriceResponse {
  bool found = 1;
  AlkaneId pool = 2;
  AlkaneId token0 = 3;
  AlkaneId token1 = 4;
  string price_q_scale = 5;
  optional string executed_price_q = 6;
  optional uint64 executed_timestamp = 7;
  optional uint32 executed_height = 8;
  bytes executed_txid = 9;
  optional string marginal_price_q = 10;
  optional string oracle_price_q = 11;
  optional uint32 state_height = 12;
}

message Candle {
  uint64 bucket_start = 1;
  string open = 2;
  string high = 3;
  string low = 4;
  string close = 5;
  string volume0 = 6;
  string volume1 = 7;
  uint32 trades = 8;
}

message GetCandlesRequest {
  AlkaneId pool = 1;
  uint32 bucket = 2;                 // 3600 or 86400
  optional uint64 from = 3;
  optional uint64 to = 4;
  optional uint32 page = 5;
  optional uint32 limit = 6;
}
message GetCandlesResponse {
  bool ok = 1;
  AlkaneId pool = 2;
  uint32 bucket = 3;
  uint32 page = 4;
  uint32 limit = 5;
  uint32 total = 6;
  bool has_more = 7;
  string price_q_scale = 8;
  repeated Candle candles = 9;
  repeated uint32 supported_buckets = 10;
}

message GetCandleAtRequest {
  AlkaneId pool = 1;
  uint32 bucket = 2;
  uint64 at = 3;
}
message GetCandleAtResponse {
  bool found = 1;
  AlkaneId pool = 2;
  uint32 bucket = 3;
  Candle candle = 4;
  string price_q_scale = 5;
  repeated uint32 supported_buckets = 6;
}