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.getpricerequest_hex—0x+ 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 readfound(orok), 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_qistoken1pertoken0, in RAW token units, scaled byprice_q_scale(1e18). It is NOT a USD figure, and the direction is not the one most people expect:token0is frUSD andtoken1is frBTC, soprice_qis 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_supplyis currently 23113653069174808444 — pastu64— andDscales with reserves. Parse with a bignum (intin Python,BigIntin TS), never a float.
Two prices, and which to use
| Field | Meaning | Available |
|---|---|---|
executed_price_q | Price of the pool's most recent actual trade | Only after the pool has traded |
marginal_price_q | The pool's own current price, from its price_scale | From the pool's first block |
oracle_price_q | The pool's internal EMA of recent trade prices | From 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
| View | Request | Response |
|---|---|---|
ping | PingRequest | PingResponse |
indexheight | IndexHeightRequest | IndexHeightResponse |
getblocksummary | GetBlockSummaryRequest | GetBlockSummaryResponse |
getrawvalue | GetRawValueRequest | GetRawValueResponse |
getpools | GetPoolsRequest | GetPoolsResponse |
getpool | GetPoolRequest | GetPoolResponse |
getpoolstate | GetPoolStateRequest | GetPoolStateResponse |
getreserves | GetReservesRequest | GetReservesResponse |
getswaps | GetSwapsRequest | GetSwapsResponse |
getprice | GetPriceRequest | GetPriceResponse |
getcandles | GetCandlesRequest | GetCandlesResponse |
getcandleat | GetCandleAtRequest | GetCandleAtResponse |
Notes that are not obvious from the schema:
getpoolreturns the pool record plus its lateststate, so it saves a round trip overgetpool+getpoolstate.stateis absent until the pool has written state.getpoolstatecarries 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_qfields. Use the_qfields unless you are reproducing contract internals; the raw slots are not in the same orientation, and for this pool the two orders are opposite.getcandlesonly supports 3600 and 86400 second buckets. An unsupported width returnsok: falsewithsupported_bucketspopulated 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.getrawvaluereads 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.getswapsandgetpoolspaginate withpage/limit. Omittinglimitreturns everything (capped at 20000).annisA * n^n * A_MULTIPLIER(CryptoSwap amplification), not a StableSwapA. Fees are over 1e10:mid_fee20000000 is 0.2% (20 bps),out_fee80000000 is 0.8%, andadmin_fee5000000000 is 50% — of the swap fee, not of the trade. The charged fee slides betweenmid_fee(balanced) andout_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;
}