frUSD Deposits (Ethereum)
Track SUBFROST frUSD deposits made on Ethereum, indexed by BTC recipient ("has this user's crosschain deposit landed on Ethereum yet, and for how much?"), served from a dedicated evmshrew indexer.
When a user deposits into the frUSD vault on Ethereum, the mint carries the BTC
recipient the bridged value is destined for. This index joins each PaymentQueued
event back to that recipient, so a single lookup answers the pre-unwrap window
that no Bitcoin-side index can see.
Endpoint
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/frusd
A separate path from the main /v4/YOUR_API_KEY endpoint, backed by its own
Ethereum indexer, so its height advances independently. Call frusdindexheight
to see how current it is. The data is public on-chain history; the free
/v4/jsonrpc/ethereum/frusd anchor serves it without a key.
The ABI is protobuf
Every view takes a protobuf-encoded request and returns a protobuf-encoded response, the same convention as the BTC/USD pool and the rest of the Alkanes view stack. There is no JSON on the wire and no JSON envelope inside the result.
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.frusddepositsbyrecipientrequest_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.
The request is a protobuf message, not the bare value.
frusddepositsbyrecipientwants{ recipient_script = 1 }, not the raw script bytes. Passing the raw script returns an empty0xthat looks exactly like "no deposits". Encode the request message (and do not prepend the height: the endpoint does that for you).
Views
| View | Request | Response |
|---|---|---|
frusddepositsbyrecipient | DepositsByRecipientRequest | DepositsResponse |
frusdindexheight | (empty: "0x") | IndexHeightResponse |
recipient_scriptis the raw BTC output script (scriptPubKey). For a taproot recipient that is5120++ the 32-byte x-only key. Decode the address to its script first; it is not an address string.fr_amountis a big-endianuint256with 8 decimals (matching the frUSD alkane on Bitcoin):99701199000is997.01199 frUSD. Parse with a bignum and divide by1e8for display.- An empty
DepositsResponseis not an error. It means the recipient has no deposits yet: either none were ever made, or the deposit's Ethereum block is still abovefrusdindexheight. Compare against the index height before telling a user "not found". payment_idis the vault's monotonic id, stable and unique; use it as your render key and to detect new deposits.confirmedis alwaystruetoday (a record exists only once mined) and is reserved for a future pending variant.
Generating a client
Save the schema as frusd.proto and generate:
# Python
protoc -I. --python_out=. frusd.proto
# TypeScript (ts-proto)
protoc -I. --plugin=./node_modules/.bin/protoc-gen-ts_proto \
--ts_proto_out=. frusd.proto
import requests
import frusd_pb2 as pb
ENDPOINT = "https://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/frusd"
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:])
# height the index has reached
h = pb.IndexHeightResponse()
h.ParseFromString(view("frusdindexheight"))
print("indexed to", h.height)
# a user's deposits, by BTC recipient script (taproot 5120…)
script = bytes.fromhex("5120475c097fdebbc6900e42d6fc8603d2f6030f43a77bfe02b9547a32ecb2df342e")
resp = pb.DepositsResponse()
resp.ParseFromString(view("frusddepositsbyrecipient",
pb.DepositsByRecipientRequest(recipient_script=script)))
for d in resp.deposits:
frusd = int.from_bytes(d.fr_amount, "big") / 1e8
print(d.payment_id, f"{frusd:.2f} frUSD", d.block_number, "0x" + d.tx_hash.hex())
import {
DepositsByRecipientRequest, DepositsResponse, IndexHeightResponse,
} from "./frusd";
const ENDPOINT = "https://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/frusd";
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"));
}
// how current is the index?
const h = IndexHeightResponse.decode(await view("frusdindexheight"));
// a user's deposits
const script = Buffer.from("5120475c097fdebbc6900e42d6fc8603d2f6030f43a77bfe02b9547a32ecb2df342e", "hex");
const req = DepositsByRecipientRequest.encode({ recipientScript: script }).finish();
const { deposits } = DepositsResponse.decode(await view("frusddepositsbyrecipient", req));
for (const d of deposits) {
const frUsd = Number(BigInt("0x" + Buffer.from(d.frAmount).toString("hex"))) / 1e8;
console.log(d.paymentId, `${frUsd.toFixed(2)} frUSD`, d.blockNumber, "0x" + Buffer.from(d.txHash).toString("hex"));
}
Rendering & tracking deposits
A "your deposits" panel or a live tracker follows the same shape:
- Resolve the user's BTC recipient script, the taproot output they bridged
to. Convert address →
scriptPubKey(taproot →5120<x-only-key>). - Call
frusddepositsbyrecipient. Render each deposit: amount (fr_amount / 1e8), the Ethereum tx (tx_hash→ explorer link),block_number, andpayment_id(stable key). - Show sync context with
frusdindexheight. If a user's expected deposit block is above the index height, show "indexing…", not "not found". Confirmations ≈frusdindexheight − block_number. - Poll ~15s for a live tracker (ETH block time ≈ 12s). Detect new deposits by
the max
payment_idseen. Cache by(recipient, indexHeight); deposits are append-only per recipient, so only refetch when the height moves.
Schema
syntax = "proto3";
package frusd;
message DepositsByRecipientRequest {
bytes recipient_script = 1; // raw BTC scriptPubKey (taproot = 0x5120 ++ x-only key)
}
message DepositsResponse {
repeated Deposit deposits = 1;
}
message Deposit {
uint64 payment_id = 1; // vault paymentId (monotonic, stable id)
bytes fr_amount = 2; // frUSD amount, big-endian uint256, 8 decimals
uint64 block_number = 3; // Ethereum block mined in
bytes tx_hash = 4; // depositAndBridge tx hash (32 bytes)
bool confirmed = 5; // always true today (record == mined)
}
message IndexHeightRequest {}
message IndexHeightResponse {
uint64 height = 1; // highest Ethereum block indexed
}