# SUBFROST Documentation
> Official docs for SUBFROST on Bitcoin L1: frBTC, frUSD, DIESEL, FIRE, the Alkanes metaprotocol, the SUBFROST API and alkanes-cli.
This file contains all documentation content in a single document following the llmstxt.org standard.
## Get Started in 5 Minutes
This guide takes you from zero to your first swap. You will pick how to access SUBFROST, set up a wallet, and swap some Bitcoin.
### Step 1: Open SUBFROST
The **web app** works everywhere, and there are two native clients out already:
- **Web app.** Works in any browser, and the quickest way to get started.
- **Android.** A direct install file. No app store account needed. Google Play listing coming soon.
- **Chrome browser extension.** On the Chrome Web Store.
- **iOS.** Coming soon (in App Store review).
For this guide we will use the web app. See [Wallets](../using-subfrost/wallets) for details on each option.
### Step 2: Set up a wallet
You have two paths. Either works.
#### Create a new SUBFROST wallet (recommended for newcomers)
1. Open the Connect Wallet menu and choose **Create New Wallet**.
2. Set a strong password (at least 8 characters).
3. **Write down your 12-word recovery phrase** and store it somewhere safe and offline. This phrase is the only way to recover your funds.
4. Optionally, back up to Google Drive for convenience.
#### Or connect a wallet you already have
SUBFROST connects to popular Bitcoin wallets, including **OKX, Unisat, and Xverse**. Open **Connect Wallet**, pick your provider, and approve the connection.
:::caution[Burn risk: use a separate wallet for Ordinals, Runes and BRC-20]
SUBFROST does not index `ord` at this time. You must transfer Ordinals, Runes and BRC-20 assets to a different wallet before interacting with our apps. Those assets live on a specific coin (a UTXO) that SUBFROST cannot see, so if it gets spent in the course of ordinary activity, the asset on it is gone. See [Safety](../using-subfrost/safety).
:::
### Step 3: Add Bitcoin
You do not have to manually wrap your BTC. When you swap from BTC, the app atomically wraps your Bitcoin into frBTC and performs the swap in a single step. The wrapping is completely abstracted away.
If you just want frBTC directly, wrapping is a 1:1 conversion.
### Step 4: Make your first swap
1. **Select your tokens.** Choose what you are swapping **from** (You Send) and what you want to receive (You Receive).
2. **Enter an amount.** Type it in, or use the quick buttons: 25%, 50%, 75%, or Max of your balance.
3. **Review the details.** Before confirming you will see the exchange rate, the swap route, the minimum you will receive, the deadline (in blocks), your slippage tolerance, and the miner fee rate.
4. **Confirm and sign.** Tap Swap, then approve the transaction with your connected wallet.
:::tip[Start small]
If you are new to the platform, do a small swap first to get comfortable with the flow before moving larger amounts.
:::
:::info[Swapping back to BTC takes a few blocks]
When you swap into native BTC, the protocol waits **3 to 7 block confirmations** before sending it. That wait protects the Bitcoin reserve against a chain reorg. See [Wrap & Unwrap](../using-subfrost/wrap-unwrap-frbtc) for why.
:::
### Where to go next
- **[Key Concepts](./key-concepts):** understand frBTC, DIESEL, FIRE, and the rest.
- Explore pools, the FIRE vault, and more in the **[Using SUBFROST](../using-subfrost/wallets)** guides.
---
## Key Concepts
A short glossary to get you oriented. Each item links to a deeper page where one exists. The assets are grouped into what is **Live** today and what is **Planned**.
### The assets
#### Live
- **frBTC.** Your Bitcoin, made programmable. frBTC is BTC atomically wrapped 1:1 into a token that works with on-chain apps and settles natively on Bitcoin.
- **DIESEL.** The protocol's native emission. Every Bitcoin block emits the same amount of DIESEL as new bitcoin (3.125 today) on the same halving schedule.
- **FIRE.** FIRE is the Alkanes governance token. It rewards people who provide liquidity to the DIESEL/frBTC pool, and is powered by staking and bonding. It is how active participants earn a share of the protocol and, over time, help govern it.
- **frUSD.** The Bitcoin-native stablecoin: a US-dollar token that settles on Bitcoin itself rather than on another chain, so a dollar balance can be composed into the same transactions as your BTC. It trades against frBTC in a pool on mainnet, and can be minted by bridging USDC or USDT in from Ethereum. See [frUSD](../tokens/frUSD-overview) for how it is reserved and what that asks you to trust.
#### Planned
- **dxBTC (planned).** A yield-bearing Bitcoin token. The plan is that you deposit BTC and receive dxBTC, while behind the scenes your Bitcoin will be put to work in market-neutral yield strategies without ever leaving the Bitcoin blockchain.
- **FUEL.** FUEL is the SUBFROST protocol governance token. It is planned to govern the protocol's treasury and parameters (for example, mint/redeem fees and protocol upgrades). Its tokenomics are not yet public.
:::note[Note]
Capitalization is what separates the two "fuels" you will meet in these docs: **FUEL** in uppercase is always this token, while lowercase **fuel** is the compute budget an Alkanes contract call is allowed to spend, the equivalent of gas elsewhere.
The comparison to gas stops at "budget for computation". Unlike gas, **fuel is free**: you do not buy it, you cannot top it up, and no fee is charged against it. Each block's fixed fuel budget is shared, by size, among the transactions that actually run contracts. The only thing you pay to transact is the ordinary Bitcoin miner fee, in **native BTC**, exactly as you would for a transaction that ran no contract at all. See [Alkanes](../protocol/alkanes) for the detail.
:::
### How you trade assets
- **The AMM (automated market maker).** A swap works against a shared liquidity pool instead of matching you with another trader. This efficiency always provides you with a counterparty, and the price adjusts with supply and demand. This is what powers swaps in the app.
The two fees worth knowing, both current rates rather than permanent guarantees:
| Action | Fee | Where it goes |
| --- | --- | --- |
| Swapping | 1% by default | 0.8% to liquidity providers, 0.2% permanently to the AMM protocol |
| Wrapping or unwrapping BTC | 0.1% | SUBFROST protocol |
The swap row describes a standard AMM pool. The frUSD/frBTC pool charges differently, and its liquidity providers earn differently; see [Pools & Liquidity](../using-subfrost/pools-liquidity).
See [Swap](../using-subfrost/swap), [Pools & Liquidity](../using-subfrost/pools-liquidity) and [Wrap & Unwrap](../using-subfrost/wrap-unwrap-frbtc).
- **PSBT marketplaces (partially signed Bitcoin transactions).** Across Alkanes there are several PSBT marketplaces where buyers and sellers choose to fill each other's orders. SUBFROST does not currently host one of these.
### What it all runs on
- **Alkanes.** The smart-contract protocol that these tokens live on. Alkanes lets developers run programmable contracts directly on Bitcoin (contracts are written in Rust and compiled to WebAssembly), inheriting Bitcoin's security instead of relying on a separate chain. frBTC, DIESEL, and FIRE are all Alkanes tokens. See the [Technical Overview](../protocol/alkanes) section for the full model.
- **Metashrew indexer.** Bitcoin blocks do not "run" contracts on their own. An indexer reads each new block and executes the contract code embedded in the block (specifically in the OP_RETURN). When you see your Alkanes balances, it comes from the Metashrew indexer.
:::note[One level deeper]
This section keeps things simple on purpose. Terms like protostone, cellpack, and the exact contract model are covered in the [Technical Overview](../protocol/alkanes) section, and the developer tooling (CLI, SDK, JSON-RPC) is in [Build on SUBFROST](../build/overview) and the [API & SDK Docs](../api-reference/getting-started/overview).
:::
### What is live today
Being honest about status matters. A quick snapshot:
- **Live:** Atomic wrapping of frBTC and unwrapping back to BTC, swapping in AMM pools between Bitcoin assets and BTC, holding and trading the Bitcoin-native dollar frUSD, providing liquidity to these AMM pools to earn a yield, and staking and bonding within the advanced DeFi "FIRE" vault.
- **Planned:** a yield-bearing Bitcoin vault token (dxBTC), FUEL (the SUBFROST protocol governance token), and the fully permissionless signer network.
### Where to go next
- **[Get Started in 5 Minutes](./get-started):** if you have not made your first swap yet.
- **[Tokens & Economics](../tokens-economics/overview):** the full story on frBTC, DIESEL, FIRE, dxBTC, and FUEL.
- **[Technical Overview](../protocol/alkanes):** how the custody, signing, and Alkanes model actually work.
---
## What is SUBFROST
SUBFROST is a way to use Bitcoin in decentralized finance (DeFi) without handing it to a company or leaving the Bitcoin blockchain.
Today, if you want to trade Bitcoin, earn yield on it, or use it the way people use Ethereum or Solana, you have to send it somewhere else. That means a centralized exchange, or a "wrapped BTC" bridge run by a single custodian. In both cases you are trusting one party not to lose your coins, freeze them, or misuse them.
### The problem
Bitcoin is the largest and most secure network in crypto. But its base layer was never designed to run applications. Native BTC does not plug directly into the tools people use elsewhere in DeFi: automated market makers (for swapping), lending markets, or privacy tools.
So two poor options exist for anyone wanting to do more than just hold their Bitcoin:
1. **Send it to a centralized exchange.** Now a company holds your coins.
2. **Bridge it to another chain as "wrapped BTC".** Now a bridge operator, often a small custodian, holds the real Bitcoin while you hold an IOU on a different network.
Both options mean giving up custody and trusting someone else. **Without a trust-minimized custodian, "Bitcoin DeFi" ends up relying on trust-dependent cross-chain alternatives, which is not really Bitcoin DeFi at all.**
### The core idea
A group of independent signers collectively holds the Bitcoin that backs the system. They use a method called **threshold signing**, where the key that controls the funds is split across the whole group. No single signer ever holds the full key, so no single party can move the funds on their own. If one signer is compromised, the funds stay safe.
In one sentence: **SUBFROST is a decentralized custodian for Bitcoin.** It runs as a lightweight layer-0 that watches the Bitcoin blockchain and coordinates a ring of signers who lock up native BTC and release it again when you ask.
### How you actually use it
When you put Bitcoin into SUBFROST, you receive **frBTC**, a token that is backed 1:1 by your BTC and settled natively on Bitcoin. **The best part? This can happen atomically within the same transaction as other activities.** In this way, for the first time ever, a user can swap native BTC with a Bitcoin token like DIESEL or FIRE, without ever knowing they interacted with the frBTC contract.
- **Minting (wrapping)** your BTC into frBTC is instant.
- **Redeeming (unwrapping)** frBTC back into native BTC is authorized by the distributed group of signers, not by any single person.
With frBTC, you can perform activities with native BTC such as swapping into other Bitcoin assets, providing liquidity into AMMs, lending, and earning yield in other ways, without data ever leaving Bitcoin L1. Data doesn't just settle on the base layer, it never leaves.
### What makes it different
- **You stay on Bitcoin.** SUBFROST is not a separate blockchain or a sidechain. Data always remains on Bitcoin L1 and you never lose visibility of those sats.
- **No single custodian.** The Bitcoin that backs frBTC is held by a growing number of signers together, not by one company. Currently, there are 9 signers with the ability to scale to 255.
- **Keep your exposure.** The idea is simple in spirit: never send your Bitcoin to another blockchain to use it, or be forced to trade it.
#### Where the signer set stands today
Custody is currently held by **nine signers**, and they are not yet publicly identified. That is the starting point, not the destination. Three steps are planned from here:
1. The keys move as needed so that **at least six of the nine signers are publicly identifiable (doxxed)**.
2. The **signer set expands** beyond today's nine.
3. The signer set transitions into a **permissionless staking network** with up to 255 signers, where nobody can stop you from participating.
So when you read that no single custodian holds your Bitcoin, that is true today. The larger, open network is planned, and worth judging on its own timeline. See [frBTC Peg & Custody](../protocol/frbtc-peg-and-custody) for how the signing itself works.
### Where to go next
- **[Get Started in 5 Minutes](./get-started):** pick a wallet, add Bitcoin, make your first swap.
- **[Key Concepts](./key-concepts):** a quick glossary of frBTC, DIESEL, FIRE, and more.
---
## BTCUSD Pool
The BTCUSD pool is the frUSD/frBTC market: the place where **Bitcoin trades against the dollar, settled on Bitcoin L1**.
Trading BTC against a dollar has always meant leaving the Bitcoin blockchain, until now. We brought this pair directly to Bitcoin with [frUSD](../tokens/frUSD-overview) and [frBTC](../tokens/frBTC-overview), but the user doesn't have to have either one first... just BTC or USDT/USDC.
### What trades in it
| Side | Asset | What it is |
| --- | --- | --- |
| USD | `frUSD` | The dollar on Bitcoin, fully reserved, and redeemable back out to Ethereum once the exit is screened and signed ([details](../tokens/frUSD-overview)) |
| BTC | `frBTC` | Your BTC on Bitcoin, fully reserved, and redeemable back to native BTC after 3 to 7 block confirmations ([details](../tokens/frBTC-overview)) |
You do not have to hold either one first. Swapping from BTC wraps it for you on the way in, and bridging USDT or USDC from Ethereum mints frUSD on arrival.
### Make a trade
Trading the pool is an ordinary swap. On the [Swap](./swap) page, pick what you are paying with and what you want to receive, and the app routes it through this pool for you.
Two things worth reading before you confirm:
- **Read the amount you receive, at the size you actually intend to trade.** Once you enter an amount, the rate on the screen is derived from that quote, so it already carries your size and the fee.
- **Check the minimum received.** That is your floor if the price moves between signing and confirmation, and it is what your slippage tolerance sets.
### A different kind of pool
Most SUBFROST pools are standard constant-product AMMs. This one is a **CryptoSwap pool** (the Curve V2 design), which changes two things you can feel.
**Liquidity is concentrated rather than spread evenly.** A constant-product pool spreads its depth across every price from zero to infinity. A CryptoSwap pool holds most of its depth around a reference price it maintains internally, and moves that reference as the market moves.
**The fee moves instead of being fixed.** It is currently **0.2% to 0.8%**, set by how balanced the pool is left after your trade. A trade that leaves the two sides closer to even pays the low end; one that pushes them apart pays the high end. The trade that restores balance is the cheaper one, and the quote you are shown already has the pool's fee in it.
### Providing liquidity
You can supply both sides of the pool and earn a share of what it charges. The flow is the same as any other pool and lives on the [Liquidity](./pools-liquidity) tab of the swap page.
:::info This pool splits its fee differently
Half of what this pool charges accrues to the protocol, so liquidity providers keep roughly **0.1% to 0.4%** of the volume, unlike the 0.8% a standard SUBFROST pool pays. [Pools & Liquidity](./pools-liquidity) has the details and is the page to read before you deposit.
:::
Your position is represented by an **LP token**, which is the pool contract itself (`4:1778`). It carries 18 decimals.
### Getting money in and out from Ethereum
The pool is on Bitcoin, but you can fund it from an Ethereum wallet and take value back the same way.
- **In:** deposit USDC or USDT through the frUSD bridge and receive frUSD on Bitcoin. You can ask for part of the deposit to arrive as native BTC, which is especially helpful if your wallet is empty and wouldn't be able to pay network fees.
- **Out:** burn frUSD back to a stablecoin on Ethereum, optionally taking part of it as ETH so a fresh address has gas to move with. If the network cannot execute the ETH part, it pays the whole redemption as the stablecoin instead.
Both directions charge a flat fee plus a small percentage, and there is a minimum size, so one larger trip costs less than several small ones. [frUSD Overview](../tokens/frUSD-overview) has the current numbers.
:::note Asking for a lot of BTC for USDT or USDC? You may get frUSD instead
If the BTC you want to swap for exceeds 10% of the pool's frUSD, the whole deposit arrives as frUSD instead. Nothing is lost and the deposit still goes through. To get BTC, ask for a smaller BTC part, or bridge first and swap afterwards.
:::
### Pool details
| | |
| --- | --- |
| Pool | `4:1778`, which is also the LP token, 18 decimals |
| frUSD | `4:1776`, 8 decimals |
| frBTC | `32:0`, 8 decimals |
| Design | CryptoSwap (Curve V2) |
| Swap fee | Currently 0.2% to 0.8%, by resulting balance. Not an invariant: read it from the [pool API](../api-reference/json-rpc/btcusd) |
Reading the pool, quoting a trade at a given size, or preparing a bridge deposit from the command line is covered in [BTC/USD Commands](../api-reference/cli-sdk/btcusd).
### Next steps
- [Swap](./swap): the trading flow itself, and how routes are chosen.
- [Pools & Liquidity](./pools-liquidity): what providing liquidity to this pool pays.
- [frUSD Overview](../tokens/frUSD-overview): the dollar on the USD side of the pair.
- [frBTC Overview](../tokens/frBTC-overview): the Bitcoin on the BTC side.
- [BTC/USD Commands](../api-reference/cli-sdk/btcusd): the same pool from the command line.
- [BTC/USD Pool API](../api-reference/json-rpc/btcusd): live pool state over JSON-RPC.
---
## FIRE Vault
**FIRE** is the Alkanes governance token. It also rewards the people who provide liquidity to the [DIESEL / frBTC pool](./pools-liquidity), and it is earned two ways: by **staking** your liquidity, or by **bonding**.
There is no premine. Every FIRE that exists was emitted by the protocol, and the only way FIRE leaves circulation is redemption against the treasury.
### FIRE at a glance
| Metric | Value |
| --- | --- |
| Maximum supply | 2,100,000 FIRE |
| Decimals | 8 (same as Bitcoin) |
| Premine | 0% |
| Staking pool | 85% (1,785,000 FIRE) |
| Bonding pool | 15% (315,000 FIRE) |
| Halving interval | 105,000 blocks (about 2 years) |
| Activation block | 950,420 |
| First halving | Block 1,050,000 |
| Epoch 0 length | 99,580 blocks (activation to the first halving) |
FIRE emission halves every 105,000 blocks, which is half of Bitcoin's own halving interval. Every second FIRE halving therefore lands on a Bitcoin halving.
Epoch boundaries sit on a fixed grid anchored at block 0, so they do not move to accommodate the activation block. FIRE went live at 950,420, partway through the grid epoch that ends at 1,050,000, which makes epoch 0 shorter than a full interval. Every epoch after it runs the full 105,000 blocks.
### How the vault pays you
The vault accepts **DIESEL / frBTC LP tokens**. It pays out FIRE with a reward accumulator (the standard Synthetix staking model): every block, a fixed amount of FIRE is divided pro-rata across all weighted stake. Your share of a block's emission is your weighted stake divided by the total weighted stake in the vault, so rewards accrue continuously rather than in discrete payouts.
Per-block emission in epoch 0:
| Pool | FIRE per block | Per day | Per year |
| --- | --- | --- | --- |
| Staking (85%) | 8.5 | ~1,224 FIRE | ~446,250 FIRE |
| Bonding (15%) | 1.5 | ~216 FIRE | ~78,750 FIRE |
| **Total** | **10** | **~1,440 FIRE** | **~525,000 FIRE** |
Day and year figures assume Bitcoin's average of 144 blocks per day and 52,500 blocks per year.
### The emission schedule
Every 105,000 blocks the per-block rate halves. Epoch boundaries align to a global grid anchored at block 0, not to the activation block, which is why epoch 0 below is short, and every second FIRE halving coincides with a Bitcoin halving. The 85 / 15 split between the staking pool and the bonding pool holds in every epoch.
| Epoch | Length | Total per day | Cumulative supply at epoch end |
| --- | --- | --- | --- |
| 0 | 99,580 blocks | ~1,440 FIRE | 995,800 FIRE |
| 1 | 105,000 blocks | ~720 FIRE | 1,520,800 FIRE |
| 2 | 105,000 blocks | ~360 FIRE | 1,783,300 FIRE |
| 3 | 105,000 blocks | ~180 FIRE | 1,914,550 FIRE |
| ... | ... | ... | approaches 2,045,800 FIRE |
The cumulative column is a ceiling on what the schedule releases, not a forecast of circulating supply. Emission accrues to whoever is staked at the time, so any emission in blocks where nothing was staked is simply never awarded, and it is not paid out retroactively later.
#### Why the cap needs no premine
Each epoch emits exactly half of the one before it, so the total emission is an infinite geometric series bounded by the supply cap:
```
total = grid_epoch_emission x sum(1/2^i) for i = 0..infinity
= grid_epoch_emission x 2
= 1,050,000 x 2
= 2,100,000 FIRE
```
That is the bound the contract is written against, and it is the maximum supply listed at the top of this page. Because FIRE activated partway through epoch 0 rather than on a grid boundary, that epoch released 995,800 FIRE instead of a full 1,050,000, and the series converges to about 2,045,800 instead. The cap stays where it is; emission simply approaches a point below it.
The schedule itself is what enforces the cap. Nothing needs to be held back at genesis to make the numbers work, which is why the premine is 0%.
### Lock multipliers
You can begin earning FIRE by locking your LP for a flexible period, earning the base rate. Lock your stake for longer to earn more (up to 1 year for a 3x multiplier). A longer lock applies a higher reward multiplier to the same amount of LP. Locks cannot extend past the current epoch's expiry.
| Lock period | Blocks | Reward multiplier |
| --- | --- | --- |
| No lock | 0 | 1.0x |
| 1 week | 1,050 | 1.25x |
| 1 month | 4,375 | 1.5x |
| 3 months | 13,125 | 2.0x |
| 6 months | 26,250 | 2.5x |
| 1 year | 52,500 | 3.0x |
The longer you commit, the larger your share of the staking emissions.
### Staking your LP
1. **Get DIESEL / frBTC LP.** Provide liquidity to the [DIESEL / frBTC pool](./pools-liquidity) first. The vault accepts that LP token and nothing else.
2. **Choose a lock duration.** Anything from no lock to one year. A higher multiplier earns proportionally more FIRE for the same LP, but your position is locked until expiry.
3. **Stake and receive an NFT position.** The NFT carries two things at once: your **FIRE yield rights** (the emissions you can claim) and your **LP claim** (the liquidity you get back at expiry).
4. **Earn every block.** Rewards accrue per block. Claim or compound whenever you want.
```
Stake(LP, lock, amount) -> NFT position (FIRE yield + LP claim)
```
### Getting your LP back
There are three exit paths, depending on how much flexibility you want.
#### Unstake
Wait for the lock to expire, then unstake the NFT. You get your LP back plus all accrued FIRE.
```
Unstake(NFT) -> LP + FIRE
```
Once the lock has expired, claiming your rewards does this for you: on a position you have not split, a claim after expiry returns the FIRE **and** the LP and closes the position out. You do not need to unstake separately.
#### Split into FIRE-PT
Split the NFT position into its two halves. The **NFT** keeps the FIRE yield rights. A fungible **FIRE-PT** token (**FIRE-PT-0** in the current epoch) carries the LP claim, and you can transfer or sell it on its own. Merging puts the position back together.
```
Split(NFT) -> NFT (FIRE yield only) + FIRE-PT (fungible LP claim)
Merge(NFT + FIRE-PT) -> NFT (full position restored)
```
This is how you move the LP liquidity out without giving up the FIRE yield stream.
FIRE-PT tokens are fungible regardless of the lock type. This also implies that any FIRE-PT can be recombined with any FIRE NFT that has the corresponding amount. As a result, a bare FIRE NFT represents not only the right to earn FIRE yield, but also the right to unlock the corresponding amount of FIRE-PT before the end of the epoch.
#### Redeem expired FIRE-PT
Once the epoch has expired, anyone holding FIRE-PT can redeem it straight for LP. No NFT required.
```
RedeemExpired(FIRE-PT) -> LP
```
### Bonding
Bonding is for people who want FIRE **now** instead of earning it over time. You hand the protocol LP tokens permanently, and you get FIRE at a **10% discount from market price**, vested over about **14 days** (2,100 blocks). The discount is a setting the protocol can adjust, so treat 10% as the current rate rather than a fixed rule.
Bond LP goes directly to the treasury and stays there. It is not returned, and it permanently backs the redemption floor.
In plain language, a bond is this trade: you give the protocol LP forever, and in exchange you get FIRE at a 10% discount from market price, vested over 14 days.
#### The floor guard
A bond is never priced below the redemption floor. That guard exists to stop a free round-trip.
Once the treasury holds LP and FIRE has been minted, the floor price is real money. Without the guard, you could bond below the floor and immediately redeem for more LP than you put in. Because the bond price can never fall under the floor, bonding can never be cheaper than redemption:
```
Bond at floor_price -> mint X FIRE (LP goes into treasury)
Redeem X FIRE at floor -> burn for LP (LP comes out of treasury)
= same LP back (zero net change)
```
A bonder who redeems immediately nets zero. The 14-day vest means a bonder is betting on price appreciation, not extracting value from the protocol.
#### Bonding capacity builds up over time
Bonding is metered rather than unlimited. Capacity accrues block by block from the moment the contract goes live, so shortly after launch there is little of it and a large bond may not fit. If a bond is rejected for capacity, it is not broken: wait and try again, or bond a smaller amount.
### Redemption and the price floor
Any FIRE holder can burn FIRE for a proportional share of the treasury's LP backing:
```
floor_price = total_treasury_LP / total_FIRE_supply
```
**Redeeming charges a 1% fee**, which is not paid to anyone: it stays in the treasury. So you receive about 99% of your proportional share, and the LP you leave behind raises the floor slightly for everyone still holding FIRE.
This is a hard floor, and it scales with the treasury. Every bond deposits LP that never leaves, so the floor only moves in one direction over time as bonding activity grows.
### The two reference prices
Alongside what FIRE trades at, two other numbers answer different questions.
| Price | Where it comes from | What it means |
| --- | --- | --- |
| **Oracle** | An on-chain price feed | The reference the protocol reads when it prices a bond |
| **Floor** | `treasury_LP / total_supply` | What a redeemer gets for burning FIRE |
The bond discount is measured against the **market price** of FIRE, not against the floor.
:::info[The oracle no longer depends on how much LP is staked]
It used to: the reference was derived from locked LP over annual emission, so a larger stake moved the number. That link is gone. The oracle is now a posted feed.
:::
### The flywheel
Staking, bonding, and redemption are not three separate features. They feed each other in a closed loop.
| Who | Does | Which causes |
| --- | --- | --- |
| LP providers | Stake DIESEL / frBTC LP, locking longer for more FIRE | A longer lock takes a larger share of the 85% |
| Bonders | Pay LP for FIRE at a discount from market price | More bonding fills the treasury faster |
| Treasury | Keeps every bonded LP permanently | A bigger treasury raises the floor price |
| Redeemers | Burn FIRE for a proportional slice of treasury LP | The floor guard kicks in, so bonding cheaply gets harder |
Read it as a loop: locked LP earns the 85% share of each block's emission, and the remaining 15% is what bonds are paid out of; bonders hand over LP for FIRE at a discount to the market price; that LP never leaves the treasury, so the floor rises; and a higher floor both protects FIRE holders and makes committing liquidity safer, which brings the loop back to the top. The emission itself is fixed by the schedule. What the loop moves is the **floor**, in one direction only.
### cFIRE vault: Staking FIRE itself
Everything above is about earning FIRE by staking **LP**. The **cFIRE vault** is a different vault, and it takes **FIRE** itself.
Where does the yield come from? It comes from positions that exit the vault before you. Unlike anything else on this page, cFIRE does not have an intrinsic yield: the yield comes from the activity.
#### How the pot works
Deposit FIRE and you receive a **cFIRE-POS** NFT position holding your claim. Exit and you leave an **8% tithe** in a pot. That pot does not go to the protocol: it drips back into existing holders of cFIRE-POS who have not exited yet, once every 144 blocks (a Bitcoin day), over roughly a year. This is where the yield comes from.
So the rate only moves in one direction, and it moves because somebody left. Staying longer than the people around you is the whole of the strategy.
#### What you get, and what each piece does
Voting power is read **by address**, and FIRE sitting in a vault is not at your address. That is what the position is for: it holds the deposit, and a separate token carries the vote.
One FIRE deposit into the cFIRE vault produces two things:
```
Deposit(FIRE) -> cFIRE-POS-{n} the position: holds the vault shares
-> cFIRE-DELEGATE-{n}-{m} the vote: transferable, carries the weight
```
The **position** holds the shares. It earns whatever the vault earns, and on its own it votes nothing. The **vote token** carries the position's weight and is an ordinary transferable token.
A single contract does both in one call, reading the position out of the mint, so the vote cannot be left behind.
#### Your weight
Your weight is what your position is worth **in FIRE**, so a position and a plain FIRE balance of the same value weigh the same.
It is measured **before** the exit fee: a position that would pay out 90 FIRE weighs 100. And it rises on its own as the pot drips back into the rate, without you touching anything.
#### Delegating is a transfer, made easy
There is no registry and no signature scheme behind delegation. The vote token is a **bearer** asset, and whoever holds it votes with it. So delegating is an ordinary send, but we have created a UI for it.
| Action | What happens on chain |
| --- | --- |
| **Keep it** | You hold the token and you vote |
| **Send it** | Whoever receives it votes with your weight |
| **Delegate to yourself** | The position mints a **new** vote token to you and cancels the old one in the same transaction |
Minting a replacement cancels the previous token **wherever it is**. Nothing is taken from the person holding it: their token stays in their wallet and reports zero from that moment.
:::warning[Sending cFIRE-POS is sending your FIRE away]
Whoever holds the position (**cFIRE-POS**) can exit it, so sending the position sends the deposit. Sending the vote token (**cFIRE-DELEGATE**) sends only the vote. **NEVER SEND THE cFIRE-POS NFT UNLESS YOU MEAN TO SEND YOUR FIRE AWAY.** The only NFT meant to travel is **cFIRE-DELEGATE**.
:::
#### Leaving the cFIRE vault
Exiting the cFIRE vault burns your **cFIRE-POS** NFT in return for your allocated FIRE (deposit + drip), minus **10%**:
| Where it goes | Share |
| --- | --- |
| Stays in the pot, for everyone still in | 8% |
| FIRE is burned | 1% |
| To the protocol treasury | 1% |
| **Total** | **10%** |
The position is destroyed, not kept as a receipt, and any token voting for it stops counting.
#### The contracts
| Piece | Alkane |
| --- | --- |
| Vault | `4:877` |
| Position factory | `4:876` |
| Deposit-and-mint contract | `4:878` |
| Treasury | `4:868` |
:::info[Governance does not read these tokens yet]
Minting a vote token now sets it up. It starts counting when the tally is changed to read them. Everything else on this page, including the yield and the exit, works today.
:::
### Risks
| Risk | What it means for you |
| --- | --- |
| **A trusted oracle, for now** | The bond reference is posted on chain by a 2/3 signer set. Federating that across more signers is the intent, but it has not happened yet |
| **Lock illiquidity** | Locked LP cannot be withdrawn before expiry. Split into FIRE-PT if you need partial flexibility |
| **Block time variance** | Every duration here assumes Bitcoin's roughly 10-minute average block. Real elapsed time drifts |
| **The cFIRE vault pays nothing on its own** | It holds a plain FIRE token and farms nothing. The only thing that raises the rate is somebody else leaving and paying the tithe |
| **The vote token is a bearer asset** | Send it to the wrong address and the vote is gone. Minting a replacement is the only recovery, and it costs a transaction |
### Tips
- **Match your lock to your conviction.** Longer locks earn proportionally more, but you cannot unwind early.
- **Use Split if you need the liquidity back.** Keep the FIRE yield on the NFT and trade the FIRE-PT.
- **Bonding is not staking.** Bonding gives you FIRE upfront and your LP is gone for good. Staking returns your LP plus rewards.
- **Watch the halving.** Emission halves every 105,000 blocks, so early epochs pay the most.
- **Treasury growth is floor growth.** The more LP that arrives through bonding, the stronger the redemption floor.
- **Do not confuse the two vaults.** The FIRE vault above takes DIESEL / frBTC LP. The cFIRE vault takes FIRE itself, and its yield is entirely other people's exit tithes.
- **Send the vote token (cFIRE-DELEGATE), never the position (cFIRE-POS).** The position is the deposit.
### Next steps
- [Pools & Liquidity](./pools-liquidity): get the DIESEL / frBTC liquidity you stake here.
- [Tokens & Economics](../start-here/key-concepts): where FIRE fits with frBTC and DIESEL.
---
## Futures
:::info[Coming soon]
Futures on SUBFROST are on the way. A full guide will land here when they launch officially.
:::
---
## Lending
Lending lets you **lend out your tokens to earn interest** or **borrow tokens against collateral**, directly on Bitcoin L1. It is peer-to-peer: you set your own terms and match with another user. There are no pools, no middlemen, and your funds stay in your wallet until a match happens.
### Lending at a glance
| Property | Value |
| --- | --- |
| Model | Peer to peer, fixed rate, fixed term |
| Settlement | A single Bitcoin transaction |
| Posting an offer | Instant, a small network fee, not settled on chain |
| Who pays to settle | The taker (the person who accepts an offer) |
| Protocol fee | 3,000 sats per loan, paid at settlement |
| Interest | Fixed at creation, computed from APR and duration |
| Term | Measured in blocks |
| Liquidation | None. No margin calls on an active loan |
| Custody | None. There is no custodian and no admin key |
### How it works
Lending has two sides, and you can be either one:
- **Lenders** post a **loan offer**: "I will lend X token at this rate, if you put up Y collateral."
- **Borrowers** post a **loan request**: "I want to borrow X token and I will put up Y collateral."
Whoever publishes the offer is the **maker**. Whoever accepts it is the **taker**. When someone takes the other side of your offer, the loan is created on chain in one transaction and becomes active immediately.
#### Posting an offer is instant
When you post an offer, your wallet **signs it, but the loan itself is not settled on the Bitcoin network**. So:
- You pay a **small network fee** to post, and nothing more.
- **Posting does not tie up the rest of your balance.** The small transaction you broadcast at post time sets aside exactly the loan or collateral amount and hands the remainder back to you as normal change. Even if all your tokens sat on a single UTXO, the leftover is immediately spendable.
- **No waiting for a block** before your offer is usable. It shows up in the order book instantly.
- **Your funds stay in your wallet.** A posted offer is a pre-signed instruction that, by design, **cannot be broadcast on its own**. It only becomes a real transaction once a taker accepts and adds their side.
This means **only the taker pays to settle**. As a maker, what you pay is the small network fee on the offer itself.
#### Matching settles the loan on chain
When a taker accepts your offer, a single Bitcoin transaction is broadcast that:
1. Hands the borrowed token to the borrower,
2. Locks the borrower's collateral, and
3. Creates the loan.
Once that transaction confirms, the loan is **active** and the clock starts on its term. If any part of it fails, both sides get their tokens back in the same transaction. You cannot end up half-lent.
For the mechanics of how a pre-signed offer stays inert until a taker funds it, see [Lending Protocol](../protocol/lending).
### What a loan costs
| Cost | Who pays | When |
| --- | --- | --- |
| Protocol fee, 3,000 sats | The taker | At settlement, built into the transaction |
| Bitcoin network fee | Taker (settlement), maker (offer post) | At settlement, varies with the fee rate the taker chooses |
### The interest you will pay
Interest is **fixed at creation**, not accrued over time. You know the exact repayment before you agree to anything:
```
interest = loan amount × APR × (duration in blocks / 52,560)
repayment = loan amount + interest
```
The `52,560` is the number of Bitcoin blocks in a year (144 per day × 365).
**Example.** Borrow 1 DIESEL at 12.5% APR for 1,008 blocks (about a week):
```
interest = 1 × 0.125 × (1,008 / 52,560) = 0.00239726 DIESEL
repayment = 1.00239726 DIESEL
```
Because the number comes straight from the terms, both sides can compute it up front, and it never changes.
### Collateral limits
Every loan is over-collateralized: the collateral must be worth more than what is owed. The app enforces a **maximum loan-to-value (LTV)** per collateral token, measured in USD:
| Collateral | Maximum LTV | Minimum collateral, relative to what is owed |
| --- | --- | --- |
| frBTC | 80% | about 125% |
| DIESEL | 50% | about 200% |
| Any other token | 30% | about 333% |
Two things about how this is applied:
- **It is measured against the repayment, not the amount borrowed.** Principal plus interest is what has to be covered. Raising the APR or the duration raises the repayment, which raises the collateral required.
- **More collateral than the minimum is always fine. Less is rejected.** The app will not let you post an offer below the minimum, and the check runs again when the offer is submitted.
#### Your open offer can be removed if prices move
An offer is created at or under its collateral's maximum LTV, but prices keep moving after you post. If the collateral on an open offer falls far enough to be worth **less than what is owed on it**, the offer is **removed from the order book**.
This applies only to **open offers that nobody has taken yet**. An **active loan is never touched**: once a loan exists, its terms are fixed and enforced exactly as agreed. See below.
Prices are not the only reason an open offer can disappear. Posting sets aside a specific piece of your funds for the offer, and nothing stops you from spending it in another transaction afterwards. If you do, the offer can never settle, so it is removed from the order book rather than left for a taker to trip on.
:::info[When prices are unavailable, the limits stand down]
The LTV check needs a USD price for both tokens. If either one has no price the app can resolve, the check cannot be applied, and the app will let you set any values while warning you that you are on your own. Take that warning seriously: it means nothing is checking your collateral for you.
:::
### Choosing your collateral
Collateral terms are **fixed when the loan is created** and **enforced exactly as agreed**. Beyond the collateral minimum above, there are **no margin calls and no automatic liquidations**. That has an important consequence:
> **If you are lending, you must be comfortable receiving *exactly* the agreed collateral, even if its price moves so much that it is worth less than the loan you made.** If a borrower defaults and the collateral has dropped in value, claiming it is your only recourse. The protocol will not top you up.
The LTV ceiling limits your downside, it does not remove it. It is checked when the offer is created, against prices at that moment, and once the loan is active nothing re-checks it. So ask for the level where you would be **genuinely fine** ending up with that collateral instead of being repaid:
- **Higher collateral** protects you better if prices move against you, but it makes your offer **less attractive**, and a borrower may never take it.
- **Lower collateral** is easier to fill and friendlier to borrowers, but leaves you **more exposed** if the collateral's value falls.
Borrowers face the mirror image: the more collateral you post, the more you have at risk if you cannot repay, but offers asking for less collateral may be scarce.
### How to lend
1. Open the **Lend / Borrow** page and pick the token you want to **lend** and the token you will accept as **collateral**.
2. Switch the form to **Lend**.
3. Enter the amount to lend, the collateral you require, the **APR**, and the **duration** in blocks.
4. Click **Lend** and approve the signature in your wallet.
Your offer is now live in the order book. Nothing settles on chain until a taker accepts it.
### How to borrow
1. Pick the token you want to **borrow** and the token you will post as **collateral**.
2. Browse the order book for an offer you like (sorted by APY), **or** switch the form to **Borrow** and post your own loan request.
3. To take an existing offer, click **Borrow**. You will see a summary: what you provide, what you receive, the APY, the total repayment, and the deadline.
4. Confirm and pay the network fee to settle.
### The life of a loan
Once a loan is active, here is what can happen.
#### It gets repaid, the normal path
- The borrower repays the **principal plus interest** before the deadline. One click (**Repay & reclaim**) repays the loan and returns their collateral in the same transaction.
- The lender then **claims the repayment**.
- The loan is fully closed. Both sides got what they expected.
#### It defaults, the borrower did not repay in time
- If the deadline passes without repayment, the loan is **defaulted**.
- The lender can **claim the collateral**. One click does the default and the claim together.
- The borrower keeps the tokens they borrowed but **forfeits the collateral**.
A loan cannot be repaid after it defaults. Once the deadline passes, the lender's path is to claim the collateral.
:::info[The app shows a loan as defaulted the moment its deadline passes]
On chain, a loan only becomes `DEFAULTED` when someone actually triggers it. The app is stricter: it shows an overdue loan as **Defaulted** as soon as the deadline is behind the current block, and withdraws the borrower's repay button.
:::
You can always see your loans and their status (**Active Loans**, **Closed Loans**, and your open **My Offers**) at the bottom of the Lending page.
### Managing your offers
Your open offers appear under **My Offers**. From there you can:
- **Edit** an offer. This re-signs a fresh offer, sets the funds aside again in a new small transaction (the same small network fee as posting), and cancels the old one.
- **Delete** an offer, removing it from the order book. Deleting is off-chain and free.
Neither one settles the loan on chain, just like posting.
### Tips
- **Start small** to get a feel for the flow before posting larger offers.
- **Think about default before you lend.** Price the collateral and the rate for the scenario where you are repaid in collateral, not in the token you lent.
- **Right-size your collateral ask.** Too high and nobody takes it, too low and you carry more risk. The collateral minimum is a floor, not a recommendation.
- **Watch the deadline** if you are borrowing. There is no grace period, and after it passes you cannot repay.
### Next steps
- [Lending Protocol](../protocol/lending): the contract, the escrow, and the settlement mechanics.
- [Swap](./swap): trade between Bitcoin assets against a shared liquidity pool.
- [Wallets](./wallets): set up the wallet you will sign with.
---
## Pay Fees in DIESEL
Every Bitcoin transaction pays a network fee to the miner, and that fee is always paid in BTC. If you hold DIESEL but little or no BTC, that can leave you stuck.
The SUBFROST Chrome extension and Android app let you pay that fee **in DIESEL** instead. A relayer adds its own BTC to your transaction to pay the miner, and you pay for it in DIESEL inside **the same transaction**. You do not need to hold BTC for the fee.
Want to know how it works under the hood? See [How DIESEL Fees Work](../protocol/diesel-fees).
### Where to find it
When an action supports it, you will see a **Network fee** row with two options: **BTC** and **DIESEL**.
On both the Chrome extension and Android app, the row is on the **Send** screen and **Swap** screen.
BTC is the default. If you do not have enough BTC to pay the fee, the wallet switches to DIESEL for you and says so: *"Not enough BTC for the fee. DIESEL is selected."*
### Reviewing the fee
When you pick DIESEL and continue, the review shows the fee as **up to X DIESEL**. In the extension it reads *"Paid in DIESEL, up to X DIESEL"*.
That number is a **cap**, not a fixed price:
- The wallet estimates the fee from the current DIESEL price, then adds a small margin on top. That is the "up to" figure you see on the review.
- The actual charge is set by the fee quote, and it can be lower than the cap.
- If the quote comes back **higher** than the cap you approved, the wallet stops and nothing is sent. You will see *"The DIESEL price moved above what you approved. Nothing was sent."*
:::note[Keep your DIESEL in your taproot address]
The fee is paid from the DIESEL in your taproot address. If there is not enough there, you will see *"Not enough DIESEL in your taproot address for this fee."*
:::
While the wallet works, it shows its progress with messages such as *Reading your coins…*, *Connecting to the fee network…*, *Reading the DIESEL price…*, *Getting a fee quote…*, *Signing…* and *Adding the fee…* (the Android app shows fewer of these).
### What you can pay for in DIESEL
In both the Chrome extension and the Android app:
- **Send BTC** to one recipient.
- **Send a token** to one recipient.
- **Swap** one token for another.
- **Wrap** BTC to frBTC.
- **Unwrap** frBTC to BTC.
The Android app also supports it when you trade frBTC and frUSD in the [BTCUSD Pool](./btcusd-pool).
When you unwrap with a DIESEL fee, the review adds a line: *"You receive X BTC, minus the payout transaction's fee."* The payout of your BTC is a separate transaction, and its own fee comes out of the amount you unwrap.
### What is not supported
The DIESEL option is greyed out, with a short reason under it, in these cases:
- **Sending to more than one recipient.** *"DIESEL fee works with one recipient."*
- **Swaps with BTC on one side.** Swap from frBTC instead, or pay this one in BTC. *"DIESEL fee is not available for swaps with BTC."*
- **Hardware wallets** in the Android app. *"DIESEL fee is not available for hardware wallets."*
- **Networks other than mainnet.** *"DIESEL fee works on mainnet only."*
**Transactions a dApp asks you to sign** always pay their fee the normal way, in BTC. The DIESEL fee is only for actions you start inside the wallet.
### When DIESEL is unavailable
The fee is paid with the help of a network of relayers. When the wallet opens a Send or Swap screen, it quietly checks whether that network can take a DIESEL payment right now.
If it cannot, the wallet tells you and leaves BTC selected:
- *"DIESEL fee unavailable right now. BTC only for now."*
- *"The fee network is busy right now. Pay the fee in BTC or try again in a minute."*
- If you are also short of BTC: *"Not enough BTC for the fee, and paying in DIESEL is unavailable right now."* In that case add a little BTC, or try again later.
### If something goes wrong midway
Most problems happen before anything is sent, and the wallet says so plainly. Messages that end in **"Nothing was sent."** mean your coins did not move. You can try again, or pay in BTC.
For example, a fee quote is only valid for a short time. If it runs out first, you will see *"The fee quote expired before it could be used. Nothing was sent. Try again."*
There is one case to handle with care. If you already signed and the fee network then did not answer, you will see:
> *"The fee network did not answer after you signed. Check transaction … in the explorer before sending again."*
Your transaction may still go through. **Look up that transaction in the explorer before you try again.** If it confirmed, you are done. In this case the wallet does not switch you to a BTC fee on its own. Do not resend until you have checked, or you may pay twice.
### FAQ
**Do I need any BTC at all?**
Not for the fee. For **Send BTC** and **Wrap**, you still need the BTC you are sending or wrapping.
**Is the price fixed when I approve?**
The cap is. You approve "up to X DIESEL", and the wallet will not go above it. The final charge can be lower.
**Why does it say "up to" instead of an exact number?**
The exact amount comes from a fresh quote a moment after you approve. The cap protects you if the price moves in between.
**Is this a separate transaction or a loan?**
Neither. The relayer's BTC and your DIESEL payment are in the same Bitcoin transaction. See [How DIESEL Fees Work](../protocol/diesel-fees).
**Can I use it with a dApp?**
No. Requests from dApps pay their fee in BTC.
**Where does my DIESEL go?**
It is paid into a protocol treasury on-chain. [How DIESEL Fees Work](../protocol/diesel-fees) explains the details.
### Next steps
- [How DIESEL Fees Work](../protocol/diesel-fees): the design behind this feature.
- [Wallets](./wallets): sending, receiving and managing your balances.
---
## Pools & Liquidity
Every swap on SUBFROST trades against a **liquidity pool**: a shared reserve of two tokens that anyone can contribute to. When you provide liquidity to a pool, you deposit a pair of tokens and earn a share of the fees that the pool generates (from people swapping through it).
### How it works
- A pool holds two assets (for example DIESEL and frBTC). The price adjusts automatically as people trade, so you always have a counterparty.
- When you add liquidity, you receive an **LP position** representing your share of the pool.
- As swaps happen, they pay a fee that accrues to the pool, so your position grows over time.
- You can withdraw your liquidity (plus accrued fees) at any time by redeeming your LP position.
### The swap fee, and what you earn
On a standard AMM pool a swap pays **1% by default**, split two ways:
| Share | Rate | Goes to |
| --- | --- | --- |
| Liquidity providers | 0.8% | the pool, so it accrues to your position |
| Protocol | 0.2% | permanently to the AMM protocol |
So on those pools, **as a liquidity provider you earn 0.8% of the volume that trades through your pool**, in proportion to your share of it.
:::warning The BTCUSD pool charges differently
The frUSD/frBTC pool is a CryptoSwap pool, not a standard AMM pool, and the numbers above do not describe it.
Its fee is **dynamic between 0.2% and 0.8%**, depending on how unbalanced a trade leaves the pool: balanced trades pay the low end, unbalancing trades pay the high end.
:::
### Providing liquidity
1. Click **"Liquidity"** on the swap page to add positions to a pool.
2. On the **Add** tab, choose the pair you want to provide.
3. Deposit the two tokens in the pool's ratio. The app fills in the paired amount for you and shows the **Minimum Deposit**.
4. Confirm and sign. You now hold an LP position and start earning a share of that pool's swap fees.
### Removing liquidity
1. Click **"Liquidity"** on the swap page and switch to the **Remove** tab.
2. Choose the **LP position** you want to remove.
3. Set the **Amount to Remove**. The app shows the **Minimum Received** for each of the two tokens, quoted against current reserves.
4. Confirm and sign. Your share of the pool, plus the fees it accrued, returns to your wallet.
### Next steps
- [BTCUSD Pool](./btcusd-pool): the frUSD/frBTC pool the warning above is about.
- [FIRE Vault](./fire-vault): stake your LP and earn FIRE, or buy FIRE through bonds.
- [Swap](./swap): the other side of the pool.
---
## Safety
SUBFROST works with Alkanes tokens on Bitcoin. If you also hold **Ordinals, Runes or BRC-20 assets**, there is one precaution you have to take, and it is not optional.
### Move your non-Alkanes assets to a different wallet
> **Burn risk.** SUBFROST does not index `ord` at this time. You must transfer Ordinals, Runes and BRC-20 assets to a different wallet before interacting with our apps.
Here is why. On Bitcoin, these assets live on a specific coin (a UTXO). SUBFROST cannot see them, so it cannot avoid them: if one of those coins gets spent in the course of ordinary activity, **the asset on it is gone**. There is no recovery and no undo.
So keep two wallets:
- A **dedicated wallet** for SUBFROST: wrapping, swapping, staking.
- A **separate wallet** for your Ordinals, Runes and BRC-20 assets.
With them apart, nothing you do on SUBFROST can reach a coin that carries one.
### Protect your recovery phrase
Your 12-word recovery phrase is the only way to restore a self-custodial wallet. Write it down and store it offline, somewhere safe. Never share it, and never type it into any site other than the wallet's own restore screen.
### Start small
When you are new to the platform, do a small transaction first to get comfortable with the flow before moving larger amounts.
### Next steps
- [Wallets](./wallets): set up your SUBFROST wallet.
---
## Swap
Swapping lets you trade between Bitcoin assets (BTC, frBTC, and other Alkanes tokens) against a shared liquidity pool, all settled on Bitcoin L1.
### Make a swap
1. **Select your tokens.** Choose what you are swapping **from** (You Send) and what you want to receive (You Receive).
2. **Enter an amount.** Type it in, or use the quick buttons: 25%, 50%, 75%, or Max of your balance.
3. **Review the details.** Before confirming you will see the exchange rate, the swap route, the minimum you will receive, the deadline (in blocks), your slippage tolerance, and the miner fee rate.
4. **Confirm and sign.** Tap **Swap**, then approve the transaction with your connected wallet.
### Tips
- **Start small.** If you are new to the platform, do a small swap first to get comfortable with the flow before moving larger amounts.
- **Watch price impact on larger trades.** Splitting a large swap into several smaller ones can sometimes get a better rate than one big swap all at once.
### Swap routes
- **Direct.** Both tokens share a pool, so the swap is a single hop.
- **Multi-hop.** No direct pool exists, so the swap routes through an intermediate token (for example through frBTC or DIESEL) for the best price.
### What you can swap
Today you can swap **between BTC and Bitcoin-native Alkanes assets**.
Bitcoin-native swaps are live, and so is the Bitcoin-native stablecoin (frUSD). BTC against USD trades against the frUSD/frBTC pool, which is live on mainnet. Cross-chain, through your Ethereum wallet, you can bring **USDT or USDC in**, and take the value back **out as USDC or ETH**. From there, SUBFROST will consider adding support to other high-volume cross-chain assets such as SOL and ADA.
The frUSD/frBTC pool is young and therefore shallow, so **a trade size that would be unremarkable on another pair can move the price noticeably on this one**. Read the quote you are actually given rather than the displayed exchange rate, and see [BTC/USD Commands](../api-reference/cli-sdk/btcusd) if you want to measure the pool yourself before trading.
### Next steps
- [BTCUSD Pool](./btcusd-pool): the frUSD/frBTC market this page trades BTC against USD on.
- [Wrap & Unwrap](./wrap-unwrap-frbtc): how wrapping and unwrapping BTC works under the hood.
- [Pools & Liquidity](./pools-liquidity): earn fees by providing liquidity.
- [FIRE Vault](./fire-vault): stake and earn.
---
## Wallets
To use SUBFROST you need a Bitcoin wallet. The **web app** works in any browser, and there is an Android app and a Chrome extension out already.
Your wallet is where you view your assets, interact with smart contracts, manage UTXOs, track transactions, and send and receive BTC.
### Create a new wallet
To create a new wallet, view Step 2 in **[Get Started in 5 Minutes](../start-here/get-started)**.
### Restore an existing wallet
Choose **Restore Wallet** from the **Connect Wallet** menu. You can restore three ways:
- Your **12-word seed phrase**.
- A **keystore file** you exported earlier.
- A **Google Drive backup**, if you made one.
You then set a new password, and your wallet comes back with all of its addresses.
### Keeping your wallet secure
The browser wallet is encrypted with your password, and that password is required to unlock it each session. For backups you have three options:
- Export an **encrypted backup file**.
- **Reveal your seed phrase** (your password is required to do this).
- Back up to **Google Drive**.
:::warning[Keep your seed phrase safe]
Your 12-word seed phrase is the master key to your wallet. Anyone who gets it can take your funds. Never share it, never store it digitally, and never enter it on any website other than SUBFROST.
:::
### Your two addresses
SUBFROST gives you two address types, because different assets and different apps expect different formats.
| Address type | Format | What it is for |
|---|---|---|
| **Taproot** | `bc1p...` | Alkanes, Runes, BRC20s, Ordinals |
| **Native SegWit** | `bc1q...` | Sending and receiving BTC |
Both appear in the wallet header with copy buttons. Some exchanges and apps accept only one of the two formats, so having both means you can always receive funds.
### Your Portfolio
The wallet page is titled **Your Portfolio**. A summary card at the top shows your **Est. Total Value** in BTC with the approximate US-dollar equivalent underneath, and the **Send** and **Receive** buttons.
:::note[What the total does and does not include]
Est. Total Value is the cumulative estimated value of all your assets **excluding orbitals**. If you hold orbitals, the figure on this card is lower than everything you actually own.
:::
Everything you hold is then split across tabs:
| Tab | What it shows |
|---|---|
| **Tokens** | Your fungible balances: BTC and Alkanes tokens such as frBTC and DIESEL |
| **Positions** | LP tokens and staked positions |
| **NFTs** | Your Alkanes NFTs |
| **Activity** | Your transaction history |
| **UTXOs** | The individual coins behind your balance, and the tools to manage them |
An empty tab says so rather than showing nothing, so "No positions found" means you hold none, not that something failed to load.
A sixth tab, **FUEL**, appears only when your address is eligible for a FUEL allocation. If you do not see it, you are not eligible.
#### Tokens
Each row is one asset, with four columns:
- **Token.** The name, and for Alkanes tokens the id underneath (for example `frBTC · 32:0` or `DIESEL · 2:0`).
- **Balance.** What you hold, with the US-dollar value underneath.
- **Available.** The part you can spend right now. Anything tied up in an unconfirmed transaction is shown below it as **Mempool**.
- **Unit price.** The current price of one unit.
Token and Balance are sortable.
### UTXO management
Bitcoin does not work like a bank balance. Your funds are a set of individual coins called UTXOs (Unspent Transaction Outputs), and this tab lets you control exactly which ones you spend.
You can filter your UTXOs by address type (Native SegWit or Taproot) or by asset type (Runes, tokens, or inscriptions), and act on them:
| Action | What it does |
|---|---|
| **Freeze** | Marks a UTXO as off limits so it is never spent by accident |
| **Unfreeze** | Makes a frozen UTXO spendable again |
| **Split** | Separates Alkanes assets from the BTC attached to them, so you can spend the BTC and keep the Alkanes |
:::tip[Protect your valuables]
If you are using your Taproot address for collectibles, freeze the UTXOs holding rare inscriptions so you never spend them as transaction fees. See [Safety](./safety) for the fuller picture.
:::
### Activity
Every transaction from your wallet, with:
- **Status.** Confirmed or pending.
- **Transaction ID.** Links out to a block explorer for the full record.
- **Date and block confirmation.**
- **Fee paid.**
- **Expandable details.** All inputs and outputs of the transaction.
### Settings
- **Network.** Your wallet points at Bitcoin Mainnet.
- **HD wallet derivation.** View your active addresses and configure how they are derived.
- **Security and backup.** Export backups and manage your seed phrase.
### Sending Bitcoin
To send BTC you provide:
1. **The recipient address.** All Bitcoin address formats are supported.
2. **The amount**, in BTC.
3. **The fee rate**: Slow, Medium, Fast, or Custom.
Review the details, then confirm. Your wallet signs the transaction and broadcasts it.
Short on BTC for the fee? In the SUBFROST Chrome extension and Android app you can pay it in DIESEL instead. See [Pay Fees in DIESEL](./pay-fees-in-diesel).
:::warning[Check unusually high fees]
The app warns you when your fee looks unusually high. Always double-check before confirming a large transaction.
:::
### Receiving Bitcoin or Alkanes
The receive screen gives you a **QR code** to scan with any mobile wallet and your **full address** with a copy button.
:::warning[Before you share your address]
Send only Bitcoin or Alkanes tokens to this address. Verify the address is correct before sending anything to it.
:::
### Mobile and browser extension
SUBFROST is built mobile-first.
- **Android.** A direct install file. No app store account needed. Google Play listing coming soon.
- **Chrome browser extension.** On the Chrome Web Store.
- **iOS.** In review for the App Store.
### Next steps
- [Wrap and Unwrap BTC](./wrap-unwrap-frbtc): turn your Bitcoin into frBTC.
- [Swap](./swap): trade Bitcoin assets.
- [Safety](./safety): keep your Ordinals safe.
---
## Wrap & Unwrap BTC
**frBTC** is your Bitcoin made programmable: a token backed 1:1 by real BTC that works with on-chain apps and settles natively on Bitcoin. Wrapping is how you get frBTC; unwrapping turns it back into native BTC.
### Minting frBTC (Wrapping BTC to frBTC)
Minting frBTC locks your BTC and mints an equal amount of frBTC in the same step. It is a **1:1 conversion**: 1 BTC mints you 1 frBTC, instantly.
In practice you rarely wrap by hand. When you [swap](./swap) starting from BTC, the app wraps to frBTC and performs the swap in a single, atomic transaction. The wrapping happens behind the scenes.
### Redeeming BTC (Unwrapping frBTC to BTC)
Unwrapping burns your frBTC and releases the underlying native BTC back to you. Unlike wrapping, unwrapping is authorized by the distributed group of signers that custodies the Bitcoin (no single party controls it). See [What is SUBFROST](../start-here/what-is-subfrost) for how that custody works.
#### Why there is a wait
Your BTC arrives after **3 to 7 block confirmations**. The exact number moves with fee-rate volatility, so treat it as a range rather than a fixed number.
The wait exists to protect the reserve from a **Bitcoin reorg**, and the reason is worth understanding, because it is the kind of thing that sounds like caution and is actually arithmetic:
Suppose there were no wait, and your frBTC became BTC the instant you unwrapped. Now Bitcoin reorgs away the last block. The signers already agreed to the unwrap and paid out, but the burn of your frBTC was undone by the reorg. You would end up holding **both** the frBTC and the BTC, and the reserve would be short.
Waiting a few blocks closes that window. A reorg deep enough to reach past 3 to 7 blocks is prohibitively expensive to pull off, so that depth is enough.
### Fees
Minting and redeeming each charge **0.1%**, and the rate is the same in both directions. Wrap 1 BTC and you mint 0.999 frBTC; unwrap 1 frBTC and you redeem 0.999 BTC.
The fee is not fixed in the contract's code. It is an on-chain parameter that the protocol can change, so treat 0.1% as the rate in force today rather than a permanent guarantee. The app always quotes you the exact amount you will receive before you confirm.
### Next steps
- [Swap](./swap): trade frBTC for other Bitcoin assets.
- [Pools & Liquidity](./pools-liquidity): put your frBTC to work.
- [Tokens & Economics](../start-here/key-concepts): learn how frBTC, DIESEL, and FIRE fit together.
---
## Overview
## Tokens & Economics
The SUBFROST economy is built on a few native assets that reinforce each other. This page is the map; most assets have their own page or guide with the details.
### The assets
- **frBTC** is the base. It is your Bitcoin wrapped 1:1 into a programmable, on-chain form. Everything else is denominated against it. See [Wrap & Unwrap BTC](../using-subfrost/wrap-unwrap-frbtc) and [frBTC Peg & Custody](../protocol/frbtc-peg-and-custody).
- **DIESEL** is the protocol's native emission, issued on-chain in step with real Bitcoin block production.
- **FIRE** is the Alkanes governance token. It rewards the people who provide liquidity to the DIESEL / frBTC pool, through staking and bonds. See [FIRE Vault](../using-subfrost/fire-vault).
- **dxBTC** (planned) is a yield-bearing form of Bitcoin.
- **FUEL** (planned) is the SUBFROST protocol governance token.
### How they fit together
The core of the economy is the **DIESEL / frBTC** liquidity pool:
- **frBTC** brings real Bitcoin into the system, backed 1:1.
- **DIESEL** is minted on-chain and pairs with frBTC in that pool, so the two trade against each other and give the protocol a native unit of account.
- **FIRE** points incentives at exactly that pool: providers of DIESEL / frBTC liquidity earn FIRE, which keeps the pool deep and the market liquid.
Because DIESEL's issuance is anchored to Bitcoin's own block production, the supply side of the economy is tied to Bitcoin rather than to arbitrary inflation.
### Live vs. planned
- **Live:** frBTC (wrap/unwrap, swaps), DIESEL (on-chain emission), FIRE (staking and bonds), frUSD (the Bitcoin-native stablecoin, trading against frBTC).
- **Planned:** dxBTC (yield vaults), FUEL (the SUBFROST protocol governance token).
### Where to go next
- [FIRE Vault](../using-subfrost/fire-vault): earn FIRE.
- [frBTC Peg & Custody](../protocol/frbtc-peg-and-custody): the mechanism underneath.
---
## FUEL Token
:::note[FUEL is not a gas token]
This page is about `FUEL`, the SUBFROST protocol governance token. It is unrelated to lowercase **fuel**, the compute budget an Alkanes contract call may spend. You never buy or spend `FUEL` to transact: fuel is free, and network fees are the ordinary Bitcoin miner fee, paid in native BTC. See [Alkanes](../protocol/alkanes).
:::
FUEL is the SUBFROST protocol governance token, and also a utility token. It is designed to align the incentives of all participants in the network and to capture much of the value created by the SUBFROST protocol.
*FUEL tokenomics are not yet ready for public release, but the following information provides insight into its utility.*
### Governance
`FUEL` token holders are the governors of the SUBFROST system. They are responsible for:
* **Managing the `dxBTC` treasury:** Once dxBTC launches, `FUEL` holders will vote on which yield-generating strategies the `frBTC` held in the `dxBTC` alkane is deployed into.
* **Setting fees:** `FUEL` holders can vote to adjust the fees for wrapping/unwrapping BTC to/from `frBTC` and for other services on the network.
* **Upgrading the protocol:** `FUEL` holders can vote on proposals to upgrade the SUBFROST protocol and its various components.
This decentralized governance model ensures that the SUBFROST protocol is always aligned with the interests of its users and stakeholders.
### Value Accrual
The `FUEL` token is designed to accrue value from the economic activity on the SUBFROST protocol. The primary source of value accrual is from the fees generated at every wrap and unwrap of **native BTC** and `frBTC`, with several other future revenue streams.
Currently, a small fee of 0.1% is charged for `wrap` and `unwrap` operations.
A portion of these fees are used to buy back `FUEL` tokens from the open market and then burn them, creating a deflationary pressure on the token supply.
Additionally, there will be time-locked staking to incentivize liquidity. Once dxBTC launches, holders will be able to stake FUEL/frBTC LP in gauges on our app to boost dxBTC yield. This flywheel ensures positive pressure on FUEL price and market health.
---
## dxBTC Overview
:::info Planned
dxBTC is planned and not live. It cannot be minted or bought today.
:::
### Yield-bearing BTC of Bitcoin L1
`dxBTC` is a planned yield-generating synthetic Bitcoin asset, to be built on top of the SUBFROST ecosystem. It is designed to provide Bitcoin holders with a way to earn a sustainable yield on their assets in a trust-minimized and decentralized manner.
In practice, this will be the simplest possible way for a user to earn BTC yield with their BTC:
1. User will sign a **single** Bitcoin transaction to stake their native **BTC** in the SUBFROST yield vault. The user will receive `dxBTC`, which will represent their staked BTC.
2. Their BTC will be wrapped to `frBTC` and deployed into market-neutral LP strategies and over-collateralized lending protocols across Bitcoin L1, earning yield regardless of BTC price movement.
3. This yield will then be passed back to stakers in the form of native **BTC**, which the user will be able to check at any time.
4. User will sign a **single** Bitcoin transaction to unstake their native **BTC** and burn their `dxBTC`.

These yield vaults will be deployed across Alkanes, Arch, BRC 2.0, and any other programmable ecosystem that launches on Bitcoin mainnet.
This economic model is **already proven** on other blockchains like Ethereum and Solana and can be *dramatically simplified* for the end user.
### How does it work? `frBTC` + Yield
At its core, `dxBTC` will be an alkane that locks up `frBTC`. SUBFROST governance will then act as a decentralized strategist, deploying the locked `frBTC` into various conservative yield-generating strategies.
These strategies may include:
* **Providing liquidity to decentralized exchanges:** The `frBTC` can be used to provide liquidity to delta-neutral LPs like `frBTC`/`wBTC` or other trading pairs on decentralized exchanges, earning trading fees.
* **Lending:** The `frBTC` can be lent out on decentralized, over-collateralized lending platforms to earn interest.
* **Cross-chain Strategies:** The `frBTC` can be trustlessly bridged to other blockchain ecosystems, like Hyperliquid, to participate in yield farming opportunities.
The profits generated from these strategies will then be distributed back to the `dxBTC` holders, allowing them to earn a passive income while maintaining full exposure to their BTC. This will provide a way for Bitcoin holders to put their assets to work without having to sell them or trust a centralized custodian.
### The Role of SUBFROST Governance
**SUBFROST governance** will play a crucial role in `dxBTC`. The governance will be responsible for:
* **Selecting and managing yield-generating strategies:** The governance will research, vet, and select the most promising and secure yield-generating strategies for the locked `frBTC`.
* **Monitoring and rebalancing:** The governance will continuously monitor the performance of the selected strategies and rebalance the portfolio as needed to optimize yield and manage risk.
* **Distributing profits:** The governance will ensure that the profits generated from the yield-generating strategies are distributed fairly to all `dxBTC` holders.
By combining the security and decentralization of SUBFROST with the yield-generating potential of DeFi, `dxBTC` is intended to provide a powerful new tool for Bitcoin holders to grow their wealth.
---
## frBTC (DeFi-compatible BTC)
## frBTC Overview
### Interoperable Wrapped BTC of Bitcoin L1
`frBTC` is the Bitcoin DeFi-compatible asset pegged 1:1 to BTC and interoperable across every DeFi ecosystem on Bitcoin L1. Its Alkane ID on mainnet is `32:0`.
This is achieved through a trust-minimized process that locks BTC on the Bitcoin mainnet (Layer-1) and **atomically** mints a corresponding amount of `frBTC` for the user.
SUBFROST launched on Bitcoin mainnet in Q4 2025. Alkanes (October) & BRC2.0 (December).
### How can I use frBTC?
:::tip FOR THE FIRST TIME
Users can perform a **single** signing process to enter advanced DeFi apps on Bitcoin L1 with native BTC, completely abstracting-away the need to wrap their BTC first.
:::
The example below illustrates a user swapping native **BTC** into `DIESEL` (or any other Alkanes token), atomically, in a decentralized, permissionless manner.
`frBTC` is live on Alkanes and BRC2.0, and we have already begun development to implement it on Arch & MIDL.
### frBTC Roadmap
#### Progressive Decentralization
The launch of `frBTC` on [Alkanes](/key-components/alkanes) and `FR-BTC` (the BRC2.0 deployment of frBTC) marks a pivotal moment for Bitcoin DeFi. For the first time, users can access a truly native, decentralized BTC synthetic without leaving the Bitcoin L1 ecosystem.
This roadmap outlines our "Progressive Decentralization" strategy — a transparent, two-phased approach designed to ensure maximum security as we transition from a system with elements of human-accountability to one based purely on mathematical and economic guarantees.
`frBTC` has undergone extensive ALPHA testing by our team and partners. While we have the utmost confidence in the security of the SUBFROST protocol, this technology is the first of its kind on Bitcoin L1. Our phased approach is designed to protect users as we progressively scale the system's trustlessness.
##### Core Technical Components
The underlying technology remains consistent and robust across all phases, providing a stable and secure foundation for `frBTC`.
- **Libp2p** for peer-to-peer networking.
- **wasm32-wasip2** for deterministic, sandboxed computation.
- **WebTransport (HTTP/3)** for efficient and secure communication.
- **[FROST & ROAST](/key-components/frost-and-roast)** for cutting-edge threshold signatures.
The primary evolution across phases, therefore, is **only** the composition of the signer set.
---
#### ROADMAP
##### ALPHA
The ALPHA phase on Alkanes & BRC2.0 establishes the core functionality of `frBTC` (and `FR-BTC`) with an advanced multisig protocol managed by the SUBFROST team and a trusted group of signers.
- **9 key shares** are held by participants with a significant vested interest in the long-term success of `frBTC`.
- **6 signers** are randomly selected from this group to authorize unwrap transactions.
- **Audits & Transparency:** Formal security audits will begin as soon as possible. We encourage users to review our materials and **only participate in this phase** with a full understanding that although many of its components have been audited, the end-to-end SUBFROST protocol has not. **Use at your own risk.**
- **Length:** Expected to last 6 months to 1 year.
Building is already underway on the **Arch Network** via Saturn's Titan Indexer.
##### Phase 1: Improved Doxxed Multisig
This phase distributes the key shares among **publicly known** and **trusted** entities and individuals.
- **9 initial key shares** but as Total Value Locked (TVL) grows, the number of key shares will grow as well.
- A quorum of **6 out of 9 signers** will be required for any action, ensuring broad consensus among **publicly accountable** participants.
- SUBFROST is expected to become **open source** during this phase.
- **Length:** Expected to last 1-2 years while Bitcoin DeFi expands - fueled by frBTC.
##### Phase 2: Trustless Economic Security
The final phase transitions `frBTC` to a fully trustless and decentralized model secured by robust economic incentives.
- The signers from Phase 1 will be replaced by up to **255 permissionless stakers** who must meet a significant economic threshold. This threshold will dynamically increase as TVL grows.
- With up to 255 signers, **170 of the 255** will be randomly selected to sign unwrap transactions.
- **Staking:** The stake is expected to be a combination of BTC in the form of frBTC, and the [FUEL token](/tokens/FUEL-token), creating a powerful incentive for honest participation.
---
##### Why a Phased Approach?
This strategy is the most responsible way to build critical, decentralized, infrastructure on Bitcoin. It allows us to:
1. Launch `frBTC` on mainnet with an unparalleled level of security, immediately unlocking growth and innovation in Bitcoin DeFi (Phase 1).
2. Leverage the growth of `frBTC` to build the powerful economic incentives required to secure the SUBFROST protocol via a massive, decentralized staking network (Phase 2).
##### The Future is Bright for SUBFROST
The SUBFROST protocol is designed to capture value from the growth it enables.
Currently, a fee of 0.1% across wrap and unwrap transactions provide a sufficient revenue stream with a direct benefit to Bitcoin DeFi at large. We are only getting started.
Furthermore, future offerings like the yield-bearing `dxBTC` token will be built upon the success and utilization of `frBTC`, creating a flywheel of value for stakers and users alike.
The SUBFROST team has dedicated over seven years of research to solving the challenge of securing Bitcoin for productive use. We are proud to deliver on this vision and are more committed than ever to building the future of Bitcoin DeFi.
---
## frUSD (Bitcoin-native stablecoin)
## frUSD Overview
### The Dollar, Settled on Bitcoin L1
`frUSD` is a US-dollar token that lives on Bitcoin L1 as an [Alkanes](../protocol/alkanes) asset. It holds an ordinary Bitcoin UTXO, moves in an ordinary Bitcoin transaction, and can be composed into the same contract calls as any other Alkanes token. There is no better stablecoin UX, or security, anywhere on Bitcoin today.
It is live on mainnet at alkane id `4:1776`.
The point of frUSD is a BTC/USD market that clears on proof-of-work. Until today, stablecoins settle elsewhere and always require users to bridge their BTC onto another chain, or use a centralized exchange to swap it for USD. frUSD settles where BTC already is.
### Where the dollars are
frUSD is **fully reserved, not algorithmic**. There is no on-chain collateral ratio to watch and no liquidation engine. Each frUSD exists because someone deposited a dollar-denominated stablecoin on Ethereum:
1. A user deposits **USDC or USDT** into the SUBFROST bridge vault on Ethereum.
2. The SUBFROST signer group observes the deposit and mints the matching `frUSD` on Bitcoin, to the Bitcoin address named in the deposit.
3. Burning `frUSD` on Bitcoin is the reverse instruction: it releases value back out on Ethereum.
The two heavy arrows are the round trip. The dashed pair below the vault is the reserve: it goes into 3pool on a deposit and comes back out to fund a payout when frUSD burns.
Anchoring to USDC and USDT is a deliberate choice rather than a shortcut. On-chain, the canonical dollar is already settled: USDC and USDT are what the market quotes against and treats as cash. frUSD does not try to unseat that, it anchors to it, so the peg tracks the dollar the ecosystem has already agreed on instead of a synthetic approximation. What SUBFROST changes is not the dollar itself but the blockchain to hold and use it on.
The deposited stablecoins are not left idle. The vault's strategy contract deposits them into [Curve's 3pool](https://curve.fi/), so the reserve backing frUSD is held as 3pool LP rather than as raw USDC and USDT. Changing that strategy takes the same threshold signature as moving the collateral itself.
That shape is doing more work than earning a yield. **A Curve LP token has no issuer, and so no function to freeze a balance: whatever authority a stablecoin issuer holds** (like Tether or Circle)**, they do not hold it over a pool share.**
### The trust model, stated plainly
The reserve sits on Ethereum, in the SUBFROST bridge vault. That vault has no owner key: moving collateral out, changing the strategy and taking fees are all reachable only through a call carrying a BIP-340 signature from the same [FROST](../key-components/frost-and-roast) threshold group that signs frUSD's Bitcoin side. That group is frUSD's own; `frBTC` is signed by a separate federation.
The two dotted lines are the same authority reaching across the gap. Nothing on the Ethereum side moves without a signature from the group that mints on Bitcoin, and the 32 bytes it signs with are one key, not a pair.
#### What this does not remove
- **The collateral is off Bitcoin.** It carries Ethereum risk, Curve risk, and stablecoin issuer risk that `frBTC` does not.
- **The contracts are upgradeable, and unaudited.** Both the vault and the `4:1776` token sit behind upgradeable proxies, and neither has been through a formal security audit yet.
- **Minting and redeeming run on SUBFROST infrastructure with sanctions checks.** The bridge screens deposits against sanctions lists, such as OFAC, and checks payout addresses against the stablecoin issuers' own blacklists. An exit also waits on the signer group to act, with no bound on how long. Holding and moving frUSD on Bitcoin itself is screened by none of that, and has no freeze function.
Use it with all of that in mind.
### How can I get frUSD?
#### Option 1: Swap for it in the frUSD/frBTC pool
The most direct route, and the one that never leaves Bitcoin. `frUSD` trades against `frBTC` in a [CryptoSwap pool](../using-subfrost/btcusd-pool) at `4:1778`, so BTC-denominated value can be moved into dollars and back without touching another chain.
#### Option 2: Mint it by bridging USDC or USDT from Ethereum
You deposit on Ethereum and the `frUSD` is minted to a Bitcoin address you name. The vault takes a flat fee used for gas on Ethereum, and then a percentage of the remainder, currently **$1.99 plus 0.10%**. **Burning back out pays the same flat fee and the same percentage**, so several small transactions to-and-from Ethereum are not recommended for economic purposes.
The minimum bridge amount is **10 frUSD**: below it, the burn reverts rather than paying short.
In the app, choose USDC or USDT as what you are paying with and `frUSD` as what you are receiving. You will be asked to connect an Ethereum wallet and sign two transactions: an approval for exactly the amount you are depositing, and the deposit itself.
If you would rather drive it yourself, [BTC/USD Commands](../api-reference/cli-sdk/btcusd) prints the same two calls for you to sign in your own wallet.
:::warning[A plain transfer to the vault is not a deposit]
The deposit must be made through the bridge contract's own deposit call, which carries the instruction naming your Bitcoin address. **Sending USDC or USDT to the vault address as an ordinary token transfer does not mint anything**: it arrives with no instruction attached, and there is no automatic route back.
:::
#### Option 3: Receive it
`frUSD` is an ordinary Alkanes token. Any wallet that handles Alkanes assets can hold, send, and receive it, and it shows up alongside your other balances.
### What is live today
Being specific about status matters, so here is where each piece stands.
| | Status |
|---|---|
| The `frUSD` token on Bitcoin mainnet (`4:1776`) | **Live** |
| Minting by bridging USDC/USDT from Ethereum | **Live**. If desired as a developer, the command line prepares the two Ethereum calls for you to sign in your own wallet |
| Burning frUSD back out to Ethereum | **Live** |
| Splitting part of a deposit into BTC on the way in | **Live**. A split the network cannot execute pays plain frUSD instead, so read the notes on [BTC/USD Commands](../api-reference/cli-sdk/btcusd) before using it |
| Splitting part of a redemption into ETH on the way out | **Live**. `burn` takes `--to-eth-bps`; a split the network cannot execute pays a plain stablecoin instead, so read the notes on [BTC/USD Commands](../api-reference/cli-sdk/btcusd) before using it |
### Where to go next
- [BTCUSD Pool](../using-subfrost/btcusd-pool): the frUSD/frBTC market itself, and what to know before trading it.
- [Swap](../using-subfrost/swap): trade BTC against USD through the frUSD/frBTC pool.
- [Pools & Liquidity](../using-subfrost/pools-liquidity): what providing liquidity to that pool pays, and how its fee differs from a standard AMM pool.
- [BTC/USD Commands](../api-reference/cli-sdk/btcusd): read the pool, quote a trade, or prepare a bridge deposit from the command line.
- [frBTC Overview](./frBTC-overview): the wrapped-BTC asset frUSD trades against, and how the two custody stories differ.
- [Every Stablecoin Has an Off-Switch. frUSD Doesn't.](https://subfrost.io/articles/frusd-off-switch-free-finance): the essay making the case for why a dollar that settles on Bitcoin is worth building. It argues the destination; this page documents what is built today.
---
## Alkanes Metaprotocol
**Alkanes** is a smart-contract metaprotocol on Bitcoin. It lets developers deploy and run programmable contracts directly on Bitcoin's base layer, with no sidechain and no separate validator set. frBTC, DIESEL, and FIRE are all Alkanes tokens, and SUBFROST's custody logic is coordinated through the Alkanes indexer.
### Where it sits
Alkanes builds on **Protorunes**, which extends the Runes protocol with programmable execution. The layering is:
```
Bitcoin → Runes → Protorunes → Alkanes
```
Because everything is anchored to Bitcoin blocks and validated by Bitcoin consensus, Alkanes contracts inherit Bitcoin's security. Contracts are written in Rust and compiled to WebAssembly (WASM), and are executed deterministically by the indexer, so every node that runs the protocol computes the same state.
### Protostones and cellpacks
A Bitcoin transaction carries an Alkanes instruction as a **protostone**: a protocol message encoded in the transaction's `OP_RETURN`. A protostone can move tokens between outputs and invoke a contract.
To call a contract, the protostone carries a **cellpack**. A cellpack is a list of integers, LEB128-encoded, of the form:
```
[block, tx, opcode, ...args]
```
- The first two values, `[block, tx]`, are the **AlkaneId** of the contract you are calling (see below).
- The next value is the **opcode**, which selects the contract method.
- The remaining values are the method's arguments.
This replaces the older, flat "calldata" model: the cellpack is a length-prefixed list of variable-width integers, not a fixed-size byte array.
### AlkaneId
Every alkane is identified by the location where it was etched:
```
[block, tx]
```
that is, the block height and the transaction index within that block. Some low ranges are reserved for system contracts. In text, an AlkaneId is written `block:tx`.
### Contract interface (ABI)
A contract declares its methods with a `MessageDispatch` enum, tagging each with an `#[opcode(n)]`. At build time this generates a `__meta` export describing the contract's methods, their opcodes, parameters, and return types. You can read that ABI back from any deployed contract through the **`meta`** view function, which is the canonical way to discover what a contract can do. See [Reading Alkane Metadata](../api-reference/guides/alkane-metadata).
### Composing operations in one transaction
A single transaction can carry several protostones that run in order. A common pattern for a rich interaction is three protostones:
1. **Transfer** the tokens a contract will consume to the outputs (and shadow outputs) it reads from.
2. **Mint** (for example, mint DIESEL alongside the interaction).
3. **Call** the contract, which receives the transferred tokens and performs the operation.
The tokens routed in step 1 land on **shadow outputs**, protocol-level outputs that are not real Bitcoin outputs but are addressable by the protocol. This lets a contract receive exactly the assets it should, which keeps each contract's execution isolated. The details of building these are in [Build on SUBFROST](../api-reference/getting-started/overview).
### Reading contract state
You do not read a contract's storage directly. Instead you call its **view functions** through the indexer:
- **`simulate`** evaluates a call read-only and returns the result.
- **`trace`** returns the execution trace for a call.
- The newer **`simulateprotostones`**, **`simulatetransaction`**, and **`simulateblock`** views return full execution traces (per protostone, with fuel used and touched storage) so you can preview exactly what a transaction or block would do before broadcasting it.
These are documented in the [JSON-RPC reference](../api-reference/json-rpc/alkanes).
### Fuel
Contract execution has a compute budget called **fuel**. It is similar to gas on Ethereum in what it measures, and unlike gas in the way that matters most: **fuel is free**.
There is no fuel price, no fuel market, and no balance to top up. Each block carries a fixed fuel budget, and it is shared **only among the transactions that actually invoke a contract**: an ordinary payment neither receives a slice nor dilutes anyone else's. Among the transactions that do invoke one, the budget is split in proportion to size in bytes, with a floor so that even a small one can do useful work. So a lone contract call in a block has the entire budget available to it, not a sliver of it. Fuel a transaction does not spend goes back to the block for the next one. Nothing is charged, in any asset, at any point.
What you pay to get a transaction into a block is the ordinary Bitcoin miner fee: **denominated in native BTC and priced per byte**, exactly as it would be for a transaction that ran no contract at all. Network fees on Alkanes are always BTC, because they are simply Bitcoin transaction fees. There is no separate gas token to acquire before you can transact.
So the lever on your compute budget is your transaction's size, which you are already paying for in sats. Making a call more computationally expensive does not make it more expensive to send.
One caveat, for honesty: if a call ever needs more fuel than its slice allows, it fails, and the remedy is a larger transaction, which does cost more in miner fees. In that corner, bytes buy compute. It is a corner rather than a rule, because the budget is shared only among contract transactions and is therefore generous at present density.
Today on mainnet the budget is 1,000,000,000 fuel per block, with a per-transaction floor of 3,500,000. Both are consensus constants and can change at a protocol upgrade.
An operation that runs out of fuel fails; it does not silently cost more. View functions run with a high fuel ceiling, so read-only queries rarely hit the limit.
Protocol fees are a separate matter from network fees: wrapping and unwrapping BTC charges 0.1% today, and swapping charges a pool fee. Those are charged in the assets involved, and they have nothing to do with fuel.
### Where to go next
- [frBTC Peg & Custody](./frbtc-peg-and-custody): how custody is built on top of this.
- [JSON-RPC reference](../api-reference/json-rpc/alkanes): the view functions in detail.
- [Reading Alkane Metadata](../api-reference/guides/alkane-metadata): discover any contract's ABI.
---
## BRC2.0 Metaprotocol
**BRC2.0** (also called brc20-prog) brings **EVM-compatible smart contracts** to Bitcoin. It lets developers write contracts in Solidity, the language of Ethereum, and deploy them so their state is derived from Bitcoin transactions, without a separate chain. SUBFROST treats it as one of the execution environments that Bitcoin-backed value can move into, alongside [Alkanes](./alkanes).
### How it works
BRC2.0 follows the same broad pattern as Alkanes: state is not held by a new blockchain, it is **derived by an indexer** from data written into Bitcoin transactions.
- Contract deployments and function calls are inscribed as data in Bitcoin transactions.
- An indexer reads those inscriptions, runs the embedded **EVM bytecode**, and maintains contract state across transactions.
Because the execution environment is the EVM, BRC2.0 reuses Ethereum's conventions directly. Contract addresses are derived the Ethereum way, from the deployer and a nonce, and function calls use standard Ethereum **ABI encoding** (a four-byte selector followed by 32-byte arguments). In practice this means much of the existing Solidity toolchain works with little change.
### Inscriptions and front-running protection
A deploy or a call is carried by a Bitcoin inscription, which uses a commit-then-reveal flow. Naively, an inscription flow is exposed to front-running: someone watching the mempool could try to publish a competing reveal first. BRC2.0 guards against this by pre-building and pre-signing the transactions and broadcasting them together, so the reveal is already committed to its rightful creator by the time anything is public.
### frBTC on BRC2.0
frBTC is available inside BRC2.0, so contracts written there can hold and move Bitcoin-backed value the same way any EVM token would. This is what lets Solidity developers build on Bitcoin liquidity without leaving the language and tooling they already know.
### Where to go next
- [Alkanes](./alkanes): the other execution environment SUBFROST is built around.
- [What is SUBFROST](../start-here/what-is-subfrost): the big picture.
---
## How DIESEL Fees Work
The SUBFROST Chrome extension and Android app can pay a transaction's network fee in **DIESEL** instead of BTC. This page explains how that works and why it is safe. For the step-by-step in the wallet, see [Pay Fees in DIESEL](../using-subfrost/pay-fees-in-diesel).
### The problem
Bitcoin miners are paid in BTC, and only in BTC. The fee is whatever the inputs of a transaction are worth minus whatever its outputs are worth. So a wallet that holds DIESEL, frBTC or other Alkanes tokens, but no spare BTC, cannot get a transaction mined on its own.
### One transaction, two contributions
The fix is to let someone who does hold BTC add it to your transaction, and to pay them back **in the same transaction**.
That someone is a **relayer**. When you choose DIESEL as the fee, your wallet builds one Bitcoin transaction that contains:
**Inputs**
- Your own coins, the ones your action needs (for example the token you are sending).
- Your DIESEL.
- One BTC coin from a relayer. Its BTC is what pays the miner.
**Outputs**
- Whatever your action does: the payment, the swap, the wrap or the unwrap.
- Your change, back to your own address.
- The relayer's change: the part of its coin that did not go to the miner.
- An [Alkanes](./alkanes) instruction that pays your DIESEL into a protocol treasury.
So you spend DIESEL, the miner is paid in BTC, and no one takes custody of your coins in between. The fee payment needs no second transaction (an unwrap's BTC payout is still its own transaction, as always).
### Why it is safe
**It is one transaction.** Bitcoin confirms a transaction as a whole or not at all. If it does not confirm, neither the relayer's BTC nor your DIESEL moves. The Alkanes steps inside it (your action and the DIESEL payment) run in the block where it confirms. Before signing, the relayer runs those steps against the current chain and refuses to fund a transaction in which any of them fails. If a step still fails when the block is mined, for example because a swap price moved, the transaction still confirms and the tokens that step carried come back to your own address.
**Your signature covers everything.** When your wallet signs, the signature commits to every input and every output of the transaction, including the exact DIESEL amount. Nobody can change the amount, add an output or redirect a payment afterwards without breaking your signature.
**The relayer signs only its own coin.** The relayer first checks, against the current state of the chain, that the transaction repays it. Then it signs its single input. Its signature also commits to the whole transaction. Your wallet then checks that the only thing the relayer added was that signature, and that the transaction is otherwise exactly the one you signed. If anything else changed, the wallet refuses it.
**You set a cap.** Before anything is signed, the wallet shows you the fee as "up to X DIESEL". If the relayer's quote asks for more than that, the wallet stops and nothing is sent.
### How the price is set
The relayer's cost is in satoshis, and you repay it in DIESEL, so there has to be a DIESEL/BTC price.
- **The source** is the DIESEL/frBTC pool on SUBFROST. frBTC is 1:1 with BTC, so that pool prices DIESEL in satoshis directly.
- **It is a median, not a snapshot.** The price is the median of several readings of the pool taken over recent blocks, so one moment of unusual pool activity does not set it.
- **It is fixed when you get a quote.** The relayer signs the price into the quote it gives your wallet, so it does not change while your transaction is built.
- **Quotes are short-lived.** A quote is only valid for a short time. If it runs out before it is used, the wallet tells you nothing was sent and you can simply try again.
Your wallet also works out its own estimate from the same pool before asking for a quote. That estimate, plus a small margin, is the "up to" cap you approve.
### Where the DIESEL goes
Your DIESEL is paid into a **treasury contract** on-chain, which holds the DIESEL it receives until the protocol collects it. It is pooled rather than paid to one relayer, because the repayment is owed to the relayer network as a whole.
Turning that DIESEL back into BTC is designed to happen later and separately, in the protocol's own transactions, to top up the BTC the relayers put in. None of that is part of your transaction.
### What the relayers are
Relayers are services in the SUBFROST fee network that hold small BTC coins ready to fund transactions like yours. For each quote, a relayer puts one of those coins into your transaction, gets its change back in the same transaction, and is repaid in DIESEL through the treasury. A relayer never holds your coins.
When the network is busy or has no capacity at that moment, the wallet says so and you can pay the fee in BTC as usual.
### For developers
If you build a wallet or app on Bitcoin and would like to let your users pay fees in DIESEL, integration support exists. Contact the SUBFROST team to talk about it.
### Where to go next
- [Pay Fees in DIESEL](../using-subfrost/pay-fees-in-diesel): how to use it in the wallet.
- [Alkanes Metaprotocol](./alkanes): how the DIESEL payment is carried in a Bitcoin transaction.
---
## frBTC Peg & Custody
**frBTC** is backed 1:1 by real Bitcoin. This page explains how that backing is held and how the peg stays honest without a single custodian.
### The 1:1 peg
Every frBTC in circulation corresponds to one BTC locked in custody.
- **Minting frBTC (wrapping BTC) is instant.** When you lock BTC, the metaprotocol mints an equal amount of frBTC to you, 1:1.
- **Redeeming BTC (unwrapping frBTC) goes through the signer group.** When you burn frBTC to redeem, the signers read the contract state directly to see who burned synthetics and should receive Bitcoin, then collectively sign the payout. No single party can move the funds alone.
Because the Alkanes contract records every wrap and burn (see [Alkanes Metaprotocol](./alkanes)), the amount of frBTC and the amount of BTC held always track each other.
### Distributed custody with FROST and ROAST
The BTC that backs frBTC is held at a single Bitcoin address, but no single party controls that address. The key is shared across a group of signers using two threshold-signature schemes:
- **FROST** (Flexible Round-Optimized Schnorr Threshold)
- **ROAST** (Robust Asynchronous Schnorr Threshold)
These let a large, dynamic set of signers collectively produce a single Schnorr signature, without any individual signer ever holding the full private key. The key is generated through a **distributed key generation (DKG)** process, so the full key is never assembled in one place, even by a colluding minority below the threshold.
To authorize an unwrap, a **threshold** of signers must cooperate. Below that threshold, nothing moves.
**No single point of failure:** because the full key never exists in one place, a single compromised signer (or a minority group) cannot move the funds. This is a meaningful improvement over traditional federated or centralized peg mechanisms.
The signers coordinate over a peer-to-peer network built on QUIC and libp2p.
### Current phase vs. the plan
Custody is currently held by **nine signers**, and they are not yet publicly identified. That is the starting point, not the destination. Three steps are planned from here:
1. The keys move as needed so that **at least six of the nine signers are publicly identifiable (doxxed)**.
2. The **signer set expands** beyond today's nine.
3. The signer set transitions into a **permissionless staking network** with up to 255 signers, where nobody can stop you from participating.
The schemes underneath support signing groups of up to 255 participants, so the ceiling is far above the current nine. But nine is what is live. The larger permissionless network is planned, and nothing here should be read as saying it already exists.
### Why it stays on Bitcoin
Because the Alkanes metaprotocol already tracks balances and transfers on Bitcoin, SUBFROST needs no separate blockchain and no heavy external consensus engine to know who owns what. Signers simply run the Alkanes indexer and the SUBFROST software. There is no separate chain to bridge to and no external validator set to trust: the coordination model exists as an extension of the Bitcoin protocol itself. Everything, the synthetic assets and the custody, settles on Bitcoin L1.
This is what makes it different from the usual "wrapped BTC" approach, where a single custodian or a small federation holds the real coins on another chain. Here, custody is distributed across the signer set, and the accounting lives on Bitcoin.
### Where to go next
- [What is SUBFROST](../start-here/what-is-subfrost): the big picture.
- [The Oracle](./the-oracle): how the protocol reads outside data.
---
## frUSD Deposits from Other Chains
:::info[Status: in testing]
The contracts on this page are live on Ethereum, BNB Chain, Polygon and Base. Deposits have gone through the BNB Chain USDT route, up to a payment queued in the frUSD vault for minting on Bitcoin. There is no public way in yet: the SUBFROST app has no screen for it, and the signed deposit on the source chain is broadcast by the SUBFROST operators. This page explains the mechanism; it is not a how-to. The supported way to mint frUSD from USDC or USDT today is the Ethereum deposit in [frUSD Overview](../tokens/frUSD-overview).
:::
The frUSD vault lives on Ethereum. This page shows how stablecoins that start on BNB Chain, Polygon or Base reach it, by following one real deposit (BNB Chain USDT): 3,000.000000 USDT sent on BNB Chain on 30 September 2026, which became payment #52 in the frUSD vault, 2,989.017990 USDT owed as frUSD on Bitcoin.
### The short version
1. **The depositor signs one intent on the source chain.** It is a Permit2 signature whose witness is a `DepositIntent`: the asset, the exact amount, a hash of the Bitcoin recipient, the refund rule, an expiry and a nonce.
2. **The forwarder pulls the stablecoin and bridges it.** The `Permit2SweepForwarder` pulls the stablecoin with Permit2 and hands it to the Stargate pool, addressed to a deposit address (the gateway) on Ethereum that is derived from the intent.
3. **LayerZero delivers it.** The Stargate pool on Ethereum pays the stablecoin to the gateway, which has no code yet.
4. **`settle` sweeps it into the vault.** One call on the `GatewayFactory` deploys the gateway, sweeps it and deposits the stablecoin into the frUSD vault.
5. **The vault queues a payment.** The FROST signer group mints the matching frUSD on Bitcoin, to the script the intent committed to.
Amounts are USDT from the worked example; the dashed box is the gateway, which held the deposit with no code until `settle` deployed it and emptied it in the same transaction.
### Routes
Five bridged routes have their contracts deployed. Each starts with a stablecoin on a source chain and arrives on Ethereum as one of the two tokens the frUSD vault holds.
| Source chain | Token | Decimals | Arrives on Ethereum as |
|---|---|---|---|
| BNB Chain | USDT | 18 | USDT (asset id 1) |
| BNB Chain | USDC | 18 | USDC (asset id 0) |
| Polygon | USDC | 6 | USDC (asset id 0) |
| Polygon | USDT0 | 6 | USDT (asset id 1) |
| Base | USDC | 6 | USDC (asset id 0) |
- **Base has no USDT route.** There is no Stargate pool for it. A pair that is not in the table is not supported.
- **Polygon's USDT reports its symbol as USDT0.** It is the USDT contract on Polygon (`0xc2132D05D31c914a87C6611C10748AEb04B58e8F`), and it arrives as Ethereum USDT.
- **The Polygon USDC route is native USDC, not USDC.e.**
- **USDC or USDT already on Ethereum needs no bridge.** The deposit address receives an ordinary ERC-20 transfer on Ethereum, from a wallet or an exchange withdrawal.
- **A forwarder belongs to a route, not to a chain.** Each `Permit2SweepForwarder` is built for one token on one chain, so BNB Chain has two. The same address can be a different contract on another chain, so a client must look a forwarder up by chain and token, never by chain alone. The addresses are in the Contracts table below.
### The intent
Everything about a deposit is fixed by one `DepositIntent`. These are its fields, in order, with the values of the worked example:
| Field | What it holds | Worked example |
|---|---|---|
| `assetId` (`uint8`) | The vault's asset id: 0 = USDC, 1 = USDT. | `1` |
| `token` (`address`) | The token the frUSD vault holds for that id, on Ethereum. | `0xdAC17F958D2ee523a2206206994597C13D831ec7` (Ethereum USDT) |
| `amount` (`uint256`) | The exact amount that must arrive at the gateway, in the token's base units. | `2992000000` (2,992.000000 USDT) |
| `recipientScriptHash` (`bytes32`) | keccak256 of the Bitcoin scriptPubKey that receives the frUSD. | `0x615f28775040b5eecc619a53fce969d88d2a058566d0242818329a772bd723bb` |
| `convertBps` (`uint32`) | 0 = pure frUSD; above 0, that share in basis points is converted to BTC. | `0` |
| `btcDestHash` (`bytes32`) | keccak256 of the BTC destination when `convertBps` is above 0, else zero. | zero |
| `maxSlippageBps` (`uint16`) | Slippage limit for the BTC conversion; unused when `convertBps` is 0. | `0` |
| `refundAddress` (`address`) | Where a recoverable deposit is returned. Zero means no refund. | zero address |
| `expiry` (`uint64`) | Unix seconds. | `1790805138` (2026-09-30 21:52:18 UTC) |
| `nonce` (`uint256`) | Anti-replay; also the Permit2 nonce. | `1` |
The intent is hashed with EIP-712 under the domain name `SUBFROST frUSD Gateway`, version `1`, chain id 1, and the `GatewayFactory` as verifying contract. The type string is part of the hash, so the field order and the field names are part of it too:
```text
DepositIntent(uint8 assetId,address token,uint256 amount,bytes32 recipientScriptHash,uint32 convertBps,bytes32 btcDestHash,uint16 maxSlippageBps,address refundAddress,uint64 expiry,uint256 nonce)
typehash = 0xd0ba66893952bd61d59c1120e0819129666508130451c7f40bba1c37f5d730c5
```
Every implementation that hashes an intent (the contracts, a wallet, an off-chain service) must produce the same bytes. Reordering two fields, renaming one, or packing the narrow integers instead of widening them to 32 bytes gives a different hash, and with it a different deposit address. The worked example's intent hash is `0xf00c4c2bb3e7a7f0553508e6d4da138400426a81ce6912c665bfde24dd9858da`; `intentHashOf` on the `GatewayFactory` returns the same value.
Two fields read oddly from the source chain's side:
- **`token` names the Ethereum token the route arrives as (USDT or USDC)**, not the token on the source chain. In the worked example that is Ethereum USDT, although the money started as USDT on BNB Chain. The intent is the Ethereum side's object, hashed under the `GatewayFactory`'s domain, and the vault holds Ethereum USDC and USDT, not the source chain's tokens. The forwarder refuses an intent whose `assetId` and `token` are not the pair it is wired for.
- **`amount` is what must arrive, not what is sent.** `settle` reverts if the gateway holds less. The forwarder derives the bridge floor from it: `amount × 10^(source decimals - 6)`, because the vault's tokens have 6 decimals. That factor is 10^12 for both BNB Chain tokens (18 decimals) and 1 on Polygon and Base (6 decimals). A client sets `amount` a little below the bridge's quoted arrival (the client library's default slack is 20 bps): set too high, the deposit is stranded, because `settle` reverts until the gateway is topped up (or reclaimed, if the intent has a refund address); set lower, nothing is lost, because over-funding is relayed in full. In the worked example the depositor sent 3,000.000000 against an `amount` of 2,992.000000, so Stargate's fee fit above the floor.
### One hash, four jobs
The intent hash is used in four places, on two chains:
1. **The CREATE2 salt of the gateway.** The hash fixes the deposit address.
2. **The Permit2 witness on the source chain.** The depositor's signature covers the whole intent. The forwarder sets the Permit2 nonce to the intent's `nonce`, so Permit2 spends the intent itself, not just one signature: the same intent cannot be pulled twice, even through another forwarder.
3. **The registration key on Ethereum.** `register` stores the intent and its Bitcoin script under the hash, and the single-use `consumed` flag is keyed by it.
4. **The argument to `settle`.** `settle(intentHash, tokens)` needs nothing else to find the intent, the gateway and the recipient.
The consequence: the bridge carries only an address and an amount. The forwarder sends Stargate the gateway address and the stablecoin, with an empty compose message. The Bitcoin recipient, the asset, the amount floor and the refund rule never cross chains as instructions; they were fixed by the hash before any token moved.
### The deposit address (the gateway)
Each intent gets its own gateway: an EIP-1167 minimal-proxy clone of one `GatewayProxy` implementation, deployed with CREATE2 and salted with the intent hash.
```text
salt = intentHash
initCode = 3d602d80600a3d3981f3363d3d373d3d3d363d73 5af43d82803e903d91602b57fd5bf3
gateway = last20(keccak256(0xff ++ GatewayFactory ++ salt ++ keccak256(initCode)))
# worked example
GatewayFactory = 0xaF4C39A0Da304eF39D56783d785ea52bb8431362
implementation = 0xdbA4c9fe841A7cf0b37B041126bA4d4640B4F296
salt = 0xf00c4c2bb3e7a7f0553508e6d4da138400426a81ce6912c665bfde24dd9858da
keccak256(initCode) = 0xae78bd81aa686ad972e9cf69b3b526123eae9615a544c18a6b64819d63072968
gateway = 0x0d69fc4dc9c1d34bf63d76bf147a1e1e86065263
```
The init code depends only on the implementation address, so the gateway is a pure function of the factory and the intent hash. Anyone can derive it before anything exists: offline with the formula above, with `gatewayFor(intentHash)` or `gatewayForIntent(intent)` on the `GatewayFactory`, or with `preview(intent, srcAmount)` on the forwarder, which also returns the floor, the LayerZero fee quote and the amount Stargate expects to deliver.
A gateway can only move tokens when the `GatewayFactory` calls it; the factory address is fixed in the implementation's code. The factory picks destinations only from fields inside the intent hash: itself for the intent's token (to relay into the vault), and `refundAddress` for anything else.
In the worked example the Stargate pool paid 2,993.999999 USDT into the gateway at 00:32:11 UTC, when the address had no code. It held the deposit that way for about 12 minutes, and got its code at 00:44:35 in the same `settle` transaction that emptied it. Gateways never self-destruct, so once an address has code it keeps that code.
:::danger[The deposit address exists only on Ethereum]
The gateway is derived from the `GatewayFactory` on Ethereum and is deployed only there. The same hex on BNB Chain, Polygon or Base is not a deposit address. Tokens sent to it on any other chain cannot be recovered by this system.
:::
### The Bitcoin address is a commitment
The intent carries only `recipientScriptHash`, the keccak256 of the Bitcoin scriptPubKey. The source chain never sees the address.
`register` on Ethereum reveals the script and checks that its keccak256 equals `recipientScriptHash`. Anyone may call it, and that is safe: a different script has a different hash, so it belongs to a different intent, whose gateway is a different address with no money at it. `register` also accepts only standard script forms (P2WPKH, P2WSH, P2TR, P2SH, P2PKH) and reverts on anything else, because the vault stores the recipient without checking it.
To re-check the worked example:
```text
# the scriptPubKey revealed by register (34 bytes, P2TR: OP_1, PUSH32, 32-byte key)
recipientScript = 5120c052331c3d07822a338e86cd14d3d011e21e562380ea96f6093f7cc233dab49e
# keccak256 over those 34 bytes
keccak256(script) = 0x615f28775040b5eecc619a53fce969d88d2a058566d0242818329a772bd723bb
= intent.recipientScriptHash
# the same script as a bech32m address (witness version 1, the 32 bytes after 5120)
address = bc1pcpfrx8paq7pz5vuwsmx3f57sz83pu43rsr4fdasf8a7vyv76kj0qvhvqwd
```
### Register, settle, reclaim
The `GatewayFactory` on Ethereum has three calls a depositor needs to know. All three are permissionless.
| Call | Who may call | What it checks | What it pays out | Gas refund |
|---|---|---|---|---|
| `register(intent, recipientScript, btcDestination)` | Anyone | The intent is not registered yet. keccak256 of each script matches the hash in the intent. The vault's `assets(assetId)` is `token`. The scripts are standard and `convertBps` is at most 10,000. | Nothing. It stores the intent and the scripts. | No |
| `settle(intentHash, tokens)` | Anyone | The intent is registered and not yet consumed. It has not expired, if it has a refund address. `tokens` lists the intent's token exactly once (at most 8 entries). The gateway holds at least `amount`. | Deploys the gateway if needed and sweeps it. The intent's token goes into the frUSD vault through `depositAndBridge`; any other listed token goes to `refundAddress`. | Yes, from the float, best effort |
| `reclaim(intentHash, token)` | Anyone | The intent is registered and has a non-zero `refundAddress`. It has expired, or the token is not the intent's token, or the balance is below `amount`, or the intent was already settled. | The gateway's whole balance of that token, to `refundAddress` only. | No |
`settleMany` settles up to 16 intents in one call, with one refund for the batch. `reclaimETH` returns ETH sent to a gateway by mistake, also to `refundAddress` only.
The rules that matter to a depositor:
- **Under-funded is not settleable.** If less than `amount` arrives, `settle` reverts. `reclaim` is one way out, and it needs a refund address. The other is to bring the gateway's balance up to `amount` with a further transfer of the same token on Ethereum, after which `settle` is valid again, unless the intent has a refund address and has already expired; then only `reclaim` applies.
- **Over-funded relays the whole balance.** The extra goes into the vault with the rest. The worked example was over-funded: 2,993.999999 arrived against an `amount` of 2,992.000000.
- **Expiry blocks `settle` only when a refund address is set.** With a refund address, an expired deposit belongs to `reclaim`. Without one, `settle` stays valid after expiry, because relaying is the only way the depositor is paid.
- **`refundAddress` = zero means no refund path at all.** `reclaim` and `reclaimETH` always revert with `NoRefundAddress`, and `settle` reverts if it is asked to move any other token. A wrong token or ETH sent to such a gateway cannot be moved by any call. The worked example used a zero refund address.
- **A non-zero refund address is what makes a mistake recoverable.** A wrong token, a short deposit, a late deposit, or anything that arrives after settlement can be sent back to it by anyone, because it is the only destination `reclaim` can use.
`settle` reimburses its caller's gas in ETH from a float that the SUBFROST operators fund on the `GatewayFactory`. The refund is capped per call and priced at no more than the block's base fee plus a capped tip. When the float is empty, settlement still works: the caller is not reimbursed and the factory emits `GasRefundShortfall`. When the figures on this page were read, the float held 0 wei, so callers were paying their own gas.
The factory's owner can set the refund parameters and withdraw the float's ETH. It has no path to move a deposited token or to redirect a settlement.
### The relayer on the source chain
The signed intent is broadcast on the source chain by a relayer calling `sweepAndBridge(intent, owner, srcAmount, deadline, signature)` on the `Permit2SweepForwarder`. Anyone may call it. What the relayer can and cannot do follows from the contract:
- **It cannot redirect the funds.** Nothing about the destination is a parameter. The forwarder re-derives the intent hash and the gateway address from the signed intent, with the same code the `GatewayFactory` runs.
- **It cannot accept a bad fill.** The minimum Stargate must deliver is derived from the intent (`amount × 10^(source decimals - 6)`), so Stargate reverts rather than deliver less than `settle` will accept.
- **It cannot leave funds half-way.** The Permit2 pull and the Stargate send are one transaction. If the bridge reverts, the pull reverts with it and nothing leaves the depositor's wallet.
- **It cannot reuse the signature elsewhere.** Permit2 binds the spender to the forwarder, so the signature can only be redeemed by this forwarder on this chain.
- **Its only choices are when to broadcast and how much native fee to pay.** Both are its own money: it pays the gas and the LayerZero fee, and Stargate returns any unused native fee to it.
Permit2 can only move a token it has an allowance for, so before the first deposit a Permit2 allowance must exist for the stablecoin on that chain: `approve(Permit2, max)`, once ever per token per chain. USDT on BNB Chain has no `permit`, so there that allowance can only be set by a transaction. Every deposit after it is a signature only. The Permit2 deadline may be at most 7 days after the broadcast; a signature with a later deadline is refused.
### Fees
| Step | USDT | Rule |
|---|---|---|
| Sent on BNB Chain | 3,000.000000 | |
| Stargate fee | 6.000001 | Sent minus arrived. Stargate's fee, 0.2% in this deposit, set by Stargate, not by SUBFROST. It is not fixed: it varies with the pool's balance at the time, so it is quoted live (`quoteOFT`) before signing. |
| Arrived at the gateway | 2,993.999999 | What `settle` relays. |
| Vault flat fee | 1.990000 | `flatFee()` |
| Vault protocol fee | 2.992009 | `protocolFeeBps()` = 10, on the remainder after the flat fee, rounded down |
| Queued for Bitcoin | 2,989.017990 | Payment #52 |
| **Total** | **10.982010** | 0.366% of 3,000.000000 |
The order is fixed: Stargate takes its fee in transit, so the vault sees only what arrived. The vault then takes its flat fee first and the percentage on what remains. Both vault fees are read live at `settle` and can be changed on chain; they apply to bridge-outs too. For current values see [frUSD Overview](../tokens/frUSD-overview).
The network costs are separate. In this deposit the relayer paid the BNB Chain gas (453,336) and the LayerZero fee (0.002225801385214171 BNB). On Ethereum, `register` used 253,158 gas and `settle` 628,204, paid by whoever sent them.
### What the vault records
The frUSD vault stores each deposit as a payment. Payment #52, as `getPayment(52)` returns it:
```text
getPayment(52)
depositor 0xaF4C39A0Da304eF39D56783d785ea52bb8431362 # the GatewayFactory, not the user
assetAmount 2989017990 # 2,989.017990 USDT, after fees
bridgeData 0x12225120c052331c3d07822a338e86cd14d3d011e21e562380ea96f6093f7cc233dab49e
timestamp 1790729075 # 2026-09-30 00:44:35 UTC
assetId 1 # USDT
```
- **`depositor` is the `GatewayFactory`.** The factory made the vault call, so it is recorded as the depositor. Who is paid on Bitcoin is decided by `bridgeData` alone.
- **`bridgeData` is protobuf field 2 plus the script:** `0x12` (field 2, bytes), `0x22` (length 34), then the 34-byte scriptPubKey from `register`. The vault stores it verbatim and never parses it; the factory builds it from the registered script and validates it before every vault call.
- **`frAmount` is fixed at deposit time.** The stored record and the `PaymentQueued` event carry `frAmount` = 298,901,799,000, the same value in frUSD's 8 decimals, so the conversion from 6 to 8 decimals is settled when the payment is queued.
There is no "processed" flag on a payment. The alkane on Bitcoin records how far it has minted, and the at-most-once key for a mint is the pair (vault address, payment id). From Ethereum you can see that a payment is queued, not whether it has been minted.
### The reserve
On every deposit, after queuing the payment, the vault moves its idle USDT into its Curve 3pool strategy, but only once the idle balance (not counting fees) reaches `minSweep`, which is 1,000 USDT. Smaller deposits wait in the vault and go in with a later one. That is why this `settle` swept 3,011.989995 USDT: this payment's 2,989.017990 plus 22.972005 left idle by payment #51, an earlier 24.984999 USDT intent settled at 2026-09-29 20:22:47 UTC that stayed under `minSweep`. Fees are never swept; they stay in the vault.
:::note[For integrators]
A `settle` that triggers the sweep costs more gas: 628,204 for this one, against 403,384 for the previous `settle`, which did not sweep. A gas limit sized for the path without the sweep fails whenever a deposit triggers one, so size it for the sweep.
:::
`pending(assetId)` is a running total of every net amount ever queued for that asset, and it is never decremented. It is not the outstanding liability. Read on 1 October 2026, `pending(1)` was 1,364,365.728510 USDT, more than the vault held: 1,504.071913 idle, which is exactly `feesCollected`, plus 1,362,094.122333 in the strategy. The vault had 53 payments at that point.
### The worked example, end to end
| Time (UTC) | Chain | What happened | Transaction |
|---|---|---|---|
| 2026-09-29 21:52:35 | Ethereum | `register`: the intent and the Bitcoin script are stored under hash `0xf00c…58da`. 253,158 gas. | [0x4fce…2e1a](https://etherscan.io/tx/0x4fce8c88d60aa3050726b17704c1d743b05563a44f6e6d077f083c8e2d8e2e1a) |
| 2026-09-30 00:31:33 | BNB Chain | `sweepAndBridge`: Permit2 pulls 3,000.000000 USDT and the forwarder calls Stargate `sendToken` to the gateway. A relayer pays 453,336 gas and 0.002225801385214171 BNB of LayerZero fee. | [0x8611…8767](https://bscscan.com/tx/0x8611ead789ba2546a00604793cb28db2098d8330dbfd075f6b6d936722208767) |
| 2026-09-30 00:32:11 | Ethereum | 38 s later, the Stargate pool releases 2,993.999999 USDT to the gateway, which has no code. | [0xa4cd…fd15](https://etherscan.io/tx/0xa4cd35770fb089361e66981da91e9ff6d83cd033b30137cbc8f959856f6efd15), [LayerZero message](https://layerzeroscan.com/tx/0x8611ead789ba2546a00604793cb28db2098d8330dbfd075f6b6d936722208767) |
| 2026-09-30 00:44:35 | Ethereum | `settle`: the gateway is deployed and swept, 2,993.999999 USDT goes into the frUSD vault, payment #52 is queued for 2,989.017990, and the vault sweeps 3,011.989995 USDT into its strategy. 628,204 gas. | [0xd0ec…46cd](https://etherscan.io/tx/0xd0ec299b18a3dc126c89e521e330e9102c5e67b49940abb2cbc14d337f5c46cd) |
From the send on BNB Chain to the queued payment took 13 minutes 2 seconds. `register` and `settle` were sent from the depositor's own wallet; both calls are open to anyone.
### Contracts
| Contract | Chain | Address | Role |
|---|---|---|---|
| `GatewayFactory` | Ethereum | `0xaF4C39A0Da304eF39D56783d785ea52bb8431362` | Derives gateways; `register`, `settle`, `reclaim`; relays into the vault. |
| `GatewayProxy` implementation | Ethereum | `0xdbA4c9fe841A7cf0b37B041126bA4d4640B4F296` | The code every gateway clones. |
| `Permit2SweepForwarder` (USDT) | BNB Chain | `0xb24bccc6256309d0dff6883ab874e84179d53da9` | Pulls USDT with Permit2 and bridges it to the gateway. |
| `Permit2SweepForwarder` (USDC) | BNB Chain | `0x9A2Bd5Ab5A9fB426d7F754eae14B54b3C3F93F19` | Pulls USDC with Permit2 and bridges it to the gateway. |
| `Permit2SweepForwarder` (USDC) | Polygon | `0xb24bccc6256309d0dff6883ab874e84179d53da9` | Pulls USDC with Permit2 and bridges it to the gateway. |
| `Permit2SweepForwarder` (USDT0) | Polygon | `0x24167a1b385145057bdc93aabf5dbe2047e5c7c6` | Pulls USDT0 with Permit2 and bridges it to the gateway. |
| `Permit2SweepForwarder` (USDC) | Base | `0x24167a1b385145057bdc93aabf5dbe2047e5c7c6` | Pulls USDC with Permit2 and bridges it to the gateway. |
| Stargate pool (USDT) | BNB Chain | `0x138EB30f73BC423c6455C53df6D89CB01d9eBc63` | Takes the USDT in on BNB Chain. |
| Stargate pool (USDC) | BNB Chain | `0x962Bd449E630b0d928f308Ce63f1A21F02576057` | Takes the USDC in on BNB Chain. |
| Stargate pool (USDC) | Polygon | `0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4` | Takes the USDC in on Polygon. |
| Stargate pool (USDT0) | Polygon | `0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7` | Takes the USDT0 in on Polygon. |
| Stargate pool (USDC) | Base | `0x27a16dc786820B16E5c9028b75B99F6f604b5d26` | Takes the USDC in on Base. |
| Stargate pool (USDT) | Ethereum | `0x933597a323Eb81cAe705C5bC29985172fd5A3973` | Releases USDT to the gateway on Ethereum. |
| Stargate pool (USDC) | Ethereum | `0xc026395860Db2d07ee33e05fE50ed7bD583189C7` | Releases USDC to the gateway on Ethereum. |
| Permit2 | BNB Chain, Polygon, Base | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | The canonical Permit2 the forwarders pull through, at the same address on every chain listed. |
| frUSD vault (proxy) | Ethereum | `0x95779E7E1C943042255B8a78273fe6DE4823CF06` | Takes the deposit and queues the payment for Bitcoin. |
| frUSD vault implementation | Ethereum | `0x5376beedba9ba7f5b0396d28bd75473027b8bd55` | The code behind the vault proxy. |
| frUSD vault strategy | Ethereum | `0x5c5d66c82e2c634074661c3e7427668737c70100` | Holds the reserve in Curve 3pool. |
`Permit2Gateway`, the gasless path for funds already on Ethereum, is not deployed.
### Where to go next
- [frUSD Overview](../tokens/frUSD-overview): what frUSD is, its trust model, and the supported Ethereum deposit.
- [frBTC Peg & Custody](./frbtc-peg-and-custody): threshold custody with FROST and ROAST, as it works for frBTC.
- [Signing and Keys](./signing-and-keys): the threshold signatures behind the signer group.
- [BTC/USD Commands](../api-reference/cli-sdk/btcusd): prepare an Ethereum frUSD deposit from the command line.
---
## Lending Protocol
If you just want to lend or borrow in the app, read [Lending](../using-subfrost/lending) instead. This page is the mechanism underneath it.
SUBFROST lending is a **peer-to-peer, fixed-term, over-collateralized loan market** for Alkanes tokens, settled in a **single Bitcoin transaction** through a pre-signed PSBT escrow. There is no pooled liquidity and no custodian. A lender and a borrower agree on terms off chain, and one transaction atomically creates the loan, delivers the borrowed tokens, and locks the collateral in a freshly cloned loan contract.
Every loan is its own on-chain contract instance, so a loan's entire lifecycle (repayment, collateral release, default) is enforced by code rather than by a counterparty.
### Core ideas
- **Off-chain order book, on-chain settlement.** Makers publish *signed but un-broadcast* offers. Nothing touches the chain until a taker accepts, at which point a single settlement transaction is assembled and broadcast.
- **One transaction, one loan.** The settlement transaction clones the loan-contract template, initializes it with the agreed terms, hands the loan token to the borrower, and locks the collateral, atomically. The loan is `LOAN_ACTIVE` the moment it confirms.
- **No bearer token, no admin.** A party's identity is simply the `scriptPubKey` of the output they receive at settlement. Every later action is authorized by committing that same script. There are no auth NFTs and no privileged roles.
- **Fixed terms.** Interest is `principal × APR × duration`, not floating, so the exact repayment is known at creation and computable in any state.
### Roles and terms
A loan has two parties and a fixed set of terms:
| Term | Meaning |
| --- | --- |
| **loan token / amount** | what the borrower receives (for example 1 DIESEL) |
| **collateral token / amount** | what the borrower locks to back the loan (for example 0.1 frBTC) |
| **APR** | annual rate, stored in basis points |
| **duration** | loan term in blocks. The repayment deadline is `loan_start + duration` |
The **maker** is whoever publishes the offer, and the **taker** accepts it. Either side can be the maker:
- **Lend offer.** The maker is the **creditor** and brings the loan token. A taker borrows against it.
- **Loan request.** The maker is the **debitor** and brings the collateral. A taker fills it by lending.
Internally the contract only knows **creditor** (lender) and **debitor** (borrower). The maker/taker distinction is purely about who published the offer.
### Lifecycle at a glance
```
maker builds + signs offer taker accepts (one tx)
────────────────────────────────────────► ─────────────────────────► LOAN_ACTIVE
• prep tx, broadcast at create • taker prep (token+fee)
• partial settlement PSBT • append taker input + sign
• POST to order book • broadcast taker prep
• broadcast settlement (clone+init)
LOAN_ACTIVE ──repay──► REPAID ──claims──► FULLY_CLOSED
└─past deadline─► DEFAULTED ──claim collateral──► DEFAULTED_CLAIMED
```
### The on-chain contract
#### Template and per-loan clone
A single `lending-contract-psbt` **template** is deployed at a block-4 slot. On mainnet it is **`4:47876`**. Each loan is a **block-6 clone** of that template, produced inside the settlement transaction's first protostone. The clone becomes a child contract at `[2:seq]` and is `LOAN_ACTIVE` immediately. There is **no factory contract**: the host's block-6 clone mechanism creates each loan directly.
#### State machine
```
0 UNINITIALIZED
2 LOAN_ACTIVE
3 LOAN_REPAID
4 LOAN_DEFAULTED
5 REPAYMENT_CLAIMED
7 COLLATERAL_CLAIMED
8 DEFAULTED_CLAIMED
9 FULLY_CLOSED
```
The two claims in the happy path (`ClaimRepayment` by the creditor, `ClaimCollateral` by the debitor) are **independent and order-free** from `REPAID`. Each advances the state before paying, so a repeat reverts, and the loan reaches `FULLY_CLOSED` once both have run.
#### Opcodes
| Op | Name | Caller | Effect |
| --- | --- | --- | --- |
| 0 | Initialize | (settlement) | create and arm the loan from the agreed terms |
| 1 | RepayLoan | debitor (permissionless tokens-in) | send the loan token back, giving `REPAID` |
| 2 | ClaimRepayment | creditor | pull principal plus interest |
| 3 | ClaimCollateral | debitor | reclaim the collateral after repayment |
| 4 | TriggerDefault | anyone, after the deadline | flip an expired `ACTIVE` loan to `DEFAULTED` |
| 5 | ClaimDefaultedCollateral | creditor | seize the collateral after default |
**Views:** `90 GetLoanDetails`, `91 GetRepaymentAmount`, `92 GetState`, `93 GetTimeRemaining`, `99 GetName`.
#### Authorization model
A claim (op 2, 3 or 5) has no signature check. Instead the **calling protostone's `pointer` must reference a real transaction output** whose `scriptPubKey` equals the party's stored script, and the payout is routed to that same output.
The creditor and debitor scripts are recorded at `Initialize` from the settlement's receive outputs. So to claim, a party simply puts their own address at the protostone's pointer output: only they can produce that script, and the payout lands there. A reconnected wallet works because its taproot address still matches.
### The settlement transaction
A single transaction does everything. Its **outputs** are fixed:
```
vout 0 OP_RETURN ← the two protostones (Initialize + refund splitter)
vout 1 3000 sats ← protocol fee
vout 2 dust ← maker's receive output
vout 3 dust ← taker's receive output
```
Which party is creditor and which is debitor maps onto outputs 2 and 3 by the maker's role:
- maker is **creditor**, so `creditorOutput = 2` (maker) and `debitorOutput = 3` (taker)
- maker is **debitor**, so `creditorOutput = 3` (taker) and `debitorOutput = 2` (maker)
#### The two protostones
- **p0, the Initialize message.** Target `{6, templateTx}` (clone the template), opcode `0`, with the terms as calldata. Its `pointer` is the **debitor output**, so the borrowed loan token routes to the borrower. Its `refundPointer` is **p1's shadow vout**, so if anything reverts the inputs flow into the splitter instead of being lost.
- **p1, the refund splitter (edicts only).** On the revert path it returns the **loan token to the creditor** and the **collateral to the debitor** (amount `0` meaning "all"), making both parties whole if the clone and init fails.
`Initialize` calldata argument order:
```
collateral_token(block,tx) · collateral_amount · loan_token(block,tx) · loan_amount
· duration_blocks · desired_apr(bp) · creditor_output · debitor_output
```
The Initialize message's `pointer` **must equal `debitor_output`**. This is how the contract knows which output is the borrower, and therefore the loan recipient.
#### The SIGHASH scheme, why an escrow works
This is the heart of the design. The maker signs **before the taker even exists**, yet the taker can complete the transaction without invalidating the maker's signatures:
- **Maker inputs (0, 1, 2) use `SIGHASH_SINGLE | ANYONECANPAY` (`0x83`).** `SINGLE` makes each maker input commit to **only its own corresponding output**, so the maker pre-commits outputs 0, 1 and 2 (the OP_RETURN, the protocol fee, and their own receive). `ANYONECANPAY` makes each input commit to **only itself** and not the other inputs, so the taker can freely append input 3 later.
- **The taker input (3) uses `SIGHASH_ALL` (`0x01`).** Signed last, it commits the **entire assembled transaction**, locking it in place.
The result: the maker publishes a partial transaction committing exactly their side, any taker can later add their input and output and finalize, and the maker's commitment still verifies.
#### Why the protocol-fee output exists
The maker's partial transaction has **outputs worth more than its inputs**: its three dust inputs come to about 1,638 sats, but it commits the 3,000 sat protocol fee plus dust outputs. That negative balance makes the partial transaction **impossible to broadcast on its own**. It stays inert until the taker adds an input that funds the deficit and the miner fee.
This is deliberate. It prevents a published offer's PSBT from being broadcast early or out of context, and it routes a small protocol fee to the treasury on every settlement.
### PSBT lifecycle in detail
#### 1. Exact-amount UTXOs (the prep transaction)
The contract requires the **incoming alkanes to equal the agreed amounts exactly**, and there is no edict-shifter in the settlement OP_RETURN to peel off excess. So before signing, each party splits their token UTXO into exact-amount pieces with a small **prep transaction**:
```
maker prep → v0 token-only dust (EXACT loan/collateral) · v1 dust · v2 dust · v3 change · v4 OP_RETURN
(v0/v1/v2 become the maker's three SIGHASH_SINGLE settlement inputs)
taker prep → v0 token + btc (exact token + fee budget) · v1 change · v2 OP_RETURN
(v0 becomes the taker's single settlement input)
```
The prep's leading edict peels exactly the needed amount to a dedicated output and routes the leftover (excess token plus BTC) to change.
#### 2. Maker side, building the offer
When a maker publishes an offer, the client:
1. Builds and signs the **prep**, and **broadcasts it right away**, immediately after the settlement-signature gate. Both signatures are collected under a single wallet approval, and the prep goes out only once that gate has passed, so a refused signature leaves nothing on chain.
2. Builds the **partial settlement PSBT** (three maker inputs at `0x83`, the three committed outputs, the OP_RETURN) and signs the maker's inputs.
3. Submits the signed prep hex, the partial PSBT and the terms to the order book. The stored hex is no longer what puts the prep on chain, it is the taker's idempotent re-broadcast fallback for mempool eviction.
The maker can then go offline. The prep spends the maker's UTXOs into the maker's own outputs, so **the funds stay in the maker's wallet**, but the prep itself is a real transaction and the maker pays its miner fee at post time whether or not the offer is ever taken.
:::info[Offers created before 2026-07-12]
The prep used to be **deferred**: signed at post time, held off chain, and broadcast by the taker at settlement. Offers from that era still sit in the order book, which is why the taker path keeps the re-broadcast fallback below.
:::
#### 3. Taker side, accepting
When a taker accepts:
1. They build and sign their own **prep** (their token plus a BTC fee budget) and broadcast it.
2. They **append** their input (input 3, `SIGHASH_ALL`) and receive output (vout 3) to the maker's partial PSBT, then sign input 3. The maker's `0x83` signatures survive untouched.
3. They broadcast the **settlement**, a child of their own just-broadcast prep. The maker's prep has been on chain since the offer was posted, so it is a settled parent rather than a package member.
Before broadcasting, the taker first probes whether the maker's prep is visible on chain. It normally is, and the step is skipped. Only if it has been evicted from the mempool, or the offer predates broadcast-at-create, does the taker re-broadcast the stored hex.
#### 4. Dynamic fee, carrying the taker's prep
The settlement spends **one unconfirmed parent**, the taker's own prep, so the taker sizes the settlement's fee to carry that pair at the chosen fee rate (Child Pays For Parent). The fee is:
```
max( ceil(feeRate × packageVsize) − prepsAlreadyPaid , ceil(feeRate × settlementVsize) )
```
`packageVsize` covers the taker prep plus the settlement, and the maker prep enters it only on a legacy deferred offer, where it becomes a second unconfirmed parent and the package widens to match. The floor on the right keeps the child paying its own way when the deficit is already covered.
### Loan lifecycle operations
After settlement the loan is `ACTIVE`. The portfolio surfaces the right action per party and state.
#### Repay and reclaim (borrower), one transaction
The borrower's "Repay & reclaim" combines **two** operations in a single transaction by appending a second protostone:
```
p0 (shifter) route exactly the repayment (principal+interest) into p1
p1 [child, 1] RepayLoan ACTIVE → REPAID
p2 [child, 3] ClaimCollateral REPAID → COLLATERAL_CLAIMED (collateral → borrower's output)
```
Protostones execute **in order within the transaction**, so `ClaimCollateral` sees the just-`REPAID` state and releases the collateral. One click takes the loan from `ACTIVE` to `REPAID` to `COLLATERAL_CLAIMED`.
#### Default and claim (lender), one transaction
Symmetrically, once the deadline passes, the lender's "Default & claim" combines:
```
p0 [child, 4] TriggerDefault ACTIVE → DEFAULTED
p1 [child, 5] ClaimDefaultedCollateral DEFAULTED → DEFAULTED_CLAIMED (collateral → lender's output)
```
#### Claim repayment (lender)
After the borrower repays, the lender runs `ClaimRepayment` to pull the principal plus interest. Once both the lender's repayment claim and the borrower's collateral reclaim have run, the loan is `FULLY_CLOSED`.
#### Intermediary recovery
If a combined transaction only half-lands (one protostone succeeds, the other reverts), the loan sits in the intermediate state and the UI offers the remaining step:
- repay and reclaim where the reclaim reverted leaves the loan at `REPAID`, and the borrower still sees **Claim Collateral**.
- default and claim where the claim reverted leaves the loan at `DEFAULTED`, and the lender still sees **Claim Collateral**.
The available actions are derived from the **live on-chain state**, so the finish step is always offered.
### Interest math
Interest is fixed at creation, not accrued during the term:
```
interest = floor( loanRaw × aprBasisPoints × durationBlocks / (APR_PRECISION × BLOCKS_PER_YEAR) )
repayment = principal + interest
APR_PRECISION = 10,000 (basis-point denominator)
BLOCKS_PER_YEAR = 52,560 (144 blocks/day × 365)
```
Example, 1 DIESEL at 12.5% APR for 1,008 blocks:
```
100,000,000 × 1250 × 1008 / (10,000 × 52,560) = 239,726
```
giving a repayment of `1.00239726 DIESEL`.
Because it is deterministic from the terms, the exact repayment is known at offer time and computable in any later state. The on-chain `GetRepaymentAmount` view only returns it while the loan is `ACTIVE`.
The division floors, which has a consequence worth knowing: if the terms are small enough that interest rounds to **zero**, the contract **reverts the create**. Clients mirror that guard so the offer fails in the form rather than on chain.
### Collateral policy
The contract itself enforces the collateral **that was agreed**, and nothing more. It has no notion of price, no margin call, and no liquidation path. An active loan can only end by repayment or by claim after default.
The **loan-to-value ceiling** described in the [user guide](../using-subfrost/lending) is an **application-layer policy**, not a contract rule. It is applied when an offer is created and while it sits in the book, using USD prices resolved off chain. It never touches a settled loan. Two consequences follow:
- A loan that was within its LTV ceiling at creation stays valid no matter how far prices move afterwards.
- An offer that nobody has taken can be dropped from the book if its collateral falls below what is owed on it, because it is not yet a loan.
### Off-chain order book
Offers live in a database with **complete physical isolation per network**: a separate offer database per network, selected at request time. There is no shared offers table, so a test loan can never appear on mainnet. Identity and auth data live on a shared default database, since those are global rather than per-network.
Each open offer holds only the **off-chain matching data**: the terms, the maker's role and address, the signed **partial PSBT**, the signed **prep transaction hex** kept as a re-broadcast fallback, and a status (`open`, then `taken` or `cancelled`). Once a loan settles, **nothing about it is read from the database**. The live loan lives entirely on chain.
#### Submission is verified against the signature
A submitted offer is not trusted. The maker's `SIGHASH_SINGLE|ANYONECANPAY` signatures must be valid, **and** the PSBT's committed outputs (the OP_RETURN terms and edicts, the protocol fee, the maker payout) must byte-match what the client builds from the declared terms. Stored metadata therefore cannot diverge from what the maker actually signed. This check is offline and applies on every network.
#### Ownership of edits and deletes
Editing an offer is a full **re-sign**: the amounts are committed in the settlement's OP_RETURN, so changing them rebuilds and re-signs the whole PSBT. On an edit, the old offer is cancelled atomically alongside the new submission, authorized by the new offer's verified maker signatures, so an edit needs no separate cancel proof.
Cancelling or deleting is gated server-side: the request must carry the connected wallet's address, and it must equal the offer's maker address.
#### Dead-offer pruning
An offer is only valid while the three settlement outputs of its prep, `prepTxid:0..2`, are unspent. Those are the maker's `SIGHASH_SINGLE` inputs, so spending one elsewhere invalidates the signed partial and the offer can never settle. The wallet therefore treats them as locked for as long as the offer is open, while the prep's change outputs go back to the normal spendable set. On a legacy deferred offer the same reasoning applies one step earlier, to the prep's own inputs, since spending those double-spends a prep that has not been broadcast yet. The list endpoint prunes offers that fail this check: on networks the server can reach it **deletes** them, and where the chain is only reachable in the browser the client **hides** them. If the UTXO check is unavailable it does **not** prune, surfacing a loading state rather than risking the deletion of a live offer.
### Loan portfolio
Active and historical loans are read **from the chain via the indexer**, not from the database:
1. `get_factory_children` on the lending template enumerates every loan clone.
2. For each clone, `get_keys(/creditor_script, /debitor_script)` is matched against the connected wallet's receive `scriptPubKey`, which gives the wallet's role.
3. Live terms and state come from the contract views (`GetLoanDetails`, `GetRepaymentAmount`, `GetTimeRemaining`).
The UI shows an **effective state**. An `ACTIVE` loan past its deadline is displayed as **Defaulted** even before anyone calls `TriggerDefault`, and the borrower's repay action is withdrawn. The on-chain state only flips to `DEFAULTED` once someone triggers it. Time remaining is computed as `deadline − current_tip_height`.
### Wallets and signing
The hand-built flows (offer creation, take, BTC send) all funnel through one signing entrypoint, so the protocol is wallet-agnostic at the app layer: build PSBT, sign, finalize, broadcast.
- **Keystore, in app.** BIP86 key-path signing with the BIP-341 tweak. P2WPKH (segwit) fee inputs are signed with the BIP84 key.
- **Browser wallets.** The same PSBT is signed through the wallet's adapter, after patching the taproot internal key so the wallet accepts it.
- **Finalization is wallet-agnostic.** Only not-yet-finalized inputs are finalized, so a self-finalizing wallet's pre-finalized maker inputs do not break the taker's finalize.
The alkanes (loan and collateral) always live at the **taproot** address. The BTC prep fee is sourced from the **taproot address first, then the segwit address**, where dual-address wallets keep clean BTC.
### Network configuration
Lending is enabled on a network only when its lending template id is configured, meaning the deployed template's `[4, tx]` slot. Today that is **mainnet**, at `4:47876`, plus the local development network.
Bringing lending to a new network requires three things: deploying the template on that chain and setting its slot in config, a per-network order book database, and an indexer that serves `get_factory_children`.
### Security properties
- **Atomic settlement.** The loan, the loan delivery, and the collateral lock all happen in one transaction. If the clone and init reverts, the refund splitter returns both legs to their owners.
- **No early broadcast.** The maker's partial PSBT cannot be broadcast until a taker funds it, because the protocol-fee output makes its outputs exceed its inputs.
- **Tamper-evident escrow.** The maker's `SIGHASH_SINGLE|ANYONECANPAY` signatures commit exactly their side, and the taker's `SIGHASH_ALL` locks the rest. Neither can alter the other's committed portion.
- **Verified submissions.** An offer's stored terms must byte-match the outputs the maker signed, so the book cannot advertise terms the maker did not commit to.
- **No custodian, no admin keys.** Authorization is "commit your own script". Collateral and repayments are only ever released to the party whose stored script matches the claim's output.
- **Per-network isolation.** Offer data is physically separated per network, so test and production loans can never mix.
### Next steps
- [Lending](../using-subfrost/lending): the user guide for the same feature.
- [Alkanes Metaprotocol](./alkanes): protostones, cellpacks, and the contract model this builds on.
- [What is SUBFROST](../start-here/what-is-subfrost): the big picture.
---
## Signing and Keys
SUBFROST custody rests on a few pieces of well understood cryptography. This page explains them in one place: the Schnorr signatures that Bitcoin already uses, the threshold schemes that let a group sign together, the ceremony that generates the shared key, and how your keys are protected on your device.
For how these secure the frBTC peg specifically, see [frBTC Peg & Custody](./frbtc-peg-and-custody).
### Schnorr signatures
A **Schnorr signature** is the digital signature scheme that Bitcoin adopted in the Taproot upgrade (BIP-340). It works over **secp256k1**, the same elliptic curve Bitcoin has always used, so a Schnorr signature is a native, first-class Bitcoin signature. This matters for SUBFROST: the group has to produce signatures that spend real BTC, so the signing scheme has to speak Bitcoin's own language.
Schnorr has three properties that make it the right building block here:
- **Linearity.** Signatures and keys add together cleanly. Several signers can each produce a partial signature and those partials combine into one valid signature. This is the property that makes threshold signing possible, and it is exactly what older ECDSA signatures lack.
- **Fixed, compact size.** A Schnorr signature is 64 bytes.
- **Provable security.** The scheme is secure under standard assumptions, as long as the discrete logarithm problem stays hard.
Because the combined signature is an ordinary Schnorr signature, what lands on Bitcoin is indistinguishable from a single-key spend. The chain does not see a multisig; it sees one signature.
### Threshold signing with FROST and ROAST
SUBFROST does not give any one party the key that controls custody. Instead the key is shared across the signer set using two threshold schemes, FROST & ROAST. You can read about these in [frBTC Peg & Custody](./frbtc-peg-and-custody).
FROST and ROAST together let a group of `n` signers produce a signature only when a threshold `t` of them cooperate, without any single signer ever holding the whole private key.
Signing happens in two phases:
1. **Pre-computation.** Each participant generates nonce material ahead of time and shares the public part. This can be done before any specific signature is needed.
2. **Online signing.** When a signature is required for a specific message, such as a Bitcoin withdrawal transaction, each participant uses their secret share and a pre-computed nonce to produce a **partial signature**. A coordinator, which can be any participant, aggregates the partials into one valid signature.
ROAST is the piece that makes this robust in the real world: it keeps the signing session moving even when some participants are slow or drop offline, so a single unresponsive signer cannot stall a withdrawal.
The signer set is not fixed forever. Participants can join or leave through a **reshare**, which redistributes the shares to a new group while keeping the same group public key, so the custody address does not change and no funds move.
### Distributed key generation
Before the group can sign anything, it has to agree on a shared key. It does this through **distributed key generation (DKG)**, an interactive ceremony where the participants jointly produce a single group public key while each one ends up holding only a secret **share** of the matching private key.
The important property is that the full private key is never assembled anywhere, not on any one machine and not even by a colluding subset below the threshold. There is no moment where the complete key exists to be stolen. This is the foundation the whole custody model stands on.
### Keys on your device
As a user, you also hold keys: the secret behind your own wallet, and, if you take part in signing, your share of a group key. These live in a **keystore** on your device.
A keystore never holds secrets in plaintext. When you set it up, you choose a passphrase, and:
1. The passphrase is stretched through a **key-derivation function** with a random salt. This is deliberately slow, so guessing the passphrase offline is expensive even if someone copies the file.
2. The derived key encrypts your secrets with **AES-256-GCM**, an authenticated cipher that protects both confidentiality (the data is unreadable) and integrity (tampering is detected).
On mobile, this is backed by the device's secure hardware where available, using **StrongBox** on Android and the **Secure Enclave** on iOS, so the protecting key can be bound to the device and never exposed to the app or the operating system.
### Where to go next
- [frBTC Peg & Custody](./frbtc-peg-and-custody): how this signing secures the BTC behind frBTC.
- [What is SUBFROST](../start-here/what-is-subfrost): the big picture.
---
## The Oracle
Some parts of the protocol need information that does not live on Bitcoin, most importantly **prices**. For example, FIRE bond pricing needs a reference price to work. SUBFROST provides this through a decentralized **oracle**.
### How it works
Rather than trusting a single server to report a value, the oracle runs as a small **distributed group**. Each participant independently reads the relevant external source and the current chain state, and the group must reach agreement before a value is committed on-chain. A single participant cannot dictate the result on its own.
Because the value is agreed by a threshold of the group and posted on-chain, contracts that depend on it (such as the [FIRE](../using-subfrost/fire-vault) bond mechanism) can read a price that is resistant to any one party manipulating it.
### Where it is used
- **FIRE bonds.** The bond price is anchored to an oracle-reported reference, with a floor tied to the protocol treasury, so bonds cannot be sold below the value backing them.
- **Perpetual futures (unreleased).** Details will be released when ready.
### Where to go next
- [FIRE Vault](../using-subfrost/fire-vault): where the oracle price is used.
- [What is SUBFROST](../start-here/what-is-subfrost): the big picture.
---
## BRC2.0 Integration
[BRC2.0 Metaprotocol](../protocol/brc20) runs EVM-compatible smart contracts on Bitcoin. If you write Solidity, you can deploy and call contracts here with tooling you already know, and reach Bitcoin-backed frBTC from inside them. This page covers the developer workflow through `alkanes-cli`. For the concept, see [BRC2.0 Metaprotocol](../protocol/brc20); for the full command list, see the [CLI & SDK Reference](../api-reference/cli-sdk/brc20-prog).
### The endpoint
BRC2.0 has its own JSON-RPC endpoint, separate from the Alkanes one. Pass it with `--brc20-prog-rpc-url`:
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog block-number
```
It speaks standard Ethereum JSON-RPC (`eth_call`, `eth_getBalance`, `eth_getCode`, `eth_getLogs`, `eth_chainId`, and the rest, see the [BRC20 Programmable Module (JSON-RPC)](../api-reference/json-rpc/brc20-prog) reference for the full method list), plus a few `brc20_*` methods for inscription-aware lookups.
### Deploying a contract
Compile your contract with Foundry, then deploy the build JSON:
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog deploy-contract ./out/MyContract.sol/MyContract.json \
--fee-rate 10
```
Useful flags:
| Flag | Description |
|------|-------------|
| `--from ` | Addresses to source UTXOs from |
| `--change ` | Change address |
| `--no-activation` | Skip the activation transaction (use the two-transaction commit/reveal pattern) |
| `--trace` | Enable transaction tracing |
| `--mine` | Mine a block after broadcasting (regtest only) |
A deploy is carried by a Bitcoin inscription with a commit-then-reveal flow. By default the tooling also sends an activation transaction, a third transaction after the commit and reveal; pass `--no-activation` to skip it and use only the two-transaction commit/reveal pattern. If any of your UTXOs carry inscriptions, the tooling automatically inserts a split transaction first, moving the inscribed sats to a safe output before spending clean sats for the commit, and broadcasts the bundle atomically.
### Calling a contract
State-changing calls use `transact`; read-only calls use `call`:
```bash
# State-changing: transfer(address, uint256)
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog transact --address 0xYourContract --signature "transfer(address,uint256)" --calldata "0xRecipient,1000" \
--fee-rate 10
# Read-only (eth_call): raw calldata
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog call --to 0xYourContract --data 0x70a08231...
```
`transact` takes a human-readable function signature (`--signature`) and comma-separated arguments (`--calldata`), so you do not encode the calldata by hand. `call` takes raw ABI-encoded data, mirroring `eth_call`.
### Wrapping BTC into a contract call
`wrap-and-execute2` wraps BTC to frBTC and calls a target contract in the same flow, so a user can go from native BTC to a contract interaction in one step. `--signature` names the function to call on `--target`; pass its arguments as comma-separated values with `--calldata` (empty by default, so `deposit()` needs none):
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog wrap-and-execute2 100000 \
--target 0xYourContract \
--signature "deposit()" \
--fee-rate 10 \
-y
```
To wrap BTC to frBTC without calling a contract, use `brc20-prog wrap-btc --from ` instead.
### Reading state
```bash
# frBTC balance of an address (eth_getBalance)
brc20-prog get-balance 0xYourAddress
# Contract bytecode (eth_getCode)
brc20-prog get-code 0xYourContract
# Event logs (eth_getLogs)
brc20-prog get-logs --address 0xYourContract --from-block 840000 --to-block latest
```
### Where to go next
- [BRC2.0 Metaprotocol](../protocol/brc20): how the protocol works.
- [CLI & SDK Reference](../api-reference/cli-sdk/brc20-prog): every `brc20-prog` command and flag.
- [Wrapping frBTC](./wrapping-frbtc): the Alkanes-side wrap flow.
---
## Connecting a wallet
A web app never holds the user's keys. To do anything that costs money or moves assets, it asks a wallet to sign. On SUBFROST there are two ways to reach that wallet, depending on where the user is.
### Two connection paths
- **Injected provider.** When the user has the SUBFROST browser extension installed, it injects a provider object that your page can call directly. This is the smoothest path when it is available.
- **Remote signing.** When the user is on a desktop browser with no wallet present, the app pairs with the wallet on the user's phone through the SUBFROST pair bridge, with end-to-end encryption. The app shows a QR code, the phone scans it, and from then on the app sends sign requests to the phone, which prompts the user for each one. This is documented in full on the [WalletConnect](../api-reference/guides/walletconnect) reference page.
Both paths expose the same two operations that matter: **sign a PSBT** (to authorize a Bitcoin transaction) and **sign a message** (to prove control of an address). The app builds the transaction or message, the wallet approves and signs, the app broadcasts.
### The injected provider
When the SUBFROST browser extension is installed, it defines `window.subfrost` on every page. The object is read-only, and its shape follows the same convention as other Bitcoin wallet providers, so if you have integrated a Bitcoin wallet before, the method names will look familiar.
#### Detecting the wallet
The extension injects the provider as the page loads, so a script that runs very early can execute before `window.subfrost` exists. Handle both cases: check for the object, and also listen for the `subfrost:initialized` event the extension fires once the provider is ready.
```javascript
function getProvider() {
return typeof window !== 'undefined' ? window.subfrost : undefined;
}
// Already injected, the common case
let subfrost = getProvider();
// Injected later, for scripts that run before the extension
window.addEventListener('subfrost:initialized', (event) => {
subfrost = getProvider();
// event.detail: { id: 'subfrost', name: 'Subfrost', icon: '/icons/icon-128.png' }
});
if (!subfrost) {
// No extension in this context. Fall back to remote signing (QR pairing).
}
```
#### Connecting and reading account state
`requestAccounts` is the connect call. The first request from a site the user has not connected yet, whichever method it is, opens a connect prompt in the wallet; the request continues if the user approves and fails if they decline. `getAccounts` sends exactly the same request as `requestAccounts`, so it also prompts on a site that is not connected yet.
Both resolve with an array holding one address: the address currently selected in the wallet (the active account and address type). While the wallet is locked, the request waits for the user to unlock rather than failing, and the page's own 60-second timeout applies.
```javascript
// Prompts the user to connect, returns the selected address
const accounts = await subfrost.requestAccounts();
// The network the wallet is on. The extension currently always answers 'mainnet'.
const network = await subfrost.getNetwork();
// Taproot x-only public key of the active account, 64 hex characters
const pubkey = await subfrost.getPublicKey();
```
`getPublicKey` accepts an address argument but ignores it: it always returns the x-only taproot public key of the active account. It needs an unlocked wallet and waits for an unlock like `getAccounts`.
#### Signing
```javascript
// Sign one PSBT (hex). The user approves it in the wallet.
const signedPsbtHex = await subfrost.signPsbt(unsignedPsbtHex);
// Keep the signed PSBT unfinalized, for example when another party still has to sign
const partial = await subfrost.signPsbt(unsignedPsbtHex, { autoFinalized: false });
// Request a sighash type for a specific input (0x83 = SIGHASH_SINGLE | ANYONECANPAY)
const listing = await subfrost.signPsbt(unsignedPsbtHex, {
autoFinalized: false,
toSignInputs: [{ index: 0, sighashTypes: [0x83] }],
});
// Prove control of an address
const signature = await subfrost.signMessage('Authorize this action', accounts[0]);
```
`signPsbt(psbtHex, options?)` options:
| Option | Effect |
| --- | --- |
| `autoFinalized` | When omitted, the wallet finalizes the inputs it signs. Pass `false` to get a signed but unfinalized PSBT back. |
| `toSignInputs` | Array of `{ index, sighashTypes? }` entries that request a sighash type for an input. It does not choose which inputs get signed. |
`signMessage(message, address)` signs with the format that fits the address: BIP-322 for taproot addresses, BIP-137 (legacy Bitcoin Signed Message) for the others.
#### Building PSBTs for the extension
- **Inputs.** The wallet signs every input it owns and leaves the rest alone. It recognizes its inputs by the `witness_utxo` script, so set `witness_utxo` on every input. For p2wpkh inputs, also set `bip32_derivation` with the wallet's public key.
- **Other parties' inputs.** By default the wallet finalizes the PSBT, and it refuses if any input is left unsigned. When the PSBT has inputs that someone else signs, pass `autoFinalized: false`.
- **Coin selection is yours.** The wallet does not pick inputs for your PSBT. Choose the inputs yourself and leave out UTXOs that carry Alkanes, or those assets move with the transaction.
- **Fee.** If your PSBT pays less than the wallet's current fee rate, the wallet can raise the fee before signing, by lowering your change output or adding an input and change output of its own. Read the transaction back from the returned PSBT.
- **Broadcast.** The provider has no broadcast method. Your app broadcasts the signed transaction.
#### Signing several PSBTs
Two calls take an array of PSBT hex strings, and they differ in how many times the user is asked to approve:
- **`signPsbts(psbts, options?)`** sends one request per PSBT, each with the same `options` as `signPsbt`. The user approves them one at a time.
- **`signPsbtBundle(psbts, options?)`** sends the whole array as a single request, and the user approves the bundle once. The approval shows every transaction in the bundle; when the wallet recognizes the bundle as a known operation, it also labels the approval with that operation. `options` is accepted but not applied: every PSBT in a bundle is signed with the defaults (finalized, default sighash).
Both resolve with the signed PSBTs in the same order as the input, and both resolve with an empty array when given one. If the user rejects, the whole call rejects and nothing is returned, so re-submit the unsigned remainder rather than expecting a partial result.
```javascript
// One approval per PSBT
const signed = await subfrost.signPsbts([psbtA, psbtB, psbtC]);
// One approval for the whole bundle
const signedBundle = await subfrost.signPsbtBundle([psbtA, psbtB, psbtC]);
```
#### Reacting to wallet changes
The provider is an event emitter. Subscribe with `on`, unsubscribe with `off`.
| Event | Handler arguments | Fires when |
| --- | --- | --- |
| `accountsChanged` | `(accounts)`, an array with the newly selected address, or empty | The wallet is unlocked, or the user switches wallet, account or address type, or adds or removes an account. An empty array means the wallet was locked. |
| `disconnect` | none | The user revokes your site under Connected Sites |
```javascript
const onAccountsChanged = (accounts) => {
// Re-read state, or treat an empty list as "locked"
};
subfrost.on('accountsChanged', onAccountsChanged);
subfrost.on('disconnect', () => {
// Drop the session and show your connect button again
});
// Later, when your component unmounts
subfrost.off('accountsChanged', onAccountsChanged);
```
There is no network-changed event. Read `getNetwork()` when you need the current network.
#### Errors and timeouts
Every method rejects with an `Error` whose message is prefixed with a code in square brackets, followed by a human-readable message:
```
[REQUEST_TIMEOUT] subfrost: request timed out
```
Two codes matter to your app:
| Code | Meaning |
| --- | --- |
| `REQUEST_TIMEOUT` | 60 seconds passed with no answer from the wallet. The user most likely never saw the prompt, or the wallet stayed locked. |
| `UNKNOWN_ERROR` | The request did not go through, including the user rejecting it or declining the connect prompt. |
Treat any other code as a failure to reach the extension.
A request that goes unanswered rejects after 60 seconds. Treat that as "no answer" rather than as a refusal, and invite the user to open the wallet and retry.
:::warning[A user rejection is not distinguishable from other failures today]
When the user rejects a prompt, the wallet replies with a message but no code, so it reaches your page as `UNKNOWN_ERROR`. The accompanying message is localized, which means it changes with the user's language and is not safe to match on. Until a dedicated rejection code exists, treat any `UNKNOWN_ERROR` from a signing call as "the request did not go through", and let the user retry rather than showing a rejection-specific error.
:::
```javascript
try {
const signed = await subfrost.signPsbt(psbtHex);
} catch (err) {
if (err.message.startsWith('[REQUEST_TIMEOUT]')) {
// No answer. Ask the user to open the wallet and try again.
} else {
// The user rejected, or the wallet reported an error. Let them retry.
}
}
```
### Remote signing (QR pairing)
When there is no injected provider, pair with the mobile wallet over the SUBFROST pair bridge. The app renders a pairing QR code, the phone scans it and connects to the app through the bridge, and a session is established. Requests and responses are end-to-end encrypted: the bridge never sees the pairing code or the app's public key, so it cannot read or change a request, and every signing request is approved on the phone.
The full protocol (pairing URI, bridge frames, key derivation, framing, message format, security model) and the reference client are on the [WalletConnect](../api-reference/guides/walletconnect) page.
### Where to go next
- [WalletConnect](../api-reference/guides/walletconnect): the remote-signing protocol in detail.
- [Wrapping frBTC](./wrapping-frbtc): a first transaction to sign.
- [Safety](../using-subfrost/safety): the fresh-wallet habit for Alkanes.
---
## Indexing with metashrew
Alkanes does not have its own chain. Its state is **derived from Bitcoin** by an indexer called **metashrew**, which runs a WebAssembly program (the Alkanes indexer, `alkanes.wasm`) against block data and answers queries about the result. Understanding this layer matters whenever you read on-chain state, and it is required if you want to run your own infrastructure.
Most developers never run an indexer: they use the [hosted JSON-RPC endpoint](../api-reference/json-rpc/overview). This page is for when you want to know how it works, or run it yourself.
### What metashrew does
metashrew is a general indexer that executes WASM against the Bitcoin blockchain. The Alkanes protocol ships as one such WASM program. Because the same program that indexes the chain is also the one that answers reads, the reads are a faithful projection of consensus state, not a separate database that could drift.
Two ideas do most of the work:
- **View functions.** A read is a **view function** invoked through `metashrew_view`: you name the view (`simulate`, `trace`, `meta`, and so on), pass its input, and pick a block tag. This is how you query balances, simulate a call, get an execution trace, or read a contract's ABI. The full catalog is on the [`metashrew_*` reference](../api-reference/json-rpc/metashrew).
- **Cost.** `simulate` is the heaviest call because it loads and runs the WASM. Reading a raw storage slot with `getstorageat` is much cheaper: it just fetches the value at a path without a full evaluation. Reach for the cheapest call that answers your question.
### How metashrew runs
metashrew runs as a single binary, `rockshrew-mono`, that bundles the runtime with a local RocksDB database and serves the JSON-RPC view layer from the same process. Because one binary indexes the chain and answers reads, the reads cannot drift from consensus state.
The runtime is async and times out long-running requests, and host functions can be added without breaking the interface. A newer, unreleased line of work adds Block-STM style parallel block execution to speed up indexing, on the same RocksDB model.
### Running an indexer
`rockshrew-mono` takes a Bitcoin Core RPC source, a database path, and the Alkanes indexer WASM:
```sh
rockshrew-mono \
--daemon-rpc-url http://localhost:8332 \
--auth bitcoinrpc:password \
--indexer ./alkanes.wasm \
--db-path ~/.metashrew \
--start-block 880000 \
--host 0.0.0.0 \
--port 8080 \
--cors '*'
```
`--start-block 880000` is the Alkanes genesis height, so the node skips the pre-genesis chain. Syncing from there still takes on the order of days.
To skip the full sync, bootstrap from existing state. There are two different mechanisms, and they are easy to confuse:
- **The published tarball.** SUBFROST serves `https://cdn.subfrost.io/snapshots/latest.tar.gz`, a gzipped copy of a healthy indexer's whole `/data` directory. You extract it into your database path yourself and start `rockshrew-mono` with no `--start-block`; it reads the height out of the database and resumes. Check its size and `Last-Modified` before you commit to it — see [Snapshot Download](../api-reference/platform/snapshots) for the procedure and the current figures.
- **`--repo `.** metashrew's own incremental repo sync, which expects an `index.json` at that base listing per-interval WASM and diff files. A node publishes such a repo with `--snapshot-directory` and `--snapshot-interval`. The SUBFROST CDN path is **not** a `--repo` base: it holds the tarball, not an `index.json`.
Business-tier accounts can also pull the raw database over [rsync](../api-reference/platform/rsync).
### Building your own indexer WASM
metashrew runs any WASM program that implements its host interface, so you can write custom indexing logic, not just run the Alkanes program. A program processes each block and writes key-value state through the runtime, and exposes view functions for reads. The runtime is async, times out long-running requests, and can add new host functions without breaking the interface.
One caveat comes with the unreleased parallel-execution work: block execution can run in parallel, so the old "same WASM, same index" guarantee no longer holds automatically. If you write an indexer that relies on ordering, write it with **serializable semantics** so the result does not depend on execution order.
### When to use metashrew vs a data indexer
metashrew is the source of truth, but it is not built for bulk history or dashboards. A secondary data indexer covers that ground: trade history, per-pair statistics, holder lists, and similar aggregates that the base indexer does not compute.
The rule of thumb:
- Data that becomes input to a **user-signed action** should come from metashrew view functions (the verifiable path).
- Data that only **renders a screen** (history, charts, leaderboards) is a better fit for a secondary data indexer.
### Where to go next
- [`metashrew_*` reference](../api-reference/json-rpc/metashrew): every view method and its inputs.
- [`alkanes_*` reference](../api-reference/json-rpc/alkanes): the simulate and trace views in detail.
- [Interacting via the CLI](./interacting-via-cli): query state without running a node.
---
## Interacting via the CLI
`alkanes-cli` is the command-line tool for working with the Alkanes metaprotocol: wallets, deployment, contract calls, and chain queries. This page is a task-oriented tour. For the exhaustive command list, see the [CLI & SDK Reference](../api-reference/cli-sdk/overview).
To install it, follow the [Quickstart](./quickstart).
### Choosing a network
Every command takes a network through `-p` (provider): `regtest`, `signet`, or `mainnet`.
```bash
alkanes-cli -p signet
```
Common global options:
| Option | Description |
|--------|-------------|
| `--wallet-file ` | Path to the wallet file |
| `--passphrase ` | Passphrase used to sign |
| `--wallet-address ` | Watch-only, read-only mode |
| `--jsonrpc-url ` | Custom JSON-RPC endpoint |
For production traffic, pass an API key through `--jsonrpc-url` for higher rate limits. See [Authentication](../api-reference/getting-started/authentication) and [Rate Limits](../api-reference/platform/rate-limits).
### Wallet basics
```bash
# Create a new wallet, or restore one from a mnemonic phrase
alkanes-cli wallet create
alkanes-cli wallet create ""
# List addresses (type:start-end)
alkanes-cli wallet addresses p2tr:0-5
# Balances and unspent outputs
alkanes-cli wallet balance
alkanes-cli wallet utxos
```
See [Wallet Commands](../api-reference/cli-sdk/wallet) for the rest of the namespace: signing, history, backups, and fee rates.
### Deploying and calling contracts
Contract operations live under the `alkanes` namespace. A contract is addressed by its `[block, tx]` Alkane ID, and a call is a **cellpack**, a list that begins with the target ID and an opcode (see [Alkanes Metaprotocol](../protocol/alkanes)).
```bash
# Execute a call: contract [2,1], opcode 1
alkanes-cli -p signet alkanes execute "[2,1,1]" --fee-rate 10 -y
# Read state by simulating a call without broadcasting: contract 2:1, opcode 2
alkanes-cli -p signet alkanes simulate "2:1:2"
# Inspect a contract's bytecode and metadata
alkanes-cli -p signet alkanes inspect "2:1" --meta
```
The `-y` flag auto-confirms. `execute` produces a Bitcoin transaction and pays a fee; `simulate` reads indexer state and costs nothing. See [Alkanes Commands](../api-reference/cli-sdk/alkanes) for the full flag set on `execute`, `simulate`, and `inspect`.
### Querying Bitcoin directly
The CLI also proxies Bitcoin Core and Esplora, so you can inspect the underlying chain without a second tool:
```bash
# Bitcoin Core RPC
alkanes-cli bitcoind getblockcount
alkanes-cli bitcoind getrawtransaction
# Esplora
alkanes-cli esplora address
alkanes-cli esplora tx
alkanes-cli esplora fee-estimates
```
The `esplora` namespace has its own reference page ([Esplora Commands](../api-reference/cli-sdk/esplora)); the `bitcoind` commands mirror the methods documented on [Bitcoin Core RPC](../api-reference/json-rpc/bitcoind).
### Broadcasting options
By default a transaction goes to the public mempool. For higher-value or MEV-sensitive transactions, the CLI can route through private relays instead:
| Option | Description |
|--------|-------------|
| (default) | Standard mempool broadcast |
| `--use-slipstream` | Submit through MARA Slipstream (bypasses the public mempool) |
| `--use-rebar` | Submit through Rebar Shield (private relay) |
| `--mine` | Mine immediately (regtest only) |
### Getting help
```bash
alkanes-cli --help
alkanes-cli --help
```
### Where to go next
- [Wrapping frBTC](./wrapping-frbtc): a full wrap and unwrap walkthrough.
- [CLI & SDK Reference](../api-reference/cli-sdk/overview): every command and flag.
---
## Build on SUBFROST
SUBFROST is built on **Alkanes**, a smart-contract metaprotocol that runs on Bitcoin (see [Alkanes Metaprotocol](../protocol/alkanes)). If you can write a contract or a web app, you can build on it: tokens, AMMs, vaults, and anything that reads or moves Bitcoin-backed value like frBTC.
This section is the developer track. It gets you from an empty machine to a deployed contract and a working integration.
### The toolchain
You will use a small set of tools, all of them public:
- **`alkanes-cli`** for wallets, deployment, and calling contracts from the command line. It is built from source (see [Quickstart](./quickstart)).
- **`@alkanes/ts-sdk`** for talking to the chain from TypeScript, in a web app or a backend.
- **A hosted JSON-RPC endpoint**, so you do not have to run your own indexer to get started. Requests go to `https://mainnet.subfrost.io/v4/jsonrpc` (see the [JSON-RPC Overview](../api-reference/json-rpc/overview) for the full method catalog). If you want to run the indexer yourself, see [Indexing with metashrew](./indexing-with-metashrew).
The full command and SDK reference lives in [API & SDK Docs](../api-reference/cli-sdk/overview). This track is the guided path; that section is the lookup table.
### What is here
- **[Quickstart](./quickstart):** install the toolchain and deploy your first contract.
- **[Interacting via the CLI](./interacting-via-cli):** wallets, balances, executing and querying contracts.
- **[Wrapping frBTC](./wrapping-frbtc):** wrap and unwrap Bitcoin-backed frBTC from the CLI and the SDK.
- **[BRC2.0 Integration](./brc20-integration):** deploying EVM-style contracts on Bitcoin.
- **[Connecting a wallet](./connecting-a-wallet):** the injected provider and remote signing for web apps.
- **[Indexing with metashrew](./indexing-with-metashrew):** run the indexer and query chain state yourself.
### Before you start
Alkanes uses a fresh wallet model. Do not reuse a wallet that holds ordinals or inscriptions for contract work: use a dedicated Alkanes wallet, as covered in [Safety](../using-subfrost/safety). On testnet and regtest this does not matter, but the habit is worth building early.
### Where to go next
- [Quickstart](./quickstart): deploy your first contract.
- [Alkanes Metaprotocol](../protocol/alkanes): the model your contracts run on.
---
## Quickstart
This guide takes you from an empty machine to a deployed Alkanes contract on Bitcoin. It targets **signet** (a public test network) so you are not spending real BTC while you learn.
### Prerequisites
- **Rust 1.70 or later** (install from rustup.rs)
- **wasm-pack** (`cargo install wasm-pack`)
- **Node.js 18 or later** (for the CLI tooling)
### 1. Install the CLI
The CLI is built from source. It is not published to a package registry, so building from the repository is the supported path.
```bash
# Clone alkanes-rs (develop branch)
git clone https://github.com/kungfuflex/alkanes-rs.git -b develop
cd alkanes-rs
# Build the CLI
cargo build --release -p alkanes-cli
# Add it to your PATH (optional)
export PATH="$PWD/target/release:$PATH"
```
See [Installation](../api-reference/cli-sdk/installation) for prerequisites, PATH setup, and troubleshooting in more detail.
### 2. Create a wallet
```bash
# Create a new wallet
alkanes-cli wallet create
# Or restore a wallet from an existing mnemonic phrase
alkanes-cli wallet create ""
# Show your receive address (first taproot address) so you can fund it
alkanes-cli wallet addresses p2tr:0
```
Fund the address from a signet faucet before deploying. See [Wallet Commands](../api-reference/cli-sdk/wallet) for the rest of the wallet surface.
### 3. Write a contract
Create a Rust library project:
```bash
cargo new --lib my-alkane
cd my-alkane
```
Set the crate up to compile to WebAssembly in `Cargo.toml`:
```toml
[package]
name = "my-alkane"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
alkanes-std = { git = "https://github.com/kungfuflex/alkanes-rs" }
alkanes-support = { git = "https://github.com/kungfuflex/alkanes-rs" }
[profile.release]
opt-level = "z"
lto = true
```
Write a minimal contract in `src/lib.rs`:
```rust
use alkanes_std::prelude::*;
use alkanes_support::context::Context;
#[derive(Default)]
pub struct MyAlkane {
counter: u64,
}
impl MyAlkane {
pub fn new() -> Self {
Self { counter: 0 }
}
pub fn increment(&mut self) {
self.counter += 1;
}
pub fn get_count(&self) -> u64 {
self.counter
}
}
// Export the contract
alkane!(MyAlkane);
```
### 4. Build to WebAssembly
```bash
wasm-pack build --target web --release
# Output: pkg/my_alkane_bg.wasm
```
### 5. Deploy
Deploying an Alkane means broadcasting a Bitcoin transaction that carries the WASM as an envelope and a **cellpack** that tells the protocol what to do (see [Alkanes Metaprotocol](../protocol/alkanes) for the model behind this).
```bash
alkanes-cli -p signet alkanes execute "[3,0,0]" \
--envelope "./pkg/my_alkane_bg.wasm" \
--fee-rate 10 \
-y
```
The output shows your contract's **Alkane ID**, a `[block, tx]` pair such as `[2, 1]` (block 2, transaction index 1). That ID is how you address the contract from now on. `execute` takes more flags than shown here, see [Alkanes Commands](../api-reference/cli-sdk/alkanes) for the full set.
### 6. Interact
```bash
# Call increment (opcode 1) on your contract at [2,1]
alkanes-cli -p signet alkanes execute "[2,1,1]" \
--fee-rate 10 \
-y
# Read the counter (opcode 2) by simulating the call, which costs nothing
alkanes-cli -p signet alkanes simulate "2:1:2"
```
Read calls are free and do not touch the chain: they run against indexer state. Write calls are Bitcoin transactions and pay a fee.
### Where to go next
- [Interacting via the CLI](./interacting-via-cli): the full set of wallet and contract commands.
- [Wrapping frBTC](./wrapping-frbtc): put real Bitcoin-backed value into a contract.
- [Alkanes Metaprotocol](../protocol/alkanes): cellpacks, opcodes, and view functions explained.
---
## Reproducible builds
Reproducing an alkane's wasm byte-for-byte means reproducing **everything the compiler baked into it**. The wasm embeds panic paths (`HOME`, the registry src-dir hash, git-checkout hashes, the toolchain triple) and a `producers` section (rustc plus the clang vendor and version). Each of those is an axis you have to pin. This page explains the ones that actually bite.
:::tip You do not pin these by hand
The verifier reverses every axis below out of the on-chain bytecode itself: the embedded paths and the `producers` record are the ground truth. A [verify request](../api-reference/verification/verify-and-attest#verify) of just `{ alkane, repo_url, commit, package }` is enough for most contracts. The sections here explain what the reverser and the builder reconstruct on your behalf, and what to override when a reversal is imperfect.
:::
### The environment axes
| Axis | Why it matters |
|---|---|
| **Toolchain host triple** | wasm32 rust-std is host-independent, but the toolchain's *host triple* leaks into the wasm two ways: (1) toolchain **paths** in panic strings, and (2) the `rustc -vV` **`host:` line**, which rustc folds into every crate's `-C metadata` and `StableCrateId`. The path is reconstructed on Linux by copying the toolchain into a directory named for the origin triple. The `host:` line only matches when the toolchain that runs really has the origin host triple: see the macOS and Windows examples below. |
| **clang vendor and version** | Written verbatim into `producers`. Ubuntu clang 14.0.0 (apt) and Homebrew clang 20.1.7 give different bytes. Homebrew clang is assembled on Linux from content-addressed GHCR bottle blobs (`llvm`, `z3` and `libedit`) plus `patchelf`. |
| **`HOME` and `remap-path-prefix`** | Registry, git-checkout, and toolchain paths all hang off `HOME`. With an empty `remap-path-prefix`, paths are raw and `HOME` must be reconstructed exactly (for example `/home/lee` or `/Users/kevinyao`). |
| **Registry mode** | A committed `Cargo.lock` means real crates.io plus `--locked`. No lock means the **time-machine**: freeze the crates.io index tree at the build date and serve it *as* `index.crates.io` so the registry src-dir hash is canonical. |
| **Git-dependency source-id** | A **bare** git source-id (`?rev=X#` rewritten to `#`, then `--locked`) changes `-C metadata` and therefore monomorphization order, which gives different bytes. Reproduce the exact spelling the origin lockfile used (`bare` or `rev`). |
| **secp256k1 C object** | The one axis that does not fully reconstruct on Linux: a secp256k1 object built by a *foreign host's* clang has a slightly different static footprint, which shifts the memory base a few bytes. Pure-Rust alkanes reach byte-exact; alkanes that link a foreign-host C object reach `verified`. A small shift of the memory base changes many offsets, so the match percentage is lower than the few shifted bytes suggest: on the explorer `4:76` shows a 96.7% match (checked 2026-09-26). Below that line the diff engine grants `verified` only when the code section is byte-identical. |
### The registry time-machine
When a repo commits no `Cargo.lock`, cargo would resolve against *today's* crates.io: different versions, different bytes. Instead the builder:
1. Fetches, at depth 1, the `crates.io-index-archive` commit frozen at the build's date.
2. Serves it over local HTTPS **as** `index.crates.io` (a hosts entry pointing `index.crates.io` at loopback, a self-signed certificate and `CARGO_HTTP_CAINFO`); the CLI's Docker sandbox does the same with `--add-host index.crates.io:127.0.0.1`.
This makes the registry src-dir hash canonical. The hash depends on the cargo version: cargo 1.82 gives `index.crates.io-6f17d22bba15001f`, cargo 1.86 gives `index.crates.io-1949cf8c6b5b557f`.
#### Freezing to the deploy date automatically
The time-machine needs a `freeze_commit`. When none is supplied, the builder resolves it from the **deploy date**, which is the block time of the deploy transaction: it finds the [`rust-lang/crates.io-index-archive`](https://github.com/rust-lang/crates.io-index-archive) snapshot branch that covers that date and takes the last commit at or before it. If the dependency versions recovered from the bytecode show that the lockfile was resolved before deployment, the verifier freezes to that earlier date instead. This is the piece that lifts a build with no recipe and no committed lock to byte fidelity without anyone hand-picking a freeze commit.
### Bare git source-ids
Alkanes commonly depend on `alkanes-rs` (and `metashrew`) by git. The lockfile's *spelling* of that source-id changes the compiled metadata. To match a **bare** origin, resolve the dependency *with* its rev, then rewrite the lockfile source (`?rev=X#` becomes `#`) and build `--locked`. Reproducing the `rev` form is a straight `--locked` build.
### Multiple git dependencies, transitive pins, and mirrored repos
A real contract rarely pins one git dependency. The builder handles the whole graph:
- **Multi-dependency pinning.** It pins *every* git dependency it can identify (a pool commonly pins both `alkanes-rs` **and** `metashrew`), not just a single `alkanes-rs` rev. The reverser derives the exact rev each dependency was checked out at from the bytecode's `.cargo/git/checkouts/-//` paths and pins them by name.
- **Transitive pins through the lockfile.** A dependency pulled in *through* another git dependency (for example `metashrew` reached through `alkanes-rs` under the same URL) would otherwise drift to a moving `HEAD`, producing two revs of one crate and an `E0599` "multiple versions" error. The builder uses cargo's own resolver (`cargo update --precise`) to move the whole graph to the ground-truth rev consistently.
- **Mirrored repos.** When a contract and its dependencies reference the same crate through *different* repo URLs (for example `sandshrewmetaprotocols/metashrew` and `kungfuflex/metashrew`), cargo builds two incompatible copies. The builder can redirect one URL onto the other's exact source spec so both unify to a single source. Unlike the pins, a redirect is not derived from the bytecode automatically.
- **Date-freeze for unpinned dependencies.** Any git dependency still left unpinned (a `git = "url"` with no rev and no committed lock) is frozen to that repo's `HEAD` as of the deploy date, the same date the registry time-machine uses. This is the universal fallback that needs no per-dependency rev.
When the repo commits its own `Cargo.lock`, none of these rewrites run: the committed lock's source-ids are authoritative.
Over the API these map to the `git_pins` field (explicit `url rev [bare|rev]` lines) and the `git_date` field, but both are auto-reversed from the bytecode and the deploy transaction's block time, so you seldom set them. See the [verify request fields](../api-reference/verification/verify-and-attest#request-body).
### secp256k1 to wasm with Ubuntu clang
C dependencies like `secp256k1-sys` used to compile to wasm only under the self-contained Homebrew clang, because Ubuntu's apt clang leaks the host glibc include path (`/usr/include/stdint.h` pulls in a missing `bits/libc-header-start.h`) when targeting `wasm32-unknown-unknown`. The builder now passes `-nostdlibinc`, which drops the system libc includes while keeping clang's own builtin headers; the crate's bundled `wasm/wasm-sysroot` supplies the rest. With that, apt `clang-13`, `clang-14` and `clang-15` can build secp256k1 to wasm, so a **Linux-origin** contract can be matched at its *actual* apt clang version instead of being forced onto Homebrew clang.
### Worked example: `4:797` is reproducible
free-mint commits no lockfile and links no C. With `HOME` (`/home/lee`) and rustc (`1.82.0`) read from the bytecode, it reduces to three pins:
- **Registry**: time-machine, index frozen at `2025-03-19`, served as `index.crates.io` (hash `…6f17d22bba15001f`).
- **clang**: Ubuntu 14.0.0 from apt (`clang-14`). Even though the artifact is pure Rust, the `producers` record still names it.
- **Git source-id**: `alkanes-rs` as a **bare** source-id (`d787cdd1…`).
Result: the rebuilt sha256 `8b51384a…` equals the on-chain sha256, byte-exact, 100%: **reproducible**.
### Worked example: `2:0` is reproducible (macOS)
DIESEL's `alkanes-std-genesis-alkane-upgraded-eoa` was built on an Apple Silicon Mac (rustc `1.86.0`, Homebrew clang `20.1.7`, `HOME=/Users/kevinyao`), with the crates.io index frozen by the time-machine (cargo 1.86 hash `…1949cf8c6b5b557f`) and `metashrew` as a bare git source-id.
On Linux, the reconstruction **copies** the `1.86.0` toolchain to `$RUSTUP_HOME/toolchains/1.86.0-aarch64-apple-darwin` (a symlink is canonicalized away by rustc), sets `HOME=/Users/kevinyao`, clones the repo to the origin build directory, and assembles Homebrew clang 20.1.7 from bottle blobs. That makes the toolchain paths and `producers` byte-exact, but DIESEL links secp256k1, and the C object from a Linux-built Homebrew clang differs from the macOS-built one by about 6 bytes. The workbench fixture records that Linux rebuild as **verified**.
Rebuilt on a native macOS host with the origin's clang, the secp256k1 object matches too, and the rebuilt sha256 equals the on-chain sha256: **reproducible**. The verifier routes builds whose bytecode shows a macOS origin to a native macOS builder when one is available for exactly this reason.
### Worked example: `4:9200` is reproducible (Windows through Wine)
predicates `pair-equality` was built with a **Windows** rustc `1.86.0` (`x86_64-pc-windows-msvc`). The stubborn residual was the `rustc -vV` **`host:` line**, `x86_64-pc-windows-msvc`, which rustc folds into every crate's `-C metadata` and `StableCrateId`. Nothing on Linux reproduces that except a genuine Windows host triple, so the reproduction runs the **Windows toolchain under Wine** on Linux. The target architecture (`x86_64`) is the same, so Wine executes the real `rustc.exe`, and it reproduces the wasm **bit-for-bit**: **reproducible**.
### Worked example: `4:76` (busd) is verified (foreign-host C object)
Some alkanes link secp256k1's C object built by a host that cannot be reproduced on Linux the way the macOS and Windows toolchains are. `4:76` (busd) and the oyl implementations fall here: the Rust code and the `producers` record reconstruct exactly, but the secp256k1 static footprint from the foreign host's clang shifts the memory base by about 6 bytes. That is the entire, understood residual, but it does not mean only 6 bytes differ: a small shift of the memory base changes many offsets, so the match percentage is lower than the byte count suggests. On the explorer `4:76` shows a 96.7% match (checked 2026-09-26), below the 97% line.
The explorer's diff engine gives `verified` to a build whose code section is byte-identical even when the overall match is below 97%, so an ordinary verify request can publish an alkane whose residual is confined to data layout. Results the sandbox cannot produce at all, such as a build proven on a host the verifier does not run, go through the admin-only [attest endpoint](../api-reference/verification/verify-and-attest#attest) instead (`alkanes-cli upload` without `--verify`).
### Next steps
- [Verifying alkanes](./verifying-alkanes): the verdicts and how the explorer verifies.
- [BuildInfo workbench](../api-reference/cli-sdk/build-info): run this formula locally with `alkanes-cli`.
- [Verify and attest API](../api-reference/verification/verify-and-attest): every field you can override.
---
## Verifying alkanes
Every contract on the [SUBFROST explorer](https://explorer.subfrost.io/alkanes) can be traced back to the exact source and build environment that produced its on-chain bytecode, reproduced byte-for-byte in a controlled sandbox. This is verification you can re-run yourself, not a checkmark you have to trust.
Verification answers one question: **does this on-chain contract really come from the source someone claims?** The explorer answers it by rebuilding the source and comparing the result with the bytecode that is actually deployed.
### Better than "verified" on an EVM explorer
On most EVM explorers a "verified" badge means someone pasted Solidity that *compiles to matching bytecode* under one blessed compiler. Alkanes are WebAssembly built from Rust, and the Rust toolchain bakes far more into the artifact: panic paths, the registry source hash, git-checkout hashes, the host triple, and the clang `producers` record. So the explorer verifies by **full reproduction**. Given an alkane ID, it fetches the on-chain bytecode, rebuilds the source in a sandbox that pins every environment axis, and diffs the result.
Each verified alkane shows a **reproduction recipe** panel on its explorer page: the repo, commit, rustc, clang, `HOME`, registry mode, and git source-ids that were needed to reproduce it. That is enough to replay the build on your own machine.
### The verdicts
The explorer's diff engine compares the rebuilt wasm with the on-chain bytecode and records one of four verdicts. The first two earn a badge on the alkane page and publish the source.
| Verdict | Meaning |
|---|---|
| **reproducible** | The rebuilt wasm's sha256 is **byte-exact** to the on-chain bytecode. Nothing is trusted; the match is the proof. |
| **verified** | Not byte-exact, but the code section is byte-identical, or the non-custom sections are at least 97% similar at the byte level (the comparison tolerates shifted blocks). In practice the residual is a few bytes of host-dependent build output, typically the footprint of a secp256k1 C object. The code is provably the same. |
| **partial** | At least 90% matches, but the recipe still has an unpinned axis (for example `HOME` or clang not yet reversed). No badge. |
| **mismatch** | Less than 90% matches: the source does not build to the deployed bytecode. No badge. |
Custom sections (such as `producers` and `name`) are excluded from the percentage. `alkanes-cli`, when you run the same comparison locally, uses stricter thresholds and adds a fifth verdict, `code_match`; see [the verify step](../api-reference/cli-sdk/build-info#4-verify-diff-a-candidate-against-the-target) in the BuildInfo guide.
Why isn't every honest build `reproducible`? Because a wasm embeds things the compiler put there from its *environment*: the build's `HOME`, the crates.io index source-dir hash, git-checkout hashes, the toolchain host triple, and the clang vendor and version in the `producers` section. Reproduce all of them and you get byte-exact; miss one and the bytes shift. C code compiled by a foreign host's clang (secp256k1) is the one axis that does not fully reconstruct on Linux, and it is exactly the gap between `reproducible` and `verified`.
### Real examples
These alkanes are live on mainnet and are used throughout these docs.
| Alkane | What it shows | Verdict |
|---|---|---|
| `4:797`, free-mint | Pure Rust, Linux build, byte-exact | **reproducible** |
| `2:0`, DIESEL (`alkanes-std-genesis-alkane-upgraded-eoa`) | macOS build, byte-exact when rebuilt on a native macOS host | **reproducible** |
| `4:9200`, predicates pair-equality | Windows (`x86_64-pc-windows-msvc`) build reproduced byte-exact under Wine | **reproducible** |
| `4:76`, busd (and the oyl implementations) | Foreign-host secp256k1 C-object residual (memory base shifted by about 6 bytes); 96.7% match on the explorer (checked 2026-09-26) | **verified** |
#### Reproducible versus verified, concretely
A cross-host build can still reach `reproducible`. The toolchain path baked into panic strings is reconstructed by copying the toolchain into a directory named for the origin triple, and the `host:` line that rustc folds into crate metadata is matched by running a toolchain whose host really is the origin's. What does not reconstruct on Linux is a **C object compiled by a foreign host's clang** (secp256k1). That is what leaves a build at `verified`.
- **`4:797` (free-mint)** is pure Rust, built on Linux. Once the registry path, Ubuntu clang 14.0.0, and the bare `alkanes-rs` git source-id were pinned, a Linux box reproduced it **byte-for-byte**, so it is `reproducible`.
- **`2:0` (DIESEL)** was built on an Apple Silicon Mac with rustc 1.86.0 and Homebrew clang 20.1.7, and it links secp256k1. On Linux, copying the toolchain into a `1.86.0-aarch64-apple-darwin` directory (a symlink does not work, because rustc canonicalizes it) and assembling the same Homebrew clang reconstructs everything except the secp256k1 C object. The workbench fixture for that Linux rebuild records a residual of about 6 bytes and the verdict `verified`; no explorer match percentage exists for it, because the explorer rebuilds `2:0` natively. Rebuilt on a native macOS host, it matches **byte-for-byte** and is `reproducible`. This is why the verifier sends builds whose bytecode shows a macOS origin to a native macOS builder when one is available.
- **`4:9200` (predicates pair-equality)** was built with the **Windows** rustc 1.86.0 (`x86_64-pc-windows-msvc`). The last residual was the `rustc -vV` `host:` line, which rustc folds into every crate's `-C metadata` and `StableCrateId`, and which only a genuine Windows host triple produces. Running that Windows toolchain **under Wine** on Linux (same `x86_64` architecture) reproduces it **bit-for-bit**, so it is `reproducible`.
- **`4:76` (busd) and the oyl implementations** link secp256k1 built by a *foreign host's* clang, whose C-object footprint shifts the memory base by about 6 bytes. Code logic and `producers` are exact. A small shift of the memory base changes many offsets, so the match percentage is lower than the byte count suggests: on the explorer `4:76` shows a 96.7% match (checked 2026-09-26). Below that line the diff engine grants `verified` only when the code section is byte-identical; a result proven outside the sandbox can also be recorded as `verified` through attest.
[Reproducible builds](./reproducible-builds) walks through each of these recipes axis by axis.
### How the explorer verifies
1. You (or the contract author) submit a **build recipe** for an alkane ID, either with [`alkanes-cli upload --verify`](../api-reference/cli-sdk/build-info) or by POSTing to the [verify endpoint](../api-reference/verification/verify-and-attest#verify). In practice the recipe is tiny: `{ alkane, repo_url, commit, package }` (plus `subdir` when the crate is not at the repo root) is enough for most alkanes.
2. The verifier fetches the on-chain bytecode (`metashrew_view` `getbytecode`) and queues a rebuild of the source in a sandbox that pins the recipe's axes. The request returns straight away; the build runs as a background job.
3. When the build finishes, the verifier diffs the rebuilt wasm against the bytecode and records the verdict. **A `reproducible` or `verified` result auto-promotes** the source to the alkane's verified-source panel. There is no trusted manual step for these.
4. The alkane page then shows the badge, a browsable source tree (`https://explorer.subfrost.io/alkane/{id}/source`), and the reproduction recipe. The same source is available programmatically through the [source API](../api-reference/verification/source-api).
A promoted result is keyed by the sha256 of the bytecode, so every alkane that shares that bytecode (for example, every clone of a verified template) shows as verified too.
#### The recipe reverses itself
You rarely spell out the environment axes. The verifier reads the on-chain bytecode **once** and reverses the recipe straight out of it. The embedded panic paths carry the exact git-dependency revisions, the build `HOME`, and the toolchain host triple; the `producers` section carries the clang and rustc versions; and the deploy transaction's confirmation time gives the date to freeze the crates.io index at. (When the dependency versions found in the bytecode show that the lockfile was resolved before deployment, the freeze date moves back to match.)
So a submission of just the repo, commit, and package reproduces most contracts with no hand-tuning. Any field you *do* pass simply **overrides** its auto-reversed value. This is why a verify request body is almost always four or five lines.
#### Who can submit
Because the verifier only promotes a source when its own diff confirms the match, the `verify` endpoint is safe for **any** active API key: a bad recipe simply fails to promote. The only trusted write path is [`attest`](../api-reference/verification/verify-and-attest#attest), which needs an admin key issued by the SUBFROST team because it records a result *without* a sandbox rebuild. It exists for results proven outside the verifier's own sandbox, such as builds the Linux verifier cannot reproduce byte-exact on its own.
### Next steps
- [Reproducible builds](./reproducible-builds): the reproduction formula, axis by axis.
- [BuildInfo workbench](../api-reference/cli-sdk/build-info): reverse, rebuild, and diff an alkane locally with `alkanes-cli`, then upload the result.
- [Verify and attest API](../api-reference/verification/verify-and-attest): submit a recipe over HTTP.
- [Source API](../api-reference/verification/source-api): read a verified alkane's source tree and files.
---
## Wrapping frBTC
**frBTC** is Bitcoin-backed value inside Alkanes, held 1:1 against real BTC (see [frBTC Peg & Custody](../protocol/frbtc-peg-and-custody)). This page shows how to wrap and unwrap it programmatically. The frBTC contract lives at the Alkane ID `[32, 0]`.
### How it works
**Wrapping** sends BTC to the signer address and mints an equal amount of frBTC (minus a small premium):
1. You send BTC to the signer's Taproot address.
2. You call **wrap** (opcode 77) on the frBTC contract.
3. The contract verifies the BTC arrived and mints frBTC to you.
**Unwrapping** burns frBTC and asks the signer group to release BTC:
1. You call **unwrap** (opcode 78) with the frBTC you want to redeem.
2. The contract burns it and records a payment for the signer group.
3. The signers release the BTC in a later transaction.
Wrapping is instant. Unwrapping settles once the signer group processes the payment.
### Wrapping from the CLI
The CLI wraps the whole flow in one command:
```bash
# Wrap 1 BTC (100,000,000 sats) to frBTC
alkanes-cli -p regtest \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes wrap-btc \
100000000 \
--to p2tr:0 \
--from p2tr:0 \
--change p2tr:0 \
--mine \
-y
# Check your frBTC balance
alkanes-cli -p regtest \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes getbalance
```
`wrap-btc` builds the transaction that sends BTC to the signer, attaches the wrap call on `[32, 0]`, and returns frBTC to your address. On regtest, `--mine` mines the block immediately. See [Alkanes Commands](../api-reference/cli-sdk/alkanes) for the full `wrap-btc` flag reference.
### Unwrapping from the CLI
Unwrapping is a direct contract call, so you use `execute` with a cellpack:
```bash
# Unwrap 0.5 BTC worth of frBTC (opcode 78)
alkanes-cli -p regtest \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes execute "[32,0,78,0,50000000]:v0:v0" \
--inputs "32:0:50000000" \
--from p2tr:0 \
--fee-rate 1 \
--mine \
-y
```
The cellpack `[32,0,78,0,50000000]` calls opcode 78 on `[32,0]` with a vout index of `0` and an amount of `50000000` sats. The `--inputs "32:0:50000000"` sends that frBTC into the contract to be burned. The BTC itself is released by the signer group afterward, not in this transaction.
### Querying frBTC state
Read-only opcodes are free through `simulate`:
```bash
# Total supply (opcode 105)
alkanes-cli -p regtest alkanes simulate "32:0:105"
# Signer public key (opcode 103)
alkanes-cli -p regtest alkanes simulate "32:0:103"
# Current premium (opcode 104)
alkanes-cli -p regtest alkanes simulate "32:0:104"
```
### The premium
Wrapping charges a small premium, measured in parts per 100,000,000 (that is, sats per BTC). The default is `100000`, which is 0.1%. Wrapping 1 BTC at 0.1% mints 99,900,000 sats of frBTC.
### frBTC opcodes
| Opcode | Name | Description |
|--------|------|-------------|
| 77 | Wrap | Mint frBTC for BTC sent to the signer |
| 78 | Unwrap | Burn frBTC and record a payment |
| 99 | GetName | Returns `frBTC` |
| 100 | GetSymbol | Returns `frBTC` |
| 101 | GetPendingPayments | Pending payment records |
| 103 | GetSigner | Signer public key |
| 104 | GetPremium | Current premium |
| 105 | GetTotalSupply | Total supply |
You do not have to keep this table memorized: any deployed alkane exposes its own names and opcodes through its `__meta` export. See [Reading Alkane Metadata](../api-reference/guides/alkane-metadata) for how to query it directly.
### From TypeScript
The SDK exposes a typed wrap so you do not build the cellpack by hand:
```typescript
import { AlkanesProvider } from '@alkanes/ts-sdk';
const provider = new AlkanesProvider({
network: 'mainnet', // or 'regtest' for local development
rpcUrl: 'https://mainnet.subfrost.io/v4/jsonrpc',
});
await provider.initialize();
// Load a wallet to sign transactions
provider.walletLoadMnemonic('your twelve word mnemonic ...');
const [{ address }] = provider.walletGetAddresses('p2tr', 0, 1);
// Wrap 1 BTC to frBTC
const wrap = await provider.frbtcWrapTyped({
amount: BigInt(100000000),
toAddress: address,
fromAddress: address,
feeRate: 1,
mineEnabled: true, // auto-mine on regtest
autoConfirm: true,
});
console.log('Wrap TXID:', wrap.reveal_txid);
// Read your frBTC balance
const balances = await provider.alkanes.getBalance(address);
const frbtc = balances.find(b => b.alkane_id?.block === 32 && b.alkane_id?.tx === 0);
console.log('frBTC balance:', frbtc?.balance);
```
### Where to go next
- [frBTC Peg & Custody](../protocol/frbtc-peg-and-custody): how the backing is secured.
- [Interacting via the CLI](./interacting-via-cli): the rest of the command surface.
- [CLI & SDK Reference](../api-reference/cli-sdk/overview): full SDK methods.
---
## Alkanes Commands
The `alkanes` namespace provides operations for the Alkanes protocol including contract execution, token queries, AMM swaps, and liquidity pool management.
### Example commands
Get metadata for an alkane token (name, symbol, total supply):
```bash
$ alkanes-cli -p mainnet alkanes reflect-alkane 2:0
```
Get all AMM pools from the factory contract:
```bash
$ alkanes-cli -p mainnet alkanes get-all-pools
```
Get pool details for pools 0-9 using the experimental AssemblyScript WASM path (tx-script):
```bash
$ alkanes-cli -p mainnet alkanes get-all-pools --pool-details --experimental-asm --range 0-9
```
### Commands Overview
- **`execute`**: Execute an alkanes transaction with protostones
- **`simulate`**: Simulate a contract call without broadcasting
- **`trace`**: Trace an alkanes transaction
- **`inspect`**: Inspect a contract (disassemble, fuzz, metadata)
- **`tx-script`**: Execute a tx-script with WASM bytecode
- **`getbalance`**: Get alkane token balances for an address
- **`spendables`**: Get spendable outpoints for an address
- **`reflect-alkane`**: Get metadata for an alkane (name, symbol, supply)
- **`reflect-alkane-range`**: Reflect metadata for a range of alkanes
- **`getbytecode`**: Get the bytecode for an alkane contract
- **`wrap-btc`**: Wrap BTC to frBTC
- **`unwrap`**: Get pending unwraps
- **`get-all-pools`**: Get all AMM pools
- **`all-pools-details`**: Get detailed pool information
- **`pool-details`**: Get details for a specific pool
- **`init-pool`**: Initialize a new liquidity pool
- **`swap`**: Execute a swap on the AMM
- **`sequence`**: Get the sequence for an outpoint
- **`traceblock`**: Trace a block
- **`backtest`**: Backtest a transaction
### alkanes execute
Execute an alkanes transaction with one or more protostones.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes execute \
--to \
--from \
--fee-rate 10 \
-y
```
**Arguments:**
- ``: Protostone specifications
**Options:**
- `--inputs `: Input requirements (format: `B:amount`, `B:amount:vN`, `block:tx:amount`)
- `--to `: Recipient addresses
- `--from `: Source addresses for UTXOs
- `--change `: Change address for BTC
- `--alkanes-change `: Change address for unwanted alkanes
- `--fee-rate `: Fee rate in sat/vB
- `--envelope `: Path to envelope file (for contract deployment)
- `--raw`: Show raw JSON output
- `--trace`: Enable transaction tracing
- `--mine`: Mine a block after broadcasting (regtest only)
- `-y, --auto-confirm`: Auto-confirm the transaction
### alkanes simulate
Simulate an alkanes contract call without broadcasting.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes simulate 2:0:99 \
--inputs "2:1:1000,32:0:5000"
```
**Arguments:**
- ``: Alkane ID with opcode (format: `block:tx:opcode`)
**Options:**
- `--inputs `: Input alkanes as comma-separated triplets (e.g., `2:1:1,2:2:100`)
- `--height `: Block height for simulation
- `--block `: Block hex data (0x prefixed)
- `--transaction `: Transaction hex data (0x prefixed)
- `--envelope `: Path to WASM file to pack into witness
- `--pointer `: Pointer value (default: 0)
- `--txindex `: Transaction index (default: 1)
- `--refund `: Refund pointer (default: 0)
- `--block-tag `: Block tag (e.g., "latest" or height)
- `--raw`: Show raw JSON output
### alkanes trace
Trace an alkanes transaction to see execution details.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes trace :
```
**Arguments:**
- ``: The transaction outpoint to trace
**Options:**
- `--raw`: Show raw JSON output
### alkanes inspect
Inspect an alkanes contract for debugging and analysis.
```bash
# Disassemble bytecode
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes inspect 2:0 --disasm
# Show metadata
alkanes-cli alkanes inspect 2:0 --meta
# Fuzz contract with opcode range
alkanes-cli alkanes inspect 2:0 --fuzz --fuzz-ranges "99-110"
# Get code hash
alkanes-cli alkanes inspect 2:0 --codehash
```
**Arguments:**
- ``: The contract outpoint
**Options:**
- `--disasm`: Disassemble the contract bytecode
- `--fuzz`: Fuzz the contract with a range of opcodes
- `--fuzz-ranges `: Opcode ranges to fuzz
- `--meta`: Show contract metadata
- `--codehash`: Show contract code hash
- `--raw`: Show raw JSON output
### alkanes reflect-alkane
Get metadata for an alkane token by calling standard view opcodes.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes reflect-alkane 2:0
```
**Output:**
```
Alkane Metadata for 2:0
═══════════════════════
Name: DIESEL
Symbol: DIESEL
Total Supply: 5050000000
```
**Arguments:**
- ``: Alkane ID (format: `block:tx`)
**Options:**
- `--concurrency `: Max concurrent RPC calls (default: 30)
- `--raw`: Show raw JSON output
### alkanes getbalance
Get alkane token balances for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes getbalance bc1p...
```
**Arguments:**
- `[ADDRESS]`: The address to query (uses wallet address if not provided)
**Options:**
- `--raw`: Show raw JSON output
### alkanes spendables
Get spendable alkane outpoints for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes spendables bc1p...
```
### alkanes wrap-btc
Wrap BTC to frBTC and lock in vault.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes wrap-btc 100000 \
--to bc1p... \
--fee-rate 10 \
-y
```
**Arguments:**
- ``: Amount of BTC to wrap (in satoshis)
**Options:**
- `--to `: Address to receive frBTC tokens (required)
- `--from `: Addresses to source UTXOs from
- `--change `: Change address
- `--fee-rate `: Fee rate in sat/vB
- `--raw`: Show raw JSON output
- `--trace`: Enable transaction tracing
- `--mine`: Mine a block after broadcasting (regtest only)
- `-y, --auto-confirm`: Auto-confirm the transaction
### alkanes swap
Execute a swap on the AMM.
```bash
# Swap DIESEL for frBTC
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes swap \
--path "2:0,32:0" \
--input 1000000 \
--slippage 5.0 \
--auto-confirm
```
**Options:**
- `--path `: Swap path as comma-separated alkane IDs (e.g., `2:0,32:0` for DIESEL->frBTC)
- `--input `: Input token amount (required)
- `--minimum-output `: Minimum output (overrides slippage)
- `--slippage `: Slippage percentage (default: 5.0%)
- `--expires `: Expiry block height (default: current + 100)
- `--to `: Recipient address (default: p2tr:0)
- `--from `: Sender address (default: p2tr:0)
- `--change `: Change address
- `--fee-rate `: Fee rate in sat/vB
- `--factory `: Factory ID for path optimization (default: 4:65522)
- `--no-optimize`: Skip path optimization
- `--trace`: Show trace after confirmation
- `--mine`: Mine a block after broadcasting (regtest only)
- `--auto-confirm`: Auto-confirm without prompting
### alkanes init-pool
Initialize a new liquidity pool.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
alkanes init-pool \
--pair "2:0,32:0" \
--liquidity "300000000:50000" \
--to p2tr:0 \
--from p2tr:0 \
--auto-confirm
```
**Options:**
- `--pair `: Token pair (format: `BLOCK:TX,BLOCK:TX`)
- `--liquidity `: Initial liquidity (format: `AMOUNT0:AMOUNT1`)
- `--to `: Recipient address identifier (e.g., p2tr:0)
- `--from `: Sender address identifier
- `--change `: Change address
- `--minimum `: Minimum LP tokens to receive
- `--fee-rate `: Fee rate in sat/vB
- `--factory `: Factory ID (default: 4:1)
- `--trace`: Show trace after confirmation
- `--auto-confirm`: Auto-confirm without prompting
### alkanes get-all-pools
Get all pools from an AMM factory contract.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes get-all-pools
```
Uses factory 4:65522 by default.
### alkanes pool-details
Get details for a specific liquidity pool.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes pool-details 2:3
```
### alkanes get-all-pools --experimental-asm --pool-details
Get pool details using the experimental AssemblyScript WASM approach. This uses `tx-script` execution with embedded WASM binaries to batch multiple pool detail queries into efficient RPC calls.
```bash
# Fetch all pools with details (batched)
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes get-all-pools --experimental-asm --pool-details
# Fetch a specific range of pools (single chunk)
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes get-all-pools --experimental-asm --pool-details --range 0-9
```
**How it works:**
1. **Step 1**: Calls `tx-script` with the embedded `get-all-pools` WASM to get the total pool count
2. **Step 2**: Calls `tx-script` with the `get-all-pools-details` WASM to fetch details for a range of pools in a single RPC call
**Response format:**
```json
{
"total": 142,
"start": 0,
"end": 9,
"count": 10,
"pools": [
{
"pool_id_block": 2,
"pool_id_tx": 53199,
"details": {
"token_a_block": 2,
"token_a_tx": 0,
"token_b_block": 2,
"token_b_tx": 16,
"reserve_a": "79785789299",
"reserve_b": "2835905850298579283",
"total_supply": "474859903703873",
"pool_name": "DIESEL / METHANE LP"
}
}
]
}
```
**Options:**
- `--experimental-asm`: Use AssemblyScript WASM for batched queries
- `--pool-details`: Include full pool details (reserves, tokens, LP supply)
- `--range -`: Pool index range to fetch (default: all pools)
- `--chunk-size `: Pools per batch when fetching all (default: 20)
- `--factory `: Factory ID (default: 4:65522)
### alkanes tx-script
Execute a tx-script with WASM bytecode.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes tx-script \
--envelope /path/to/script.wasm \
--inputs "1,2,3"
```
**Options:**
- `--envelope `: Path to WASM file (required)
- `--inputs `: Cellpack inputs as comma-separated u128 values
- `--block-tag `: Block tag (e.g., "latest" or height)
- `--raw`: Show raw JSON output
### alkanes getbytecode
Get the bytecode for an alkane contract.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes getbytecode 2:0
```
### alkanes unwrap
Get pending unwraps from the protocol.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes unwrap
```
### alkanes backtest
Backtest a transaction by simulating it in a block.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes backtest
```
### Next Steps
- [Wallet Commands](./wallet): Wallet management
- [Ord Commands](./ord): Ordinals queries
- [DataAPI Commands](./dataapi): High-level data queries
---
## BRC20-Prog Commands
The `brc20-prog` namespace provides operations for BRC20-Prog, the EVM-compatible smart contract layer on Bitcoin. This enables Ethereum-style contract deployment and interaction.
### Example commands
Get the current block number from the BRC20-Prog EVM:
```bash
$ alkanes-cli -p mainnet brc20-prog block-number
```
Get the chain ID for the BRC20-Prog network:
```bash
$ alkanes-cli -p mainnet brc20-prog chain-id
```
### Commands Overview
**Contract Operations:**
- **`deploy-contract`**: Deploy a contract from Foundry build JSON
- **`transact`**: Call a contract function (state-changing)
- **`call`**: Call a contract function (read-only, eth_call)
- **`wrap-btc`**: Wrap BTC to frBTC and execute in brc20-prog
**Contract Queries:**
- **`get-code`**: Get contract bytecode (eth_getCode)
- **`get-contract-deploys`**: Get contract deployments by address
- **`get-storage-at`**: Get storage at location (eth_getStorageAt)
**Balance & Account:**
- **`get-balance`**: Get frBTC balance (eth_getBalance)
- **`brc20-balance`**: Get BRC20 balance (brc20_balance)
- **`get-transaction-count`**: Get nonce (eth_getTransactionCount)
**Transaction Queries:**
- **`get-transaction`**: Get transaction by hash (eth_getTransactionByHash)
- **`get-transaction-receipt`**: Get transaction receipt (eth_getTransactionReceipt)
- **`get-receipt-by-inscription`**: Get receipt by inscription ID
- **`get-inscription-by-tx`**: Get inscription ID by tx hash
- **`get-inscription-by-contract`**: Get inscription ID by contract address
- **`trace-transaction`**: Get transaction trace (debug_traceTransaction)
**Block Queries:**
- **`block-number`**: Get current block number (eth_blockNumber)
- **`get-block-by-number`**: Get block by number (eth_getBlockByNumber)
- **`get-block-by-hash`**: Get block by hash (eth_getBlockByHash)
**Other:**
- **`estimate-gas`**: Estimate gas (eth_estimateGas)
- **`chain-id`**: Get chain ID (eth_chainId)
- **`gas-price`**: Get gas price (eth_gasPrice)
- **`get-logs`**: Get logs (eth_getLogs)
- **`version`**: Get BRC20-Prog version (brc20_version)
- **`txpool-content`**: Get txpool content (txpool_content)
- **`client-version`**: Get client version (web3_clientVersion)
- **`unwrap`**: Get pending unwraps from FrBTC contract
### Endpoint Configuration
BRC20-Prog uses a separate JSON-RPC endpoint:
```bash
# Using the brc20-prog endpoint
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog block-number
```
Or with your API key:
```bash
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/brc20-prog
```
### brc20-prog deploy-contract
Deploy a Solidity contract from Foundry build output.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog deploy-contract ./out/MyContract.sol/MyContract.json \
--fee-rate 10 \
-y
```
**Arguments:**
- ``: Path to Foundry build JSON file
**Options:**
- `--from `: Addresses to source UTXOs from
- `--change `: Change address
- `--fee-rate `: Fee rate in sat/vB
- `--raw`: Show raw JSON output
- `--trace`: Enable transaction tracing
- `--mine`: Mine a block after broadcasting (regtest only)
- `-y, --auto-confirm`: Auto-confirm the transaction
### brc20-prog transact
Call a state-changing function on a BRC20-prog contract.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog transact \
--address 0x1234...abcd \
--signature "transfer(address,uint256)" \
--calldata "0xRecipient...,1000" \
--fee-rate 10 \
-y
```
**Options:**
- `--address `: Contract address (0x prefixed hex)
- `--signature `: Function signature (e.g., "transfer(address,uint256)")
- `--calldata `: Arguments as comma-separated values
- `--from `: Addresses to source UTXOs from
- `--change `: Change address
- `--fee-rate `: Fee rate in sat/vB
- `--raw`: Show raw JSON output
- `--trace`: Enable transaction tracing
- `--mine`: Mine a block after broadcasting (regtest only)
- `-y, --auto-confirm`: Auto-confirm the transaction
### brc20-prog wrap-btc
Wrap BTC to frBTC and execute a function on a target contract.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
brc20-prog wrap-btc 100000 \
--target 0x1234...abcd \
--signature "deposit()" \
--calldata "" \
--fee-rate 10 \
-y
```
**Arguments:**
- ``: Amount of BTC to wrap (in satoshis)
**Options:**
- `--target `: Target contract address
- `--signature `: Function signature to call
- `--calldata `: Calldata arguments
- `--from `: Addresses to source UTXOs from
- `--change `: Change address
- `--fee-rate `: Fee rate in sat/vB
- `--raw`: Show raw JSON output
- `--trace`: Enable transaction tracing
- `--mine`: Mine a block after broadcasting (regtest only)
- `-y, --auto-confirm`: Auto-confirm the transaction
### brc20-prog call
Make a read-only call to a contract (eth_call equivalent).
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog call \
--to 0x1234...abcd \
--data 0x70a08231000000000000000000000000...
```
### brc20-prog get-balance
Get the frBTC balance of an address (eth_getBalance equivalent).
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog get-balance 0x1234...abcd
```
### brc20-prog get-code
Get the bytecode of a deployed contract.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog get-code 0x1234...abcd
```
### brc20-prog block-number
Get the current block number.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog block-number
```
### brc20-prog get-transaction
Get transaction details by hash.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog get-transaction 0xabc123...
```
### brc20-prog get-transaction-receipt
Get transaction receipt with logs and status.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog get-transaction-receipt 0xabc123...
```
### brc20-prog estimate-gas
Estimate gas for a transaction.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog estimate-gas \
--to 0x1234...abcd \
--data 0x...
```
### brc20-prog chain-id
Get the chain ID.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog chain-id
```
### brc20-prog get-logs
Get event logs matching filter criteria.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog get-logs \
--address 0x1234...abcd \
--from-block 840000 \
--to-block latest
```
### brc20-prog trace-transaction
Get detailed execution trace for a transaction.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog trace-transaction 0xabc123...
```
### brc20-prog unwrap
Get pending unwraps from the BRC20-Prog FrBTC contract.
```bash
alkanes-cli -p mainnet \
--brc20-prog-rpc-url https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog \
brc20-prog unwrap
```
### JSON-RPC Methods
BRC20-Prog supports standard Ethereum JSON-RPC methods:
- `eth_blockNumber`
- `eth_getBalance`
- `eth_getCode`
- `eth_call`
- `eth_estimateGas`
- `eth_getTransactionByHash`
- `eth_getTransactionReceipt`
- `eth_getBlockByNumber`
- `eth_getBlockByHash`
- `eth_getTransactionCount`
- `eth_getStorageAt`
- `eth_getLogs`
- `eth_chainId`
- `eth_gasPrice`
Plus BRC20-Prog specific methods:
- `brc20_version`
- `brc20_balance`
- `brc20_getTxReceiptByInscriptionId`
- `brc20_getInscriptionIdByTxHash`
- `brc20_getInscriptionIdByContractAddress`
### Next Steps
- [DataAPI Commands](./dataapi): High-level data queries
- [Alkanes Commands](./alkanes): Alkanes protocol operations
- [JSON-RPC brc20_\* Methods](../json-rpc/brc20-prog): Full RPC reference
---
## BTC/USD Commands
The `btcusd` namespace trades BTC against USD on SUBFROST: the frUSD/frBTC CryptoSwap pool on alkanes, plus the two EVM bridge arms (USDC/USDT in, USDC/ETH out). Use it to read market data, quote a swap, prepare a bridge deposit or a burn, and read what is pending in the mempool.
:::danger There is no testnet for this
This pool and this vault exist on mainnet only. Every failure mode flagged below is one where the system succeeds while doing the wrong thing: no error is raised on either chain, and the money is gone. Read the warnings as operational instructions, not as style.
:::
### The read rule
Read chain state with `metashrew_view`, never with an `alkanes_*` JSON-RPC method. The `alkanes_*` wrappers are a different code path from the one production reads, and they diverge. The CLI already speaks the correct surface everywhere. Also: use the `/v4` endpoints for Bitcoin data, not mempool.space.
### Endpoints
Every `btcusd` subcommand routes to one of these. `{apikey}` is your SUBFROST key, passed as `--subfrost-api-key` or `SUBFROST_API_KEY`.
| Endpoint | Serves |
| --- | --- |
| `/v4/{apikey}` | Main JSON-RPC: `metashrew_view`, `metashrew_height`, `esplora_*`, `btc_*` |
| `/v4/{apikey}/btcusd` | Dedicated BTCUSD index, protobuf in both directions (package `alspo.cryptoswap`) |
| `/v4/{apikey}/espo` | Espo-shaped `.` routes, for example `ammdata.get_btc_usd_price` |
| `/v4/{apikey}/mempool` | Mempool JSON-RPC and websocket change stream |
| `/v4/{apikey}/ethereum-builder` | Private submission for the Ethereum leg, keeping the deposit out of the public mempool |
:::warning With no key you fall back, loudly
Without a key the CLI uses the shared `/v4/jsonrpc` endpoint and prints a warning on every call. That endpoint is rate limited and shared with everyone: requests are throttled rather than failed, which is the right shape for this work, because a half-executed trade sequence is the worst available outcome. But an unattended agent that never surfaces the fallback looks like it is running normally right up until it is throttled mid-trade. Get a key at [api.subfrost.io](https://api.subfrost.io).
:::
### Command surface
What you can use today. Commands that are not built yet are listed separately at the end, under "Not built yet": they exist in `--help` and refuse on purpose rather than emitting a transaction that looks right and is not.
| Command | Status | What it does |
| --- | --- | --- |
| `btcusd price` | reads | Executed, marginal and oracle USD/BTC from the dedicated index |
| `btcusd pool` | reads | Reserves, LP supply, virtual price, marginal price, bridge conversion cap |
| `btcusd candles` | raw | OHLC candles. Buckets 3600 and 86400 only. Prints the protobuf answer as hex today |
| `btcusd quote` | reads | Expected output, effective execution price, and the size as bps of the incoming leg |
| `btcusd signers` | raw | The frBTC/frUSD signing group. Prints the protobuf answer as hex today |
| `btcusd mempool` | reads | `info`, `template` and `entry`. The `watch` subcommand refuses: the websocket stream needs its reconnect state machine |
| `btcusd deposit` | prepares | Prints the approve and `depositAndBridge` calls for USDC/USDT in. Local signing is not built |
| `btcusd burn` | prepares | Builds the `burnData` payload for frUSD out to USDC/ETH. Broadcast is not built |
### The pool
Identities and decimals. These are safe to hardcode.
| | |
| --- | --- |
| Address callers use | `4:1778`, an `Upgradeable` **proxy**, and also the frBTCUSD LP token |
| Implementation | `4:1786`, the CryptoSwap (Curve V2) math |
| token0 | frUSD `4:1776` (itself a proxy), 8 decimals |
| token1 | frBTC `32:0`, 8 decimals |
| LP token | `4:1778`, 18 decimals |
#### Curve parameters, as deployed
These are not invariants. `A` and `gamma` can ramp, fees are init parameters an admin can re-commit, and the whole thing sits behind an upgradeable proxy. Read them rather than hardcoding them, and re-read whenever `pool` reports the curve as ramping.
| | |
| --- | --- |
| A | 400000 |
| gamma | 1.45e14 |
| Precision | 1e10 on both legs |
| Fees | 0.2% mid, 0.8% out, depending on how unbalanced the trade leaves the pool |
| admin_fee | 50%, so a material share of the fee accrues to the protocol rather than to liquidity providers |
#### Opcodes
The implementation opcodes this page relies on, as deployed. The implementation may expose more than these. The proxy keeps its own in a high range so they cannot collide: `initialize = 32767`, `initialize_with_auth = 32764`, `upgrade = 32766`, `forward = 36863`. Everything else falls through to the implementation by delegatecall.
```
0 init_pool 100 get_virtual_price
1 add_liquidity 101 get_balances
2 remove_liquidity 102 get_A
3 remove_liquidity_one_coin 103 get_gamma
5 exchange 104 price_oracle
105 price_scale
106 lp_price
107 get_dy
108 calc_token_amount
```
### How this pool gets read wrong
#### Opcode 102 is `get_A`, not `get_decimals`
`4:1778` does not implement the token ABI. Opcode 99 (`get_name`) reverts, opcode 100 returns `virtual_price` as a 1e18 number rather than a symbol, and opcode 102 returns 400000, the amplification coefficient `A`. Anything that treats 102 as `get_decimals` renders every LP balance at 1e400000. Identify a CryptoSwap pool by `lp_price` (106) answering, which reverts on ordinary tokens.
#### LP is 18 decimals, and the supply is past u64
18 is a property of the math, not a declaration: LP is minted in units of the invariant D, which lives in 1e18-normalised space. Amounts come back as decimal **strings** and must stay strings. A u64 or double parse silently corrupts them. This is not hypothetical: a u64 parse is what froze 23.0137 LP as unspendable.
#### Prices are token1 per token0, so USD/BTC is the reciprocal
The index quotes frBTC per frUSD. USD/BTC is `price_q_scale / price_q`. Getting it backwards yields a number near zero, which reads as "no price" rather than as an error. The CLI's `price` and `pool` subcommands already decode this for you.
#### `indexheight` is a tautology, not a liveness signal
`/index_height` is rewritten every block with that block's height, so a view read at height H always returns H, including for a block that does not exist. Use `metashrew_height` to tell fresh from stalled.
Note also that the height stamped on a pool read is the last block that **changed** the pool, so it legitimately trails the chain tip when nobody has traded. The discriminator: call `metashrew_height` on the `/btcusd` endpoint itself. If that is at the tip while the pool stamp trails, the index is fine and nothing has traded.
#### `getbytecode` takes a wrapper, and answers empty rather than erroring
It expects a `BytecodeRequest` that **wraps** an `AlkaneId`. Passing a bare `AlkaneId` returns an empty result, not an error, which reads as "this contract has no code" rather than as a malformed request.
### Measured state
Read off mainnet on 2026-08-15. Pool state height 962,472; chain tip at the time of reading 962,510. **Everything in this section moves.** Re-run the commands rather than trusting the numbers.
```bash
export SUBFROST_API_KEY=your_key
alkanes-cli btcusd pool
alkanes-cli btcusd price
```
| | |
| --- | --- |
| frUSD reserve | 1881214389078 base units (18,812.14389078) |
| frBTC reserve | 29521902 base units (0.29521902) |
| LP supply | 74516779814608897626 (74.516779814608897626, 18 decimals) |
| Virtual price | 1.000089524433596945 |
| Marginal price | 64,083.94 USD/BTC |
| Bridge conversion cap | 188121438907 base units (1,881.21 frUSD), 1000bps of the frUSD reserve |
| Market reference | 63,572.03 USD/BTC, so the pool's marginal price sat about 0.8% above market |
The market reference comes from the espo index, which answers a scaled decimal string. At the reading above it answered `"635720265865300000000"`, which is 63,572.03 USD/BTC:
```bash
curl -s https://mainnet.subfrost.io/v4/$SUBFROST_API_KEY/espo \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"ammdata.get_btc_usd_price","params":[{}]}'
```
### What size actually costs
The marginal price is not the price you get. This pool is shallow, so price impact is material and grows fast with size: in the ladders below, buying is already 0.9% off the marginal price at $500 and 8.8% off at $5,000, and selling 0.03 BTC clears about 8.5% below market. Whether a small trade starts out slightly for or against you depends on which side of the market the pool's marginal price is sitting on that day, and it moves; it was about 0.8% above market at this reading and has been below it since. That side is the one thing here you must re-measure rather than read: the ladders show impact, which is a property of the curve, but the starting point is not. Quote at the size you intend to trade, not at a smaller one.
**Buying frBTC with frUSD**
| Size in | bps of incoming leg | frBTC out (base) | Effective USD/BTC |
| --- | --- | --- | --- |
| $100 | 53 | 155712 | 64,221 |
| $500 | 265 | 769370 | 64,988 |
| $1,000 | 531 | 1500719 | 66,635 |
| $1,881 | 999 | 2698741 | 69,707 |
| $5,000 | 2657 | 6211872 | 80,491 |
**Selling frBTC for frUSD**
| Size in | bps of incoming leg | frUSD out (base) | Effective USD/BTC |
| --- | --- | --- | --- |
| 0.001 BTC | 33 | 6385565367 | 63,856 |
| 0.03 BTC | 1016 | 174596643829 | 58,199 |
:::warning The 1000bps cap is a bridge rule, not a swap rule
`MAX_POOL_SHARE_BPS = 1000` governs **bridge conversions**: when a deposit asks to convert more than 10% of the frUSD reserve into BTC, the coordinator refuses the swap and pays out plain frUSD instead. The deposit still succeeds and neither chain raises an error, so a caller who does not check gets a stablecoin where they asked for BTC. The CLI clamps `convert_bps` down to the cap and tells you when it does, so a deposit prepared here does not hit that refusal blind, but anything building the calldata itself has to check.
A **direct** swap on the pool has no such cap: it executes at whatever the curve gives. That is why `quote` reports your size as bps of the incoming leg and calls it price impact rather than a limit. Set your own `min_dy`.
:::
### Reading market data
Amounts are in the source token's **base units**, as decimal strings. Both frUSD and frBTC are 8 decimals here, so $100 is `10000000000` and 0.001 BTC is `100000`.
```bash
# $100 of frUSD into frBTC
alkanes-cli btcusd quote --from frusd --amount 10000000000
size: 53bps of the pool's incoming leg. That is PRICE IMPACT, not a limit.
in : 10000000000 frUSD (base units)
out: 155712 frBTC (base units)
implied: 64221 USD/BTC (EFFECTIVE, not the pool's marginal price)
# 0.001 BTC the other way
alkanes-cli btcusd quote --from frbtc --amount 100000
```
`pool` also reports whether the curve is ramping and whether the pool is killed. Check both before quoting: a ramping curve moves between blocks, so a quote taken now may not hold, and `is_killed` means refuse to trade at all.
```bash
alkanes-cli btcusd pool
pool 4:1778 at height 962472
frUSD reserve 1881214389078 (18812.14389078)
frBTC reserve 29521902 (0.29521902)
LP supply 74516779814608897626 (74.516779814608897626, 18 decimals)
virtual price 1000089524433596945 (1.000089524433596945)
marginal 15604533105145 (64083.94 USD/BTC)
depth cap 188121438907 (1881.21438907 frUSD - 1000bps of the reserve)
# OHLC. Buckets 3600 and 86400 only; the CLI refuses any other value
# before it sends anything.
alkanes-cli btcusd candles --bucket 86400 --limit 30
```
### Arm A: direct swap, frUSD and frBTC
Both assets are already on alkanes, so this is a single call to `exchange` (opcode 5) on `4:1778`. The CLI can quote it today but cannot send it: `swap`, `add-liquidity` and `remove-liquidity` all refuse, because the Bitcoin transaction construction they need (UTXO selection, protostone assembly, signing) is not yet reused from the `frbtc-wrap` path in the same crate. Until it is, execute swaps through the SUBFROST app and use the CLI to quote and to verify.
1. Quote with `get_dy` (107). Do not compute the curve yourself.
2. Read the size as bps of the incoming leg and decide if you accept that impact.
3. Set a slippage bound. The pool's fee band is 0.2% to 0.8% depending on the resulting balance, so a quote taken at one balance and executed at another must tolerate the difference.
4. Build the protostone, sign, broadcast.
### Arm B: EVM to BTC (deposit)
The caller holds USDC or USDT on Ethereum and wants frUSD on Bitcoin, optionally converting a share to native BTC on arrival. The vault is `0x95779e7e1c943042255b8a78273fe6de4823cf06` (ERC1967 proxy, L1), with `assets(0) = USDC` and `assets(1) = USDT`, both 6 decimals. It takes two calls, in order, and the approve must confirm first.
```bash
alkanes-cli btcusd deposit \
--stable usdc \
--amount 500000000 \
--recipient bc1q... \
--convert-bps 5000 \
--btc-destination bc1q...
deposit 500000000 base units of USDC (assetId 0)
recipient (frUSD) bc1q...
convert 5000bps to BTC -> bc1q...
max slippage 0bps (honoured downwards only)
approve exactly 500000000 - NOT an unlimited allowance
bridgeData 0x1216...
1) approve - send to the TOKEN, not the vault
to 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
data 0x095ea7b3...
2) depositAndBridge - send to the VAULT
to 0x95779e7e1c943042255b8a78273fe6de4823cf06
data 0xbedb65ee...
No Ethereum key supplied. The two calls above are what you sign, in that
order, and the approve must confirm first.
```
With no key flags the CLI prints the two calls for the user's own wallet, which is the right default for an interactive user. Local signing is deliberately not built: the key flags exist but refuse rather than sign. When it lands, it will submit through `/v4/{apikey}/ethereum-builder` to keep the transaction out of the public mempool, which matters on this specific trade because the calldata carries the BTC destination and the conversion split, and the Bitcoin leg's `min_dy` sits in an `OP_RETURN` until it confirms. A public broadcast hands a searcher advance notice of a swap against a shallow pool.
#### bridgeData
`bridgeData` is a protobuf (`frusd.bridge.BridgeData`). Dispatch is by **field presence**, not by the `action` value.
| Field | Name | Value |
| --- | --- | --- |
| 1 | `action` | 0 (`DIRECT_TRANSFER`), always |
| 2 | `recipient_script` | The BTC scriptPubKey the frUSD settles at |
| 3 | `recipient_protostone` | Optional. **Mutually exclusive** with field 4 |
| 4 | `btc_conversion` | Optional. `convert_bps` (0 to 10000), `btc_destination` (required when `convert_bps > 0`), `max_slippage_bps` (honoured downwards only) |
#### Every failure on this path is silent
**A malformed `bridgeData` still succeeds on Ethereum.** The vault stores the bytes verbatim, and the coordinator then pays the operator's fallback recipient instead of the depositor. Validate before signing, because nothing downstream will.
**`btc_destination` is the most dangerous field in the system.** frBTC's `burn()` validates the unwrap pointer's index but never the script at it. It copies the script into the payment record and burns the frBTC immediately: no escrow, no expiry, no reclaim. A non-standard destination destroys supply against an address that can never be paid, and the contract will not stop it. Accept only P2PKH, P2SH, or witness v0 to v16, with v0 restricted to the 20 and 32 byte programs. Refuse anything else, do not clean it up.
**The destination may be someone else's address, and that is supported.** Paying a third party straight out of an EVM balance, or seeding a fresh BTC identity, is a real flow. Both `--recipient` and `--btc-destination` are validated for payability, never for ownership, because ownership is not knowable. Note also that the two can differ: `--recipient` takes the frUSD residue and `--btc-destination` takes the converted BTC, so one deposit can seed two identities in two assets. There is no recovery from a wrong but payable address.
**`convert_bps` out of range is refused, never clamped.** Somebody who wrote 20000 meant something, and it was not 100%.
**Approve exactly the deposit amount.** Not `uint256::MAX`. A standing infinite allowance on a proxy the user does not control outlives the single transaction it was granted for.
**Use addresses at the user boundary, and require mainnet.** A person can recognise `bc1p...` and cannot proofread 34 bytes of script hex. A testnet address parses fine and yields a well-formed script that mainnet pays into a hole.
#### Loading an Ethereum key
Never ask anyone to paste a private key into a chat. In order of preference: hand off to their wallet with the calldata above, unlock a keystore file locally, or use an environment variable for unattended runs while saying clearly that the key is in the process environment. The `--eth-private-key` flag is refused unless you also pass `--i-know-this-lands-in-shell-history`, because it does.
### Arm C: BTC to EVM (burn)
`burn` goes the other way: frUSD on Bitcoin out to USDC on Ethereum, optionally swapping a share onward into ETH through Uniswap V2. Splitting matters more than it looks: a burner can land most of it as a stablecoin and take enough ETH to pay gas, otherwise a fresh EVM identity receives tokens it cannot move.
```bash
alkanes-cli btcusd burn \
--amount 10000000000 \
--eth-address 0x... \
--to-eth-bps 2000 \
--min-out \
--dry-run
burn 10000000000 frUSD -> 2000bps to ETH via Uniswap V2, rest USDC, to 0x...
min out (YOURS - the coordinator has none here)
burnData 0x0101...
```
#### burnData is NOT protobuf
It is a raw byte encoding:
| Encoding | Means |
| --- | --- |
| empty, or `[0x01, 0x00]` | Plain transfer |
| `[0x01, 0x01, <20-byte target>, ]` | Call |
**`amountOutMin` is yours to set.** There is no coordinator-side slippage floor on this leg. Passing 0 is an unbounded-loss instruction. The CLI requires `--min-out` whenever `--to-eth-bps > 0` for exactly this reason.
**One allowlisted target only.** Uniswap V2 Router02 at `0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D`, calldata `swapExactTokensForETH(amountIn, amountOutMin, path, to, deadline)` with `path = [USDC, WETH]`. Anything else degrades to a plain payout. V2 and not V3 deliberately: one `target.call`, no multicall.
**The route is live; this command does not broadcast it yet.** A burn has settled end to end on mainnet, so `BurnAndBridge` itself works: reference transaction `8aa2d9412897dcb25fd91155a910848e8481d72318cc5724dfaac20d58cc576e`, block 961,294, which burned frUSD and paid USDT out on Ethereum. What this command produces is the `burnData` payload, which is authoritative, matched to the coordinator's own parser and unit tested. The frUSD `BurnAndBridge` cellpack packing around that payload still needs checking against the integration tests before anything is signed, so the CLI prints and refuses. Guessing that packing is exactly how a burn silently degrades to a plain payout.
### Watching both chains
A bridge trade is not done when the Bitcoin side settles. Track the frBTC/frUSD signing group's taproot addresses to see rollups land, and check the Ethereum side too. Persist what you submitted (txids, intents, expected outcomes) so a resumed session reconciles rather than re-submits: duplicate submission on a bridge path is a real loss, not an inconvenience.
What the CLI gives you today is the mempool read surface plus `signers`. Address polling is not wired yet: `watch`, `simulate` and `simulate-block` all refuse, and so does `mempool watch`. Poll `mempool template` in the meantime.
```bash
# what is pending, and the blocks a miner would build next
alkanes-cli btcusd mempool info
alkanes-cli btcusd mempool template
# one transaction, including the block it is projected into
alkanes-cli btcusd mempool entry
# the signing group, to see rollups land
alkanes-cli btcusd signers
```
:::warning `protorunesbyaddress` is slow
`protorunesbyaddress`, which address watching will use once it is wired, is slow and marginal: measured at about 10.5s against a 15s pool timeout on a busy address, and it has already caused an outage. Budget for it, cache it, and do not guess a poll interval. This is why the command refuses rather than shipping a default that takes the indexer down.
:::
:::note The mempool routes are live now
The CLI's own help text still says they are not deployed, which was true when it was written and is not true now: `/v4/{apikey}/mempool` answered 200 with a full snapshot, while the same request against a nonexistent path on the same key answered 404, so the route is real and not a fall-through. Verify before building on it rather than trusting either statement.
:::
`simulate-block` will be the MEV lens once its candidate-block assembly is built. The view itself already exists on the node: it drives every transaction through the same per-tx path the indexer uses, with one shared sandbox carrying writes across transaction boundaries, so it reproduces the intra-block atomicity that decides who wins a contended swap. The questions it will answer: does my transaction still get the quote I expect if it lands after the pending set, and what fee gets me ahead of the trade that would move the price against me.
### Before you execute anything
1. For a bridge conversion: is it inside the 1000bps cap? If not, say what will actually happen (frUSD, not BTC).
2. For a direct swap: have you quoted the **effective** execution price at your size, not the pool's marginal price?
3. Is `btc_destination` a standard, payable, mainnet script?
4. Is the approve exactly the amount, not unlimited?
5. Are amounts still strings, not floats?
6. Did you say out loud that you are on the shared `/v4/jsonrpc` endpoint, if you are?
7. Did you say clearly that you are not signing the Ethereum side, because they are?
If you cannot answer all seven, do not broadcast. Explain what is missing.
### Not built yet
Listed so nobody builds a workflow on top of a command that refuses.
- `swap`, `add-liquidity` and `remove-liquidity`: need Bitcoin transaction construction, and refuse rather than emit something that looks right.
- `watch` and `simulate`: need the monitor plumbing. Both refuse.
- `simulate-block`: the node-side view exists, assembling the candidate block to feed it does not. It refuses.
- `mempool watch`: the websocket stream needs its reconnect state machine, because a changed `instance` invalidates every sequence number. It refuses. `mempool info`, `template` and `entry` all work.
- `deposit` local signing: the key flags refuse. The supported path is printing the two calls and signing in your own wallet, which is what the web app does.
- `burn` broadcast: the payload is built and printed, the cellpack packing around it is unverified. The on-chain route itself is live.
- `candles` and `signers`: they answer, but print the raw protobuf hex instead of a decoded table.
### Next steps
- [BTCUSD Pool](../../using-subfrost/btcusd-pool): the same pool as a product page, for anyone you are handing this to.
- [Pools & Liquidity](../../using-subfrost/pools-liquidity): what providing liquidity to this pool pays.
- [frUSD Overview](../../tokens/frUSD-overview): the asset on the USD side of the pair.
- [Alkanes Commands](./alkanes): contract execution, traces, and the general AMM surface.
---
## BuildInfo Workbench
`alkanes-cli` has a **BuildInfo workbench**: subcommands that reverse an alkane's build environment from its bytecode, reconstruct the wasm in a controlled sandbox, diff the two, and `upload` the result to the [SUBFROST explorer](https://explorer.subfrost.io/alkanes). The output is a single self-contained `BuildInfo` JSON (schema `1.0.0`) that fully specifies how to reproduce one alkane's on-chain wasm.
For what verification means and how the explorer runs it, see [Verifying alkanes](../../build/verifying-alkanes). For the reproduction formula itself, see [Reproducible builds](../../build/reproducible-builds).
### Availability
The workbench lives on the **`develop` branch** of [`kungfuflex/alkanes-rs`](https://github.com/kungfuflex/alkanes-rs/tree/develop/crates/alkanes-cli-common/src/buildinfo) and is not in any release yet, so build `alkanes-cli` from source as described in [Installation](./installation) (which already clones `develop`). The `upload` command is compiled into the `alkanes-cli` binary by default; no extra Cargo features are needed.
What each step needs:
- `build-info build` and the full `build-info` pipeline run the build in a **Docker** container, so Docker must be installed and running.
- `build-info reverse` and `build-info verify` need no Docker. They work offline on a local `.wasm`; they only need `--jsonrpc-url` when the target is an on-chain alkane ID.
- `upload` only POSTs the JSON to the explorer over HTTPS.
### Subcommands
`--at` accepts **either** an alkane ID (`block:tx`, fetched with `metashrew_view` `getbytecode` from `--jsonrpc-url`) **or** a local `.wasm` path. Offline is the default: `--jsonrpc-url` is not filled in from `-p`, so pass it whenever `--at` is an alkane ID. Both are global options and go before the subcommand. If the endpoint needs an auth header, add it with `--jsonrpc-header "Name: Value"`.
| Command | What it does |
|---|---|
| `build-info reverse --at ` | Reverse the build environment (rustc, clang, `HOME`, host triple, whether it links C) from a target wasm. |
| `build-info build ` | Rebuild a wasm from a BuildInfo JSON in a Docker sandbox. |
| `build-info verify --at ` | Diff a candidate wasm against the target and print a verdict. |
| `build-info [src-dir] --at ` | Full pipeline: reverse, reconstruct from source, diff, and emit a populated BuildInfo JSON. |
| `upload --api-key ` | Submit a BuildInfo to the explorer (attest by default, or `--verify`). |
The end-to-end flow is **`build-info` then `upload`**: produce the recipe, then submit it.
#### 1. reverse: read the build environment out of the bytecode
```bash
# From a local .wasm (fully offline):
alkanes-cli build-info reverse --at ./free_mint.wasm --emit reversed.json
# From an on-chain ID (fetches the bytecode over JSON-RPC):
alkanes-cli -p mainnet --jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
build-info reverse --at 4:797
```
The readout on stderr summarizes what the wasm reveals: `rustc`, `clang` vendor and version, `home`, `host_triple`, and `links_c`. Without `--emit`, the full reversed JSON goes to stdout.
#### 2. build-info: the full pipeline
Point it at a git checkout of the alkane's source and at the target. It reverses, rebuilds in the sandbox, diffs, and emits the complete BuildInfo:
```bash
alkanes-cli -p mainnet --jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
build-info ./free-mint --at 4:797 \
--repo https://github.com/kungfuflex/free-mint \
--package free-mint \
--emit build-info.json
```
Flags: `--repo`, `--commit`, `--package` (the Cargo `-p` package), `--features` (comma-separated, for example `mainnet`), `--emit` (write to a file instead of stdout). The `-p` global option is recorded as the BuildInfo's `identity.network` and defaults to `regtest`, so pass `-p mainnet` for mainnet alkanes.
If the source directory has a committed `Cargo.lock`, the BuildInfo switches to `committed-lock` registry mode and embeds the lockfile. If the build cannot run (for example, Docker is missing), the JSON is still emitted, with a `build not run` note and no `result`.
#### 3. build: reconstruct a wasm from a recipe
```bash
alkanes-cli build-info build build-info.json --out built.wasm
# built built.wasm (297481 bytes, sha256=8b51384a…)
```
Without `--out`, the wasm is written to `./built.wasm`.
#### 4. verify: diff a candidate against the target
```bash
alkanes-cli -p mainnet --jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
build-info verify --at 4:797 built.wasm
```
For `4:797` this reports byte-exact, verdict `reproducible`. The local comparison scores each non-custom wasm section as matched or not matched, and uses these verdicts:
| Verdict | Rule |
|---|---|
| `reproducible` | Byte-exact. |
| `verified` | Code section identical, and the byte-identical sections account for at least 99.5% of the non-custom section bytes. |
| `code_match` | Code section identical, but the identical sections account for less than 99.5%. |
| `partial` | The byte-identical sections account for at least 98% of the non-custom section bytes. |
| `mismatch` | Anything else. |
The explorer's own diff engine scores byte-level similarity within each section and uses lower thresholds (see [the verdicts](../../build/verifying-alkanes#the-verdicts)), so a cross-host, C-linking build such as `4:76` (busd) can score lower here than it does on the explorer.
#### 5. upload: submit a BuildInfo to the explorer
`upload` POSTs a request derived from the BuildInfo JSON to `/api/v1//attest` or `/api/v1//verify`, using a built-in HTTP/2 client (no curl needed):
```bash
# Default is attest: record the BuildInfo's verdict directly.
# Needs an admin key issued by the SUBFROST team.
alkanes-cli upload build-info.json --api-key $ADMIN_KEY
# --verify: send the recipe to /verify so the server rebuilds and diffs it.
# Any active API key works.
alkanes-cli upload build-info.json --api-key $SUBFROST_API_KEY --verify
```
Flags: `--api-key ` (required), `--explorer-url ` (defaults to `https://explorer.subfrost.io`; must be `https`), `--verify` (switch from attest to a sandbox rebuild).
- **attest** (default) writes the verdict without a server-side rebuild, so it needs an **admin key** issued by the SUBFROST team. The request carries the BuildInfo's `target_sha256`, alkane ID, source provenance, `result.verdict` and `result.normalized_match_pct`, the notes, and the full BuildInfo as `manifest`. The BuildInfo must have `source.repo`. If it has no `result`, `upload` attests `verified` at 100%, so run the full pipeline first.
- **`--verify`** works with any active key. The server rebuilds in its own sandbox and only promotes the source if its diff confirms the match. The rebuild runs in the background: `upload` prints the server's response and the `run_id`, and the verdict appears on the alkane's explorer page when the run finishes.
`upload` prints the server's JSON response, and exits with an error on any status other than 200 or 201.
:::caution Known issue with `upload --verify`
In the current `develop` code, `upload --verify` sends `build_env` as a JSON array and `typo_fix` as the text of a BuildInfo note, while the verify endpoint expects `build_env` as a newline-separated string and `typo_fix` as a boolean. A BuildInfo with a non-empty `environment.build_env`, or with a note that mentions a typo, can therefore be rejected. If that happens, submit the recipe with `curl` as shown [below](#submitting-without-the-cli), sending `build_env` as one string with a `KEY=VALUE` pair per line and `typo_fix` as `true` or `false`.
:::
### The BuildInfo artifact
A `BuildInfo` captures every axis that matters for a byte-exact rebuild. The canonical definition is the Rust type in `schema.rs` and the JSON Schema `build-info.schema.json`, both in [`crates/alkanes-cli-common/src/buildinfo/`](https://github.com/kungfuflex/alkanes-rs/tree/develop/crates/alkanes-cli-common/src/buildinfo) on `develop`, next to golden fixtures (the `4:797` fixture is the one quoted below; the `2:0` fixture records the Linux-only rebuild, and the `4:9200` fixture predates deployment). An abbreviated example, from the `4:797` fixture:
```json
{
"schema_version": "1.0.0",
"identity": { "target_source": "onchain", "alkane_id": "4:797", "network": "mainnet",
"target_sha256": "8b51384a…", "target_size": 297481 },
"source": { "kind": "git", "repo": "https://github.com/kungfuflex/free-mint", "commit": "e33d5e8",
"package": "free-mint", "is_workspace_member": false,
"build_command": "cargo build --release --locked --target wasm32-unknown-unknown -p free-mint" },
"toolchain": { "rustc_version": "1.82.0", "host_triple": "1.82.0-x86_64-unknown-linux-gnu",
"os_image": "ubuntu-22.04", "target": "wasm32-unknown-unknown" },
"c_toolchain": { "vendor": "Ubuntu", "version": "14.0.0",
"source": { "method": "apt", "apt_package": "clang-14" }, "c_object_host_dependent": false },
"environment": { "home": "/home/lee", "build_env": ["CC_wasm32_unknown_unknown=clang-14", "FREE_MINT_BUILD_IN_PROGRESS=1"] },
"registry": { "mode": "time-machine", "archive_commit": "010733a0…", "served_as": "index.crates.io",
"registry_hash": "index.crates.io-6f17d22bba15001f" },
"git_deps": [ { "url": "https://github.com/kungfuflex/alkanes-rs", "rev": "d787cdd1…", "source_form": "bare" } ],
"result": { "verdict": "reproducible", "built_sha256": "8b51384a…", "byte_exact": true,
"normalized_match_pct": 100.0, "sections": [] }
}
```
| Key | What it records |
|---|---|
| `identity` | The target: `onchain` (by alkane ID) or `local-file` (a `.wasm`, including a not-yet-deployed alkane), its sha256 and size. |
| `source` | A git repo, commit, `subdir` and package, or `inline` source files; the features and the literal build command. |
| `toolchain` | rustc version and the toolchain directory name (`host_triple`) whose paths must match, plus the sandbox OS image. |
| `c_toolchain` | clang vendor and version, how to obtain it (`apt`, `llvm-release` or `homebrew-bottle`), and whether the C object is host-dependent. |
| `environment` | `HOME`, `CARGO_HOME`, extra build environment variables, and any `remap-path-prefix` rules. |
| `registry` | `committed-lock` or `time-machine`, with the frozen `crates.io-index-archive` commit and the expected registry hash. |
| `git_deps` | Each git dependency's URL, rev, and source-id form (`bare` or `rev`). |
| `resolved_deps`, `cargo_lock_b64` | The resolved dependency graph and the exact `Cargo.lock` (base64), when known. |
| `result` | The diff outcome: verdict, rebuilt sha256, `byte_exact`, match percentage, per-section results. |
Each axis is explained in [Reproducible builds](../../build/reproducible-builds).
### Submitting without the CLI
`alkanes-cli upload` maps a BuildInfo onto the right request body for you. To call the HTTP API directly (for example from a script), the same `POST /api/v1/{key}/verify` endpoint accepts the fields as JSON. This is the `curl` equivalent of `upload --verify`:
```bash
curl -sS -X POST https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/verify \
-H 'content-type: application/json' \
-d '{
"alkane": "4:797",
"repo_url": "https://github.com/kungfuflex/free-mint",
"commit": "e33d5e8",
"package": "free-mint",
"rustc": "1.82.0",
"clang_version": "14.0.0",
"home_dir": "/home/lee",
"alkanes_rev": "d787cdd1f22ef1b7e32337fc531209df85ed1db8",
"git_source_form": "bare"
}'
```
Most of those fields are optional: the server **auto-reverses** the recipe (rustc, clang, `HOME`, host triple, git-dependency revs, and the crates.io freeze date) from the on-chain bytecode, so `{ alkane, repo_url, commit, package }` alone verifies most alkanes. Anything you send overrides the auto-reversed value. The endpoint also accepts `git_pins` (several git dependencies as `url rev [bare|rev]` lines) and `git_date` (a deploy-date freeze) for contracts that pin several git dependencies or ship no committed lockfile. See the [Verify and attest API](../verification/verify-and-attest) for the full field list and the admin-only `attest` path.
### Next steps
- [Verify and attest API](../verification/verify-and-attest): the HTTP endpoints `upload` calls.
- [Source API](../verification/source-api): read a verified alkane's source once it is published.
- [API Keys](../platform/api-keys): get the key `upload` needs.
---
## DataAPI Commands
The `dataapi` namespace provides high-level queries for Alkanes tokens, AMM pools, balances, and market data via the SUBFROST Data API.
### Example commands
Get the current Bitcoin price:
```bash
$ alkanes-cli -p mainnet dataapi get-bitcoin-price
```
Get all liquidity pools from the AMM factory:
```bash
$ alkanes-cli -p mainnet dataapi get-pools
```
Get the number of unique holders for an alkane token:
```bash
$ alkanes-cli -p mainnet dataapi get-holder-count 2:0
```
### Commands Overview
**Alkanes Queries:**
- **`get-alkanes`**: Get all alkanes tokens
- **`get-alkanes-by-address`**: Get alkanes for an address
- **`get-alkane-details`**: Get details for a specific alkane
**Pool & AMM:**
- **`get-pools`**: Get all liquidity pools
- **`get-pool-by-id`**: Get pool details by ID
- **`get-pool-history`**: Get historical pool data
- **`get-swap-history`**: Get swap history
**Balances:**
- **`get-address-balances`**: Get alkane balances for an address (with UTXO tracking)
- **`get-outpoint-balances`**: Get alkane balances for a specific outpoint
- **`get-holders`**: Get holders of an alkane token
- **`get-holder-count`**: Get holder count for an alkane
**Market Data:**
- **`get-bitcoin-price`**: Get current Bitcoin price
- **`get-market-chart`**: Get Bitcoin market chart
**Indexer Status:**
- **`get-block-height`**: Get latest processed block height
- **`get-block-hash`**: Get latest processed block hash
- **`get-indexer-position`**: Get indexer position (height + hash)
- **`health`**: Health check
### Configuration
The Data API uses a separate endpoint from JSON-RPC:
```bash
# Using the data API endpoint
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-alkanes
```
Or with your API key for higher rate limits:
```bash
--data-api https://mainnet.subfrost.io/v4/YOUR_API_KEY
```
### dataapi get-alkanes
Get all alkanes tokens indexed by the system.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-alkanes
```
**Example Output:**
```
Alkanes Tokens
═══════════════════════════════════
1. 2:0
2. 32:0
3. 4:65522
...
Total: 25
```
### dataapi get-alkanes-by-address
Get alkanes tokens held by a specific address.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-alkanes-by-address bc1p...
```
**Arguments:**
- ``: Bitcoin address to query
### dataapi get-alkane-details
Get detailed information about a specific alkane token.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-alkane-details 2:0
```
**Arguments:**
- ``: Alkane ID (format: `block:tx`)
### dataapi get-pools
Get all liquidity pools from the AMM factory.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-pools
```
Uses factory 4:65522 by default.
**Example Output:**
```
Liquidity Pools
═══════════════════════════════════
1. DIESEL / frBTC LP
Pool ID: 2:3
Pair: 2:0 -> 32:0
Reserves: 300000000 x 50000
LP Supply: 3872983
Creator: bc1p...
Total: 1 pools
```
### dataapi get-pool-by-id
Get details for a specific liquidity pool.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-pool-by-id 2:3
```
**Arguments:**
- ``: Pool ID (format: `block:tx`)
### dataapi get-pool-history
Get historical data for a liquidity pool.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-pool-history 2:3
```
**Arguments:**
- ``: Pool ID
### dataapi get-swap-history
Get swap history for the AMM.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-swap-history
```
### dataapi get-address-balances
Get alkane balances for an address with UTXO tracking.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-address-balances bc1p...
```
**Arguments:**
- ``: Bitcoin address to query
Returns balances broken down by UTXO for accurate tracking.
### dataapi get-outpoint-balances
Get alkane balances for a specific transaction output.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-outpoint-balances :
```
**Arguments:**
- ``: Transaction outpoint (format: `txid:vout`)
### dataapi get-holders
Get all holders of a specific alkane token.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-holders 2:0
```
**Arguments:**
- ``: Alkane ID
### dataapi get-holder-count
Get the number of unique holders for an alkane token.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-holder-count 2:0
```
**Arguments:**
- ``: Alkane ID
### dataapi get-bitcoin-price
Get the current Bitcoin price.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-bitcoin-price
```
### dataapi get-market-chart
Get Bitcoin market chart data.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-market-chart
```
### dataapi get-block-height
Get the latest block height processed by the indexer.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-block-height
```
**Example Output:**
```json
{
"height": 850123
}
```
### dataapi get-block-hash
Get the latest block hash processed by the indexer.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-block-hash
```
### dataapi get-indexer-position
Get the current indexer position (height and hash).
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi get-indexer-position
```
### dataapi health
Check if the Data API is healthy.
```bash
alkanes-cli -p mainnet \
--data-api https://mainnet.subfrost.io/v4/api \
dataapi health
```
**Output:**
```
OK
```
### REST API Equivalent
These DataAPI commands correspond to REST endpoints:
- `get-alkanes` maps to `GET /alkanes`
- `get-pools` maps to `GET /pools`
- `get-address-balances` maps to `GET /address/{address}/balances`
- `get-block-height` maps to `GET /indexer/height`
- `health` maps to `GET /health`
See the [REST API documentation](../rest/overview) for the full endpoint reference.
### Next Steps
- [Alkanes Commands](./alkanes): Protocol operations
- [REST API Overview](../rest/overview): Full REST endpoint reference
- [Pools & AMM](../rest/pools): AMM REST endpoints
---
## Esplora Commands
The `esplora` namespace provides block explorer functionality for querying blocks, transactions, addresses, and fee estimates via the Electrs/Esplora API.
### Example commands
Get the current blockchain tip height:
```bash
$ alkanes-cli -p mainnet esplora blocks-tip-height
```
Get unspent transaction outputs (UTXOs) for an address:
```bash
$ alkanes-cli -p mainnet esplora address-utxo bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
### Commands Overview
**Block Queries:**
- **`blocks-tip-hash`**: Get the hash of the latest block
- **`blocks-tip-height`**: Get the height of the latest block
- **`blocks`**: Get latest blocks
- **`block-height`**: Get block hash at a height
- **`block`**: Get block details
- **`block-status`**: Get block status
- **`block-txids`**: Get transaction IDs in a block
- **`block-header`**: Get block header
- **`block-raw`**: Get raw block data
- **`block-txid`**: Get transaction at index in block
- **`block-txs`**: Get transactions in a block
**Address Queries:**
- **`address`**: Get address information
- **`address-txs`**: Get address transactions
- **`address-txs-chain`**: Get confirmed address transactions
- **`address-txs-mempool`**: Get mempool transactions for address
- **`address-utxo`**: Get UTXOs for an address
- **`address-prefix`**: Search addresses by prefix
**Transaction Queries:**
- **`tx`**: Get transaction details
- **`tx-hex`**: Get transaction as hex
- **`tx-raw`**: Get raw transaction data
- **`tx-status`**: Get transaction status
- **`tx-merkle-proof`**: Get merkle proof
- **`tx-merkleblock-proof`**: Get merkleblock proof
- **`tx-outspend`**: Get outpoint spending info
- **`tx-outspends`**: Get all output spending info
**Broadcasting:**
- **`broadcast`**: Broadcast a transaction
- **`post-tx`**: Post a transaction
**Mempool:**
- **`mempool`**: Get mempool info
- **`mempool-txids`**: Get mempool transaction IDs
- **`mempool-recent`**: Get recent mempool transactions
**Fees:**
- **`fee-estimates`**: Get fee rate estimates
### esplora blocks-tip-height
Get the current blockchain tip height.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora blocks-tip-height
```
**Output:**
```
Tip Height: 850123
```
### esplora blocks-tip-hash
Get the hash of the latest block.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora blocks-tip-hash
```
### esplora block
Get details for a specific block.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora block 840000
```
**Arguments:**
- ``: Block hash or height
### esplora block-txids
Get all transaction IDs in a block.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora block-txids 840000
```
### esplora address
Get information about a Bitcoin address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora address bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
**Arguments:**
- ``: Bitcoin address
### esplora address-utxo
Get unspent transaction outputs (UTXOs) for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora address-utxo bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
**Arguments:**
- ``: Bitcoin address
**Example Response:**
```json
[
{
"txid": "abc123...",
"vout": 0,
"value": 10000,
"status": {
"confirmed": true,
"block_height": 840000
}
}
]
```
### esplora address-txs
Get transactions for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora address-txs bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
### esplora address-txs-mempool
Get unconfirmed (mempool) transactions for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora address-txs-mempool bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
### esplora tx
Get transaction details.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora tx
```
**Arguments:**
- ``: Transaction ID
### esplora tx-hex
Get transaction as hexadecimal.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora tx-hex
```
### esplora tx-status
Get transaction confirmation status.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora tx-status
```
### esplora tx-outspend
Check if a specific output has been spent.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora tx-outspend
```
**Arguments:**
- ``: Transaction ID
- ``: Output index
### esplora fee-estimates
Get current fee rate estimates for different confirmation targets.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora fee-estimates
```
**Example Response:**
```json
{
"1": 25.5,
"3": 20.0,
"6": 15.2,
"25": 10.0,
"144": 5.0,
"504": 3.0
}
```
The keys represent confirmation target in blocks, values are fee rates in sat/vB.
### esplora broadcast
Broadcast a signed transaction to the network.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora broadcast
```
**Arguments:**
- ``: Signed transaction as hexadecimal
### esplora mempool
Get mempool statistics.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora mempool
```
### esplora mempool-txids
Get all transaction IDs in the mempool.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora mempool-txids
```
### esplora mempool-recent
Get recent mempool transactions.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
esplora mempool-recent
```
### JSON-RPC Equivalent
These CLI commands correspond to JSON-RPC methods with the `esplora_` prefix:
- `esplora address-utxo` maps to `esplora_address::utxo`
- `esplora fee-estimates` maps to `esplora_fee-estimates`
- `esplora blocks-tip-height` maps to `esplora_blocks:tip:height`
**Example JSON-RPC:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_address::utxo",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
### Next Steps
- [Ord Commands](./ord): Ordinals queries
- [BRC20-Prog Commands](./brc20-prog): EVM contract operations
- [DataAPI Commands](./dataapi): High-level data queries
---
## Espo Commands
The `espo` namespace provides access to the Espo indexer for alkanes data and AMM analytics. Espo offers two modules: **Essentials** for core alkanes data and **AMM Data** for trading analytics.
### Example commands
These examples assume the Espo endpoint is configured as shown under [Configuration](#configuration).
#### Essentials Module
Ping the Espo server to check connectivity:
```bash
$ alkanes-cli -p mainnet espo ping
```
Get the current Espo indexer height:
```bash
$ alkanes-cli -p mainnet espo get-height
```
Get alkanes balances for an address:
```bash
$ alkanes-cli -p mainnet espo get-address-balances bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
```
Get holders of an alkane token (paginated):
```bash
$ alkanes-cli -p mainnet espo get-holders 2:0
```
Get total holder count for an alkane:
```bash
$ alkanes-cli -p mainnet espo get-holders-count 2:0
```
#### AMM Data Module
Ping the AMM Data module:
```bash
$ alkanes-cli -p mainnet espo ammdata-ping
```
Get OHLCV candlestick data for a pool:
```bash
$ alkanes-cli -p mainnet espo get-candles 840100:5
```
Get all AMM pools with pagination:
```bash
$ alkanes-cli -p mainnet espo get-pools
```
Find optimal multi-hop swap route:
```bash
$ alkanes-cli -p mainnet espo find-best-swap-path 840000:1 840000:2
```
### Commands Overview
#### Essentials Module
Core alkanes data queries including balances, holders, and storage.
- **`ping`**: Ping the Espo server
- **`get-height`**: Get current Espo indexer height
- **`get-address-balances`**: Get alkanes balances for an address
- **`get-address-outpoints`**: Get outpoints containing alkanes for an address
- **`get-outpoint-balances`**: Get alkanes balances at a specific outpoint
- **`get-holders`**: Get holders of an alkane token (paginated)
- **`get-holders-count`**: Get total holder count for an alkane
- **`get-keys`**: Get storage keys for an alkane contract (paginated)
#### AMM Data Module
Trading and liquidity analytics for AMM pools.
- **`ammdata-ping`**: Ping the AMM Data module
- **`get-candles`**: Get OHLCV candlestick data for a pool
- **`get-trades`**: Get trade history for a pool
- **`get-pools`**: Get all pools with pagination
- **`find-best-swap-path`**: Find optimal multi-hop swap route
- **`get-best-mev-swap`**: Find best MEV arbitrage opportunity
### Configuration
Configure the Espo RPC endpoint:
```bash
# Using environment variable
export ESPO_RPC_URL=https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo
# Or as a flag
alkanes-cli --espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo espo ping
```
The API key is required: a request to `https://mainnet.subfrost.io/v4/espo` without a key returns HTTP 401 with `{"code":"INVALID_API_KEY"}`.
### espo ping
Ping the Espo server to check connectivity.
```bash
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo ping
```
**Example Output:**
```
pong
```
### espo get-height
Get the current block height processed by the Espo indexer.
```bash
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-height
```
**Example Output:**
```
Espo Height: 825000
```
### espo get-address-balances
Get alkanes balances for a Bitcoin address. Optionally include detailed outpoint information.
```bash
# Basic balance query
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-address-balances bc1q...
# With outpoint details
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-address-balances bc1q... --include-outpoints
```
**Example Output:**
```
Alkanes Balances
═════════════════════════════════════
Address: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
Alkane ID Balance
───────────── ─────────────
840000:1 1000000
840000:2 500000
Total Alkanes: 2
```
### espo get-holders
Get paginated list of holders for an alkane token.
```bash
# First page (100 holders)
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-holders 2:0
# Specific page with custom limit
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-holders 2:0 --page 2 --limit 50
```
**Example Output:**
```
Holders for Alkane 2:0
═════════════════════════════════════
Page: 0 | Limit: 100 | Total: 1523
Address Balance
────────────────────────────────────────────── ──────────
bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh 10000000
bc1q2j3k4l5m6n7o8p9q0r1s2t3u4v5w6x7y8z9 5000000
...
Has More: true
```
### espo get-candles
Get OHLCV candlestick data for a liquidity pool.
```bash
# Get 1-hour candles for base token
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-candles 840100:5 \
--timeframe 1h \
--side base \
--limit 100
# Get daily candles for quote token
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-candles 840100:5 \
--timeframe 1d \
--side quote \
--limit 30
```
**Timeframe Options:**
- `10m`: 10 minutes
- `1h`: 1 hour
- `1d`: 1 day
- `1w`: 1 week
- `1M`: 1 month
**Example Output:**
```
Candles for Pool 840100:5
═════════════════════════════════════
Timeframe: 1h | Side: base | Page: 0
Time Open High Low Close Volume
────────────────── ──────── ──────── ──────── ──────── ──────────
2024-12-15 10:00 1000000 1050000 990000 1020000 5000000
2024-12-15 11:00 1020000 1080000 1010000 1050000 6000000
...
Total: 100
```
### espo find-best-swap-path
Find the optimal multi-hop swap path between two tokens.
```bash
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo find-best-swap-path 840000:1 840000:2 \
--mode exact_in \
--amount-in 1000000 \
--amount-out-min 900000 \
--max-hops 3
```
**Modes:**
- `exact_in`: Specify input amount, calculate output
- `exact_out`: Specify output amount, calculate input
- `implicit`: Let the system determine the best approach
**Example Output:**
```
Best Swap Path
═════════════════════════════════════
Token In: 840000:1
Token Out: 840000:2
Mode: exact_in
Amount In: 1000000
Amount Out: 950000
Hops: 2
Route:
1. Pool 840100:5 (840000:1 → 840000:3)
Amount In: 1000000
Amount Out: 1500000
2. Pool 840100:6 (840000:3 → 840000:2)
Amount In: 1500000
Amount Out: 950000
```
### espo get-best-mev-swap
Find the best MEV arbitrage opportunity for a token.
```bash
alkanes-cli -p mainnet \
--espo-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo \
espo get-best-mev-swap 840000:1 \
--fee-bps 30 \
--max-hops 3
```
**Example Output:**
```
Best MEV Opportunity
═════════════════════════════════════
Token: 840000:1
Fee (bps): 30 (0.3%)
Max Hops: 3
Amount In: 1000000
Amount Out: 1050000
Profit: 50000 (5%)
Arbitrage Route:
1. Pool 840100:5 (840000:1 → 840000:2)
Amount: 1000000 → 2000000
2. Pool 840100:6 (840000:2 → 840000:1)
Amount: 2000000 → 1050000
Net Profit: 50000
```
### Notes
- All pagination uses 0-based page numbers
- Alkane IDs are in format `block:tx` (e.g., `840000:1`)
- Outpoints are in format `txid:vout`
- AMM Data methods require AMM pools to be indexed
- The Espo indexer must be configured and running
- Use `--help` on any command to see all available options
### REST API Equivalent
These Espo commands correspond to JSON-RPC methods:
- `espo ping` → `ping`
- `espo get-height` → `get_espo_height`
- `espo get-address-balances` → `get_address_balances`
- `espo get-candles` → `ammdata.get_candles`
- `espo find-best-swap-path` → `ammdata.find_best_swap_path`
See the [JSON-RPC Espo documentation](../json-rpc/espo) for API details.
---
## Installation
This guide covers installing `alkanes-cli` from source and setting up the `@alkanes/ts-sdk` TypeScript library.
### Installing alkanes-cli
#### Prerequisites
- **Rust** (1.70+): Install via [rustup](https://rustup.rs/)
- **Git**: For cloning the repository
- **Build tools**: `gcc`, `make`, etc.
#### Clone and Build
```bash
# Clone the repository (develop branch)
git clone https://github.com/kungfuflex/alkanes-rs.git -b develop
cd alkanes-rs
# Build the CLI in release mode
cargo build --release -p alkanes-cli
# The binary is at target/release/alkanes-cli
./target/release/alkanes-cli --version
```
#### Add to PATH
```bash
# Option 1: Copy to system path
sudo cp target/release/alkanes-cli /usr/local/bin/
# Option 2: Add to your shell profile
echo 'export PATH="$HOME/alkanes-rs/target/release:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
#### Verify Installation
```bash
alkanes-cli --version
alkanes-cli --help
```
### Creating a Wallet
Before using most commands, you need a wallet:
```bash
# Create a new wallet with a random mnemonic
alkanes-cli wallet create --passphrase "your-secure-passphrase"
# Or restore from an existing mnemonic
alkanes-cli wallet create "your twelve word mnemonic phrase here" \
--passphrase "your-secure-passphrase"
```
The wallet is saved to `~/.alkanes/wallet.json` by default. You can specify a different path with `--output`:
```bash
alkanes-cli wallet create --passphrase "your-passphrase" \
--output /path/to/wallet.json
```
#### View Your Addresses
```bash
alkanes-cli --wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet addresses
```
This shows addresses for all supported types:
- **P2TR** (Taproot): Recommended for Alkanes
- **P2WPKH** (Native SegWit)
- **P2SH** (SegWit-compatible)
- **P2PKH** (Legacy)
### Installing @alkanes/ts-sdk
#### From npm (when published)
```bash
npm install @alkanes/ts-sdk
# or
yarn add @alkanes/ts-sdk
# or
pnpm add @alkanes/ts-sdk
```
#### From Source
```bash
# Clone the repository
git clone https://github.com/kungfuflex/alkanes-rs.git -b develop
cd alkanes-rs/ts-sdk
# Install dependencies
npm install
# Build the SDK (includes WASM)
npm run build
```
#### Link for Local Development
```bash
cd alkanes-rs/ts-sdk
npm link
# In your project
npm link @alkanes/ts-sdk
```
### TypeScript SDK Usage
#### Basic Setup
```typescript
import {
createKeystore,
unlockKeystore,
createWallet,
createProvider
} from '@alkanes/ts-sdk';
// Create a new wallet
const { keystore, mnemonic } = await createKeystore('your-password', {
network: 'mainnet'
});
console.log('Save this mnemonic:', mnemonic);
// Later, unlock the wallet
const unlockedKeystore = await unlockKeystore(keystore, 'your-password');
const wallet = createWallet(unlockedKeystore);
// Get addresses
const taprootAddress = wallet.getReceivingAddress(0);
console.log('Taproot address:', taprootAddress);
```
#### Create a Provider
```typescript
import { createProvider } from '@alkanes/ts-sdk';
import * as bitcoin from 'bitcoinjs-lib';
const provider = createProvider({
url: 'https://mainnet.subfrost.io/v4/jsonrpc',
network: bitcoin.networks.bitcoin,
networkType: 'mainnet',
});
// Get balance
const balance = await provider.getBalance(address);
console.log('Balance:', balance);
```
#### With WASM (for Alkanes features)
```typescript
import init, * as wasm from '@alkanes/ts-sdk/wasm';
import { createProvider, KeystoreManager } from '@alkanes/ts-sdk';
// Initialize WASM (call once at app startup)
await init();
// Create provider with WASM
const provider = createProvider({
url: 'https://mainnet.subfrost.io/v4/jsonrpc',
networkType: 'mainnet',
}, wasm);
// Use WASM-powered features
const keystoreManager = new KeystoreManager(wasm);
```
### Configuration
#### Environment Variables
Set these to avoid passing them as flags:
```bash
# SUBFROST API key
export SUBFROST_API_KEY="your-api-key"
# Default network
export ALKANES_NETWORK="mainnet"
```
#### Regtest Configuration
For local development with regtest:
```bash
alkanes-cli -p regtest \
--jsonrpc-url https://regtest.subfrost.io/v4/jsonrpc \
--data-api https://regtest.subfrost.io/v4/api \
metashrew height
```
#### Mainnet Configuration
For production use:
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY \
--data-api https://mainnet.subfrost.io/v4/YOUR_API_KEY \
metashrew height
```
### Building WASM Module
If you need to rebuild the WASM module:
```bash
# Install wasm-pack
cargo install wasm-pack
# Build the WASM module
cd alkanes-rs/ts-sdk
npm run build:wasm
```
### Troubleshooting
#### Build Errors
**Missing OpenSSL:**
```bash
# Ubuntu/Debian
sudo apt-get install libssl-dev pkg-config
# macOS
brew install openssl
```
**wasm-pack not found:**
```bash
cargo install wasm-pack
```
#### Connection Errors
**Rate limiting:**
Use your API key for higher limits:
```bash
--jsonrpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY
```
**Network mismatch:**
Ensure `-p` matches your endpoint network.
### Next Steps
- [Wallet Commands](./wallet): Manage wallets and sign transactions
- [Alkanes Commands](./alkanes): Protocol operations
- [Quickstart](../getting-started/quickstart): API quickstart guide
---
## Ord Commands
The `ord` namespace provides queries for the Ordinals protocol including inscriptions, runes, satoshis, and related data.
### Example commands
Get details about an Ordinal inscription by ID:
```bash
$ alkanes-cli -p mainnet ord inscription 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```
Get information about a Rune by name:
```bash
$ alkanes-cli -p mainnet ord rune UNCOMMON•GOODS
```
### Commands Overview
- **`inscription`**: Get inscription details by ID
- **`inscriptions-in-block`**: Get all inscriptions in a block
- **`address-info`**: Get address information
- **`block-info`**: Get block information
- **`block-count`**: Get the latest block count
- **`blocks`**: Get latest blocks
- **`children`**: Get children of an inscription
- **`content`**: Get inscription content
- **`output`**: Get output information
- **`parents`**: Get parents of an inscription
- **`rune`**: Get rune information
- **`sat`**: Get sat information
- **`tx-info`**: Get transaction information
### ord inscription
Get details about an Ordinal inscription by ID.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord inscription 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```
**Arguments:**
- ``: Inscription ID (format: `txid` + `i` + `index`)
**Example Response:**
```json
{
"id": "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0",
"number": 12345,
"address": "bc1p...",
"content_type": "image/png",
"content_length": 1234,
"sat": 1234567890,
"timestamp": "2024-01-15T12:30:00Z"
}
```
### ord inscriptions-in-block
Get all inscriptions created in a specific block.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord inscriptions-in-block 840000
```
**Arguments:**
- ``: Block height to query
### ord address-info
Get Ordinals-related information for an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord address-info bc1p...
```
**Arguments:**
- ``: Bitcoin address to query
### ord block-info
Get detailed block information from the Ord indexer.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord block-info 840000
```
**Arguments:**
- ``: Block height to query
### ord block-count
Get the current block count from the Ord indexer.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord block-count
```
**Example Output:**
```
850123
```
### ord blocks
Get information about the latest blocks.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord blocks
```
### ord children
Get child inscriptions of a parent inscription.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord children 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```
**Arguments:**
- ``: Parent inscription ID
### ord content
Get the content of an inscription.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord content 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```
**Arguments:**
- ``: Inscription ID
Returns the raw content bytes of the inscription.
### ord output
Get information about a specific transaction output.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord output :
```
**Arguments:**
- ``: Transaction output (format: `txid:vout`)
### ord parents
Get parent inscriptions of a child inscription.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord parents 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```
**Arguments:**
- ``: Child inscription ID
### ord rune
Get information about a Rune by name.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord rune "UNCOMMON•GOODS"
```
**Arguments:**
- ``: Rune name (with bullet character if applicable)
**Example Response:**
```json
{
"id": "840000:1",
"name": "UNCOMMON•GOODS",
"symbol": "⧉",
"divisibility": 0,
"supply": "21000000",
"burned": "0",
"timestamp": "2024-04-20T00:00:00Z"
}
```
### ord sat
Get information about a specific satoshi.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord sat 1234567890
```
**Arguments:**
- ``: Satoshi number
**Example Response:**
```json
{
"sat": 1234567890,
"rarity": "common",
"block": 12345,
"cycle": 0,
"epoch": 0,
"period": 0,
"inscription": null
}
```
### ord tx-info
Get transaction information from the Ord indexer.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
ord tx-info
```
**Arguments:**
- ``: Transaction ID
### JSON-RPC Equivalent
These CLI commands correspond to JSON-RPC methods:
- `ord inscription` maps to `ord_inscription`
- `ord rune` maps to `ord_rune`
- `ord sat` maps to `ord_sat`
- `ord block-count` maps to `ord_blockcount`
**Example JSON-RPC:**
```json
{
"jsonrpc": "2.0",
"method": "ord_inscription",
"params": ["6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"],
"id": 1
}
```
### Next Steps
- [Esplora Commands](./esplora): Block explorer queries
- [Alkanes Commands](./alkanes): Protocol operations
- [DataAPI Commands](./dataapi): High-level data queries
---
## alkanes-cli & @alkanes/ts-sdk
The `alkanes-cli` command-line tool and `@alkanes/ts-sdk` TypeScript library provide comprehensive access to the Alkanes protocol, Bitcoin blockchain, and SUBFROST APIs.
### What is alkanes-cli?
`alkanes-cli` is a powerful command-line interface for:
- **Wallet Management**: Create, manage, and sign transactions with HD wallets
- **Alkanes Metaprotocol**: Deploy contracts, execute transactions, and query balances
- **AMM & Liquidity**: Initialize pools, execute swaps, and manage liquidity
- **Bitcoin Operations**: Query blocks, transactions, UTXOs, and fee estimates
- **Ordinals & Runes**: Query inscriptions, runes, and sat information
- **BRC20-Prog**: Deploy and interact with EVM-compatible contracts on Bitcoin
### Connecting to SUBFROST
#### Network Endpoints
The CLI can connect to SUBFROST APIs on different networks:
**Mainnet**
- JSON-RPC: `https://mainnet.subfrost.io/v4/jsonrpc`
- Data API: `https://mainnet.subfrost.io/v4/api`
**Signet**
- JSON-RPC: `https://signet.subfrost.io/v4/jsonrpc`
- Data API: `https://signet.subfrost.io/v4/api`
**Regtest**
- JSON-RPC: `https://regtest.subfrost.io/v4/jsonrpc`
- Data API: `https://regtest.subfrost.io/v4/api`
#### Using Your API Key
For higher rate limits, use your API key in the path:
```bash
# With API key (higher rate limits)
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY \
metashrew height
```
#### Free Public Endpoints
For development and testing, use the public routes:
```bash
# Public endpoint (rate limited)
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
metashrew height
```
### Command Namespaces
The CLI is organized into these command namespaces:
- **`wallet`**: HD wallet creation, addresses, signing, and transactions
- **`alkanes`**: Alkanes protocol operations (execute, simulate, trace, swap)
- **`ord`**: Ordinals protocol queries (inscriptions, runes, sats)
- **`esplora`**: Block explorer queries (blocks, transactions, addresses)
- **`brc20-prog`**: BRC20-Prog EVM contract operations
- **`dataapi`**: High-level data queries (alkanes, pools, balances)
- **`metashrew`**: Low-level indexer queries
- **`bitcoind`**: Bitcoin Core RPC passthrough
- **`runestone`**: Runestone/protorune analysis
- **`protorunes`**: Protorune queries
- **`subfrost`**: SUBFROST-specific utilities
- **`lua`**: Server-side Lua script execution
### Global Options
These options are available for all commands:
```bash
alkanes-cli [OPTIONS]
Options:
--wallet-file Path to wallet file (default: ~/.alkanes/wallet.json)
--passphrase Wallet passphrase for signing
--wallet-address Watch-only address (no signing)
--wallet-key Private key as hex string
--wallet-key-file Private key file path
--jsonrpc-url JSON-RPC endpoint URL
--data-api Data API endpoint URL
--esplora-api-url Esplora API endpoint
--ord-server-url Ord server endpoint
--metashrew-rpc-url Metashrew RPC endpoint
--brc20-prog-rpc-url BRC20-Prog RPC endpoint
--subfrost-api-key SUBFROST API key (or use SUBFROST_API_KEY env)
-p, --provider Network provider [default: regtest]
Options: mainnet, signet, regtest
-h, --help Print help
-V, --version Print version
```
### Example commands
The commands below show the shape of the CLI; see the following pages for the full reference per namespace.
Check the current block height from the Metashrew indexer:
```bash
$ alkanes-cli -p mainnet metashrew height
```
Get the current Bitcoin price from the Data API:
```bash
$ alkanes-cli -p mainnet dataapi get-bitcoin-price
```
Query metadata for an alkane token (name, symbol, supply):
```bash
$ alkanes-cli -p mainnet alkanes reflect-alkane 2:0
```
Get unspent transaction outputs for a Bitcoin address:
```bash
$ alkanes-cli -p mainnet esplora address-utxo bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h
```
### TypeScript SDK (@alkanes/ts-sdk)
The same functionality is available in TypeScript via `@alkanes/ts-sdk`:
```typescript
import {
createKeystore,
unlockKeystore,
createWallet,
createProvider
} from '@alkanes/ts-sdk';
// Create a provider
const provider = createProvider({
url: 'https://mainnet.subfrost.io/v4/jsonrpc',
networkType: 'mainnet',
});
// Get block height
const height = await provider.getBlockHeight();
console.log('Block height:', height);
```
See the [Installation](./installation) guide to get started.
### Next Steps
- [Installation](./installation): Install the CLI and SDK
- [Wallet Commands](./wallet): Wallet management
- [Alkanes Commands](./alkanes): Protocol operations
- [DataAPI Commands](./dataapi): High-level queries
---
## Wallet Commands
The `wallet` namespace provides HD wallet management, address derivation, transaction signing, and balance queries.
### Commands Overview
- **`create`**: Create a new wallet or restore from mnemonic
- **`addresses`**: Display wallet addresses
- **`utxos`**: List UTXOs in the wallet
- **`balance`**: Get wallet balance
- **`send`**: Send a transaction
- **`sign`**: Sign a PSBT
- **`create-tx`**: Create an unsigned transaction
- **`sign-tx`**: Sign a transaction
- **`decode-tx`**: Decode a transaction
- **`broadcast-tx`**: Broadcast a transaction
- **`fee-rates`**: Get current fee rates
- **`history`**: Get transaction history
- **`mnemonic`**: Display wallet mnemonic
- **`freeze` / `unfreeze`**: Freeze/unfreeze UTXOs
- **`backup`**: Backup the wallet
- **`sync`**: Sync wallet with blockchain
### wallet create
Create a new HD wallet or restore from a mnemonic.
```bash
# Create new wallet with random mnemonic
alkanes-cli wallet create --passphrase "your-secure-passphrase"
# Restore from existing mnemonic
alkanes-cli wallet create "word1 word2 word3 ... word12" \
--passphrase "your-passphrase"
# Specify output file
alkanes-cli wallet create --passphrase "your-passphrase" \
--output /custom/path/wallet.json
```
**Options:**
- `--passphrase`: Encryption passphrase (required)
- `-o, --output`: Output file path (default: `~/.alkanes/wallet.json`)
**TypeScript SDK:**
```typescript
import { createKeystore, unlockKeystore, createWallet } from '@alkanes/ts-sdk';
// Create new wallet
const { keystore, mnemonic } = await createKeystore('password123', {
network: 'mainnet'
});
console.log('Mnemonic:', mnemonic);
// Restore from mnemonic
import { createWalletFromMnemonic } from '@alkanes/ts-sdk';
const wallet = createWalletFromMnemonic(mnemonic, 'mainnet');
```
### wallet addresses
Display all derived addresses from the wallet.
```bash
alkanes-cli -p mainnet \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet addresses
```
**Output shows:**
- P2TR (Taproot) addresses: `bc1p...` or `bcrt1p...`
- P2WPKH (Native SegWit): `bc1q...` or `bcrt1q...`
- P2SH (SegWit-compatible): `3...` or `2...`
- P2PKH (Legacy): `1...` or `m/n...`
**TypeScript SDK:**
```typescript
import { createWallet, AddressType } from '@alkanes/ts-sdk';
const wallet = createWallet(keystore);
// Get Taproot receiving address
const p2tr = wallet.getReceivingAddress(0);
// Get specific address type
const p2wpkh = wallet.deriveAddress(AddressType.P2WPKH, 0);
// Get multiple addresses
const addresses = wallet.getAddresses(0, 20); // 20 addresses starting from index 0
```
### wallet balance
Get the BTC balance of the wallet.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet balance
```
**Output:**
```
Confirmed: 0.00150000
Pending: 0.00000000
```
**TypeScript SDK:**
```typescript
const provider = createProvider({
url: 'https://mainnet.subfrost.io/v4/jsonrpc',
networkType: 'mainnet',
});
const balance = await provider.getBalance(address);
console.log('Balance:', balance);
```
### wallet utxos
List all unspent transaction outputs (UTXOs) in the wallet.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet utxos
```
### wallet send
Send BTC to an address.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet send \
--fee-rate 10 \
-y # Auto-confirm
```
**Arguments:**
- ``: Recipient Bitcoin address
- ``: Amount in satoshis
**Options:**
- `--fee-rate `: Fee rate in sat/vB
- `-y, --auto-confirm`: Skip confirmation prompt
### wallet fee-rates
Get current network fee rate estimates.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
wallet fee-rates
```
**Output:**
```
Fast: 15 sat/vB
Medium: 8 sat/vB
Slow: 3 sat/vB
```
### wallet create-tx
Create an unsigned transaction without broadcasting.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-address \
wallet create-tx \
--fee-rate 10
```
### wallet sign-tx
Sign a transaction from a hex file.
```bash
alkanes-cli -p mainnet \
--wallet-key-file /path/to/private-key.hex \
wallet sign-tx \
--from-file /path/to/unsigned_tx.hex \
--truncate-excess-vsize # Optional: truncate if over 1MB
```
**Options:**
- `--from-file `: Path to unsigned transaction hex
- `--truncate-excess-vsize`: Truncate inputs if transaction exceeds size limit
### wallet decode-tx
Decode and display transaction details.
```bash
alkanes-cli wallet decode-tx --file /path/to/tx.hex
```
**Output shows:**
- TXID
- Number of inputs/outputs
- Virtual size
- Fee rate
### wallet broadcast-tx
Broadcast a signed transaction to the network.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
wallet broadcast-tx --file /path/to/signed_tx.hex
```
### wallet sign
Sign a PSBT (Partially Signed Bitcoin Transaction).
```bash
alkanes-cli -p mainnet \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet sign --psbt
```
**TypeScript SDK:**
```typescript
const wallet = createWallet(keystore);
// Sign PSBT
const signedPsbt = wallet.signPsbt(psbtBase64);
// Extract raw transaction
const txHex = wallet.extractTransaction(signedPsbt);
```
### wallet mnemonic
Display the wallet's mnemonic phrase (use with caution!).
```bash
alkanes-cli \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet mnemonic
```
### wallet freeze / unfreeze
Freeze or unfreeze specific UTXOs to prevent them from being spent.
```bash
# Freeze a UTXO
alkanes-cli wallet freeze :
# Unfreeze a UTXO
alkanes-cli wallet unfreeze :
```
### wallet backup
Create a backup of the wallet file.
```bash
alkanes-cli \
--wallet-file ~/.alkanes/wallet.json \
wallet backup --output /path/to/backup.json
```
### wallet history
Get transaction history for the wallet.
```bash
alkanes-cli -p mainnet \
--jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
--wallet-file ~/.alkanes/wallet.json \
--passphrase "your-passphrase" \
wallet history
```
### Complete TypeScript Example
```typescript
import {
createKeystore,
unlockKeystore,
createWallet,
createProvider,
AddressType
} from '@alkanes/ts-sdk';
import * as bitcoin from 'bitcoinjs-lib';
async function main() {
// Create wallet
const { keystore, mnemonic } = await createKeystore('password123', {
network: 'mainnet'
});
console.log('Mnemonic (save securely!):', mnemonic);
// Unlock and use
const unlocked = await unlockKeystore(keystore, 'password123');
const wallet = createWallet(unlocked);
// Get addresses
const taprootAddr = wallet.getReceivingAddress(0);
const segwitAddr = wallet.deriveAddress(AddressType.P2WPKH, 0);
console.log('Taproot:', taprootAddr);
console.log('SegWit:', segwitAddr);
// Create provider
const provider = createProvider({
url: 'https://mainnet.subfrost.io/v4/jsonrpc',
network: bitcoin.networks.bitcoin,
networkType: 'mainnet',
});
// Check balance
const balance = await provider.getBalance(taprootAddr);
console.log('Balance:', balance);
// Create and sign PSBT
const psbt = await wallet.createPsbt({
inputs: [{ txid: '...', vout: 0, value: 10000, address: taprootAddr }],
outputs: [{ address: 'bc1q...', value: 5000 }],
feeRate: 10,
});
const signedPsbt = wallet.signPsbt(psbt);
const txHex = wallet.extractTransaction(signedPsbt);
// Broadcast
const result = await provider.pushPsbt({ psbtBase64: signedPsbt });
console.log('TXID:', result.txId);
}
```
### Next Steps
- [Alkanes Commands](./alkanes): Protocol operations
- [Esplora Commands](./esplora): Block explorer queries
- [DataAPI Commands](./dataapi): High-level data queries
---
## Authentication
SUBFROST supports multiple authentication methods to suit different use cases.
### Authentication methods
#### 1. API key in path
The simplest method. Include your API key directly in the URL path:
```
https://mainnet.subfrost.io/v4/YOUR_API_KEY
```
Example:
```bash
curl -X POST https://mainnet.subfrost.io/v4/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"btc_getblockcount","params":[],"id":1}'
```
#### 2. API key header
Use the `x-subfrost-api-key` header for cleaner URLs:
```bash
curl -X POST https://mainnet.subfrost.io/v4/jsonrpc \
-H "Content-Type: application/json" \
-H "x-subfrost-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","method":"btc_getblockcount","params":[],"id":1}'
```
#### 3. CORS-based authentication
For browser-based applications, register your domain in the dashboard and make requests directly:
```javascript
// From your registered domain (e.g., https://yourapp.com)
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'btc_getblockcount',
params: [],
id: 1
})
});
```
The API verifies the `Origin` header against your registered domains.
#### 4. Alias routes
Create custom endpoint aliases for your applications:
```
https://api.subfrost.io/v4/YOUR_ALIAS
```
Aliases are configured in your dashboard and can be associated with specific rate limits and permissions.
### Endpoint paths
- **`/v4/`**: authenticate with API key in path.
- **`/v4/jsonrpc`**: use with header authentication.
- **`/v4/api`**: use with CORS authentication.
- **`/v4/`**: use with a custom alias.
### Getting an API key
1. Sign up or log in at [api.subfrost.io](https://api.subfrost.io/).
2. Navigate to your Dashboard.
3. Click "Create API Key".
4. Copy and securely store your key.
:::warning Security note
API keys grant access to your account's resources. Never expose them in client-side code or public repositories.
:::
### Managing CORS domains
1. Go to the Domains page in your dashboard.
2. Add your application's domain (e.g., `https://myapp.com`).
3. Verify domain ownership if required.
4. Once verified, requests from that origin are authenticated automatically.
### Best practices
#### Server-side applications
- Store API keys in environment variables.
- Use the header method for cleaner logs.
- Rotate keys periodically.
```javascript
// Node.js example
const API_KEY = process.env.SUBFROST_API_KEY;
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': API_KEY
},
body: JSON.stringify({ /* ... */ })
});
```
#### Client-side applications
- Use CORS authentication instead of embedding API keys.
- Register only your production domains.
- Consider using a backend proxy for additional security.
#### Rate limiting
Rate limit headers are included in responses:
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699123456
```
See [Rate Limits](../platform/rate-limits) for the full details on plans and quotas.
### Next steps
- [API Platform Overview](../platform/overview): manage your account.
- [JSON-RPC Methods](../json-rpc/overview): start making requests.
---
## Introduction
## SUBFROST API
Welcome to the SUBFROST API reference. This is the developer gateway to Bitcoin and the SUBFROST protocol.
### What is the SUBFROST API?
The SUBFROST API is a unified set of APIs for building on Bitcoin. It gives you a single endpoint for Bitcoin Core, Esplora, Ordinals, BRC-20, and Alkanes data, plus server-side scripting and high-level REST endpoints.
The API provides:
- **JSON-RPC Gateway.** Unified access to Bitcoin Core, Esplora, Ord, BRC-20, and Alkanes methods.
- **Lua Scripting.** Execute custom server-side scripts with full RPC access.
- **REST API.** High-level endpoints for Alkanes, BRC-20, pools, and other blockchain data.
- **Real-time Data.** Access to mempool, UTXOs, inscriptions, and runes.
- **Mempool (Preview).** A live view of the Bitcoin and Ethereum mempools over JSON-RPC and a sequenced WebSocket change stream. See [Mempool](../mempool/overview).
- **Orderbook (Beta).** REST snapshots and a WebSocket firehose over the off-chain PSBT order books. See [Orderbook](../orderbook/overview).
### API endpoints
JSON-RPC requests are made to:
```
https://mainnet.subfrost.io/v4/jsonrpc
```
Or with an API key in the path:
```
https://mainnet.subfrost.io/v4/YOUR_API_KEY
```
REST API endpoints use:
```
https://mainnet.subfrost.io/v4/api/
```
### Quick example
Get the current block height:
```json
{
"jsonrpc": "2.0",
"method": "btc_getblockcount",
"params": [],
"id": 1
}
```
Response:
```json
{
"jsonrpc": "2.0",
"result": 850000,
"id": 1
}
```
### Available namespaces
- **`esplora_*`**: Electrs/Esplora block explorer API
- **`ord_*`**: Ordinals protocol (inscriptions, runes, sats)
- **`metashrew_*`**: Metashrew indexer views
- **`alkanes_*`**: Alkanes protocol methods
- **`btc_*`**: Bitcoin Core RPC passthrough
- **`lua_*`**: server-side Lua script execution
- **`forex_*`**: fiat exchange rates, via the `/forex` endpoint ([Foreign Exchange](../json-rpc/forex))
- **`mempool_*`**: live mempool queries, via the `/mempool` and `/ethereum/mempool` endpoints ([Mempool JSON-RPC](../mempool/jsonrpc))
Some data is served from dedicated endpoints with their own indexers: the [BTC/USD pool](../json-rpc/btcusd) (`/btcusd`), [frUSD deposits on Ethereum](../json-rpc/frusd) (`/ethereum/frusd`), and the [Espo indexer](../json-rpc/espo) (`/espo`).
### Authentication
SUBFROST supports multiple authentication methods:
1. **API Keys.** Use the `/v4/` path or the `x-subfrost-api-key` header.
2. **CORS.** Register your domain for browser-based access.
3. **Alias Routes.** Use `/v4/` with your custom endpoint alias.
See the [Authentication](./authentication) guide for details.
### Next steps
- [Quickstart](./quickstart): get up and running in minutes.
- [JSON-RPC Overview](../json-rpc/overview): learn about the RPC interface.
- [Lua Scripting](../lua/overview): execute custom scripts server-side.
- [API Platform](../platform/overview): manage API keys and CORS settings.
---
## Quickstart(Getting-started)
Get started with the SUBFROST API in just a few minutes.
### Prerequisites
- A SUBFROST account (sign up at [api.subfrost.io](https://api.subfrost.io/)).
- An API key from your dashboard.
- Basic familiarity with JSON-RPC.
### Getting started
1. **Sign up.** Create an account at [api.subfrost.io](https://api.subfrost.io/).
2. **Verify email.** Confirm your email address.
3. **Create API key.** Generate your first API key from your dashboard.
4. **Start building.** Make your first API request using the examples below.
### Networks
Choose the network for your application:
- **Mainnet:** `https://mainnet.subfrost.io`
- **Signet:** `https://signet.subfrost.io`
- **Regtest:** `https://regtest.subfrost.io`
### Making your first request
#### Using cURL
```bash
curl -X POST https://mainnet.subfrost.io/v4/jsonrpc \
-H "Content-Type: application/json" \
-H "x-subfrost-api-key: YOUR_API_KEY" \
-d '{
"jsonrpc": "2.0",
"method": "btc_getblockcount",
"params": [],
"id": 1
}'
```
#### Using JavaScript
```javascript
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'btc_getblockcount',
params: [],
id: 1
})
});
const data = await response.json();
console.log('Block height:', data.result);
```
#### Using Python
```python
import requests
response = requests.post(
'https://mainnet.subfrost.io/v4/jsonrpc',
headers={
'Content-Type': 'application/json',
'x-subfrost-api-key': 'YOUR_API_KEY'
},
json={
'jsonrpc': '2.0',
'method': 'btc_getblockcount',
'params': [],
'id': 1
}
)
data = response.json()
print(f"Block height: {data['result']}")
```
### Common operations
A few methods to try next. See the [JSON-RPC Reference](../json-rpc/overview) for full details.
| Goal | Method | Reference |
| --- | --- | --- |
| Get a Bitcoin block height | `btc_getblockcount` | [Bitcoin Core RPC](../json-rpc/bitcoind) |
| Get a block hash by height | `btc_getblockhash` | [Bitcoin Core RPC](../json-rpc/bitcoind) |
| Get all UTXOs for an address | `esplora_address::utxo` | [esplora_* Methods](../json-rpc/esplora) |
| Get current fee estimates | `esplora_fee-estimates` | [esplora_* Methods](../json-rpc/esplora) |
| Get inscription details | `ord_inscription` | [ord_* Methods](../json-rpc/ord) |
| Get rune information | `ord_rune` | [ord_* Methods](../json-rpc/ord) |
| Run a custom server-side script | `lua_evalscript` | [Lua Scripting](../lua/overview) |
### Error handling
The API returns standard JSON-RPC error responses:
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": 1
}
```
Common error codes:
- **-32700**: Parse error (invalid JSON)
- **-32600**: Invalid request (malformed JSON-RPC)
- **-32601**: Method not found
- **-32602**: Invalid params
- **-32603**: Internal error
### Next steps
- [Authentication](./authentication): set up API keys and CORS.
- [JSON-RPC Reference](../json-rpc/overview): full method documentation.
- [Lua Scripting](../lua/overview): custom server-side scripts.
---
## Reading Alkane Metadata
Every Alkanes contract exports a `__meta` wasm function that returns its own
ABI (method names, opcodes, parameter shapes, return types) as JSON.
The Metashrew `meta` view function calls this export and returns the
result, so you can ask any deployed alkane "what methods do you have?"
without having to read source code or guess opcodes.
This is the same primitive the `alkanes-cli alkanes inspect`, `alkanes-cli alkanes meta`, and `@alkanes/ts-sdk`'s `provider.alkanes.getMeta()` use
under the hood.
### How the `__meta` export works
When you build an alkane with the `declare_alkane!` macro, the runtime
generates a `__meta` wasm export that walks the `MessageDispatch` enum
attached to your contract and emits a JSON blob:
```json
{
"contract": "DIESEL",
"methods": [
{ "name": "initialize", "opcode": 0, "params": [], "returns": "void" },
{ "name": "mint", "opcode": 77, "params": [], "returns": "void" },
{ "name": "get_name", "opcode": 99, "params": [], "returns": "String" },
{ "name": "get_symbol", "opcode": 100, "params": [], "returns": "String" },
{ "name": "get_total_supply", "opcode": 101, "params": [], "returns": "u128" }
]
}
```
Each method tells you:
- **name**: human-readable method name from the `MessageDispatch` enum variant
- **opcode**: the u128 value the calldata varint list must start with
- **params**: input arguments (name + type) in order
- **returns**: the type of the response data
This is the contract's source of truth, there's no separate registry.
If a contract's deployed wasm changes its dispatch, the metadata changes
with it automatically.
### Calling from JSON-RPC
The Metashrew `meta` view function does this for you. The single
argument is the wasm calldata produced by enciphering the alkane id
target (no opcode, the view function knows it's calling `__meta`):
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": [
"meta",
"0x2a020200",
"latest"
],
"id": 1
}
```
The hex argument `0x2a020200` is the protobuf-encoded
`MessageContextParcel` with `calldata = [2, 0]` (target alkane 2:0,
the DIESEL contract). Hand-rolling that hex is painful, both
SDKs below build it for you from `"block:tx"` strings.
The response is the contract's ABI as a hex-encoded JSON string:
```json
{
"jsonrpc": "2.0",
"result": "0x7b22636f6e7472616374223a2244...",
"id": 1
}
```
Decode the hex, you get the JSON blob above.
### TypeScript / `@alkanes/ts-sdk`
```typescript
import { AlkanesProvider } from '@alkanes/ts-sdk';
const provider = new AlkanesProvider({
jsonRpcUrl: 'https://mainnet.subfrost.io/jsonrpc',
});
// 1) High-level helper, `getMeta` does the encoding + hex-decode for you.
const meta = await provider.alkanes.getMeta('2:0');
console.log(meta);
// { contract: "DIESEL", methods: [...] }
// 2) Walk the methods to discover the opcode for a name.
const mintOpcode = meta.methods.find((m) => m.name === 'mint')?.opcode;
console.log(`mint opcode is ${mintOpcode}`); // 77
// 3) Use it in a simulate call (read-only contract evaluation).
const supplyResult = await provider.alkanes.simulate('2:0', {
alkanes: [],
transaction: [],
block: [],
height: 950000,
vout: 0,
txindex: 0,
calldata: [101], // opcode 101 = get_total_supply
pointer: 0,
refund_pointer: 0,
});
console.log('totalSupply (LE u128 hex):', supplyResult.execution.data);
```
For historical metadata (e.g. inspect what the contract looked like at
height 940000 before an upgrade):
```typescript
const oldMeta = await provider.alkanes.getMeta('2:0', '940000');
```
### Rust
If you're building tooling outside the TS SDK, you can call the
view function with `alkanes-cli-common::provider::ConcreteProvider`
or roll your own:
```rust
use alkanes_cli_common::provider::{ConcreteProvider, Provider};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let provider = ConcreteProvider::new(
"https://mainnet.subfrost.io/jsonrpc".to_string(),
None, // bitcoin rpc not needed for view calls
)?;
// getMeta returns the raw JSON bytes from `__meta`.
let meta_bytes = provider.alkanes_meta("2:0", Some("latest")).await?;
let meta_str = String::from_utf8(meta_bytes)?;
let meta: serde_json::Value = serde_json::from_str(&meta_str)?;
println!("contract: {}", meta["contract"]);
for method in meta["methods"].as_array().unwrap() {
println!(
" opcode {:>4} {:<30} returns {}",
method["opcode"], method["name"], method["returns"]
);
}
Ok(())
}
```
`Cargo.toml`:
```toml
[dependencies]
alkanes-cli-common = { git = "https://github.com/kungfuflex/alkanes-rs", branch = "kungfuflex/v3.0.0" }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
serde_json = "1"
```
### From the bytecode directly (offline)
When you have an alkane's wasm bytes locally and don't want to round-trip
through an indexer, you can call `__meta` against the wasm directly
using `alkanes-cli alkanes inspect`:
```bash
alkanes-cli alkanes inspect --meta
```
Programmatically the same primitive is available via
`@alkanes/ts-sdk`'s `inspectBytecode`:
```typescript
const bytecode = await provider.alkanes.getBytecode('2:0');
const inspection = await provider.alkanes.inspectBytecode(bytecode, '2:0', {
meta: true,
});
console.log(inspection.metadata); // same shape as getMeta()
```
This is the inspection path the CLI uses, useful when you have a
binary you haven't deployed yet, or want to verify that what's deployed
matches what you compiled.
### Why `__meta` over `alkanes_meta`
The `alkanes_meta` JSON-RPC convenience method returns a different,
narrower shape (`{name, symbol, decimals, totalSupply}`) that assumes
the contract follows token conventions. Many alkanes don't (AMM pools,
factories, DAO modules), for those, `metashrew_view "meta"` is the
only way to discover what they can actually do.
See also: [metashrew_* Methods](../json-rpc/metashrew) for the
broader set of view functions.
---
## SUBFROST WalletConnect
SUBFROST WalletConnect lets a web app (the dapp) request Bitcoin signatures from the SUBFROST wallet on the user's phone. It is **not** WalletConnect v2. The two sides meet on a WebSocket rendezvous service, the pair bridge, at `wss://pair.subfrost.io/v1/pair`. The bridge forwards bytes between them. Everything after the key exchange is encrypted with ChaCha20-Poly1305 under a key the bridge cannot derive.
This page is written as a protocol specification. The reference client is not on the public npm registry yet, so everything an implementer has to put on the wire is described here: the pairing URI, the bridge frames, the key derivation, the framing, the envelope and the messages.
### Roles
- **Dapp: LISTEN.** The dapp creates the pairing. It generates an ephemeral X25519 keypair, a peer name and a pairing code, builds the pairing URI, registers its peer name on the bridge with a `listen` frame, and shows the URI as a QR code (or hands it over as a deep link). Once the wallet is connected, the dapp sends requests.
- **Wallet: DIAL.** The wallet reads the URI, connects to the bridge named in it, dials the dapp's peer name with a `dial` frame, sends its own X25519 public key, and answers requests after the user approves them.
```
Dapp (browser) Pair bridge Wallet (phone)
LISTEN wss://pair.subfrost.io/v1/pair DIAL
| | |
|--- {"op":"listen",...} -------->| |
|<-------- {"event":"ready"} -----| |
| | |
| QR / deep link: subfrost://wc/?key=...&code=...&mode=cli |
|.................... out of band, never via the bridge ............>|
| |<------ {"op":"dial",...} ---------|
| |------- {"event":"dialed",...} --->|
|<-- {"event":"incoming",...} ----| |
| | |
|<========== binary frames, forwarded verbatim by the bridge ========>|
| 1. wallet X25519 public key (first binary frame, from wallet) |
| 2. encrypted request and response envelopes |
```
### Pairing
#### Sequence
1. The dapp generates a fresh X25519 keypair (32 random bytes as the private key), a peer name (see [Peer name](#peer-name)) and a 6-character pairing code.
2. The dapp opens a WebSocket to the bridge, sends a `listen` frame for its peer name, and waits for `{"event":"ready"}`. The bridge can route a dial to the dapp only after the listen is registered, which `ready` confirms.
3. The dapp shows the pairing URI as a QR code, with the pairing code next to it.
4. The wallet parses the URI, opens a WebSocket to the `bridge` URL from the URI, and sends a `dial` frame for the dapp's peer name.
5. The bridge sends `{"event":"incoming",...}` to the dapp. From here on the socket carries binary frames only.
6. The wallet's first binary frame is its X25519 public key. Both sides derive the symmetric key (see [Key agreement](#key-agreement)).
7. The dapp sends encrypted requests. The wallet sends one encrypted response per request.
#### Pairing URI
```
subfrost://wc/?key=&code=&bridge=&origin=&mode=cli
```
The reference client emits the parameters in this order.
| Component | Value | Encoding |
| --- | --- | --- |
| `` (path) | The dapp's peer name, `fr1<...>.peer`, including the `.peer` suffix. | Verbatim. Do not case-fold it, strip the suffix, or percent-encode it: the exact string is also part of the key derivation, and both sides must hash the same bytes. |
| `key` | The dapp's X25519 public key, 32 bytes. | base64url without padding (43 characters). |
| `code` | The 6-character pairing code. | Verbatim. Every character in the alphabet is URL-safe. |
| `bridge` | The WebSocket URL of the bridge the dapp is listening on. The default is `wss://pair.subfrost.io/v1/pair`. | Percent-encoded (`encodeURIComponent`). |
| `origin` | The dapp's origin, for example `https://app.example.com`. The reference client defaults to `window.location.origin`. | Percent-encoded (`encodeURIComponent`). |
| `mode` | Always the literal `cli`. It marks a pair-bridge link. Links from the deprecated relay carried `relay=` and no `mode` parameter. | Literal. |
Example:
```
subfrost://wc/fr1dpc7mpp8ae8g364ssg82p0xz2pq8ke0zd6sgmlxs3r2a8l9wkt7qz4kznt.peer?key=WXmajmshySK07ZrZr-X3o1FPATQZWKF5z9CcGF1TLSM&code=K7QX2M&bridge=wss%3A%2F%2Fpair.subfrost.io%2Fv1%2Fpair&origin=https%3A%2F%2Fapp.example.com&mode=cli
```
#### Peer name
A peer name is the routing label the bridge uses to match a dialer with a listener. Both the dapp (for `listen`) and the wallet (for `self_peer` in `dial`) need one. It is derived from 32 random bytes:
1. `secret` = 32 bytes from a CSPRNG.
2. `fp` = BLAKE3(`secret`), the default 32-byte digest.
3. `name` = Bech32m encoding of `fp` with human-readable part `fr` (the 32 bytes are converted to 5-bit words as usual for Bech32). This yields 61 characters starting with `fr1`.
4. `peer` = `name` + `.peer` (66 characters in total).
The secret is never sent anywhere. Only the derived name goes on the wire, and it is the same string in the URI path, in the `listen` frame, and in the key derivation.
Test vectors (secret shown as base64url):
| Secret | Peer name |
| --- | --- |
| 32 bytes of `0x55` (`VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU`) | `fr1dpc7mpp8ae8g364ssg82p0xz2pq8ke0zd6sgmlxs3r2a8l9wkt7qz4kznt.peer` |
| 32 bytes of `0x00` (`AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) | `fr19tdg8svpnffh9khpyw8urhk3y0ypqn7659vx92hwd9pg5xpqlndqh0x68c.peer` |
| bytes `0x00` to `0x1f` (`AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8`) | `fr1u55wj4ucqd7lgyz58k0nrcukanw5trt3k9tavq2rnzawxta4d3js03clqq.peer` |
#### Pairing code
- **Format.** 6 characters drawn from the 31-character alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789`, which leaves out the ambiguous glyphs `O`, `I`, `L`, `0` and `1`.
- **Generation.** The dapp generates it with a CSPRNG, one random byte per character, rejecting bytes that would bias the result (bytes at or above the largest multiple of 31 that fits in 256).
- **Who shows it, who types it.** The code travels to the wallet inside the URI (`code=`), so nobody has to type it in the reference flow. The dapp should also display it next to the QR code so the user can confirm it visually.
- **What it gates.** The code is mixed into the key derivation. A wallet that has the wrong code, or none, derives a different key, and the first encrypted frame fails its authentication tag. The code never passes through the bridge.
### Bridge control frames
The bridge speaks WebSocket over TLS. There are two kinds of WebSocket messages:
- **Text messages** carry control frames: one JSON object per message, no trailing newline. They are used only during the handshake.
- **Binary messages** carry application data after the handshake. The bridge forwards each one verbatim. One binary WebSocket message is one frame; the bridge adds no length prefix of its own and preserves message boundaries.
After the handshake, ignore any text message.
#### Client frames
Client frames use the key `op` as the discriminator.
| Frame | JSON | Sent by |
| --- | --- | --- |
| listen | `{"op":"listen","peer":""}` | Dapp |
| dial | `{"op":"dial","peer":"","self_peer":""}` | Wallet |
`self_peer` is snake_case on the wire. Every peer name includes the `.peer` suffix.
#### Server frames
Server frames use the key `event` as the discriminator (not `op`).
| Event | JSON | Meaning |
| --- | --- | --- |
| `ready` | `{"event":"ready"}` | Sent to a listener: the listen is registered. |
| `dialed` | `{"event":"dialed","peer":"..."}` | Sent to a dialer: the dial reached a listener. |
| `incoming` | `{"event":"incoming","peer":"..."}` | Sent to a listener: a peer dialed in. `peer` names the dialer. |
| `error` | `{"event":"error","code":"...","msg":"..."}` | The request failed. `msg` is human-readable. Treat it as the end of that pairing and ask the user to scan a fresh QR code. |
The wallet's first binary frame can arrive immediately after `incoming`. A listener should buffer binary messages from the moment the socket opens, so that frame is not lost while it switches from handshake handling to the data phase.
### Key agreement
The dapp's public key travels in the URI (`key=`). The wallet's public key is the payload of the wallet's first binary frame, as 43 ASCII characters of base64url without padding (32 bytes when decoded). The reference client trims surrounding whitespace before decoding and rejects any key that does not decode to exactly 32 bytes.
Constants, verbatim from the reference client:
```ts
export const KEY_LEN = 32;
export const NONCE_LEN = 12;
const HKDF_SALT = new TextEncoder().encode('subfrost-wc-v1');
```
Derivation:
```
shared = X25519(own_private_key, other_side_public_key) // 32 bytes
info = UTF-8 bytes of + ":" +
sym_key = HKDF-SHA256(ikm = shared, salt = "subfrost-wc-v1", info = info, length = 32)
```
- The dapp computes `X25519(dapp_private, wallet_public)`. The wallet computes `X25519(wallet_private, dapp_public)`. Both give the same shared secret.
- The peer name in `info` is the dapp's peer name, character for character as it appears in the URI path, with the `.peer` suffix. For the first test vector above and code `K7QX2M`, `info` is `fr1dpc7mpp8ae8g364ssg82p0xz2pq8ke0zd6sgmlxs3r2a8l9wkt7qz4kznt.peer:K7QX2M`.
- The keypairs are ephemeral: new for every pairing, held in memory only.
### Framing after the handshake
The wallet always sends first (its public key), and the dapp does not send any binary frame before it receives that one. The dapp reads the wire mode from the first byte of the wallet's first binary frame, then uses the same mode for everything it sends. The three modes have disjoint first bytes, so there is no negotiation:
| First byte | Mode |
| --- | --- |
| `0x01`, `0x02`, `0x03` | Typed frames (DATA, PING, PONG). This is what the shipped SUBFROST wallet sends. |
| `0x10`, `0x11`, `0x12` | Reserved for a reconnect-capable codec. New implementations should use typed frames. |
| anything else | Bare frames with no header, from older wallets. |
The reference client only settles on typed mode if the first frame also decodes cleanly as a typed frame. A first frame that decodes as neither a typed frame nor a reserved-range frame is treated as bare.
#### Typed frames (DATA, PING, PONG)
```
offset size field
0 1 type 0x01 DATA, 0x02 PING, 0x03 PONG
1 4 length payload length, unsigned 32-bit, big-endian
5 n payload exactly `length` bytes
```
- **One frame per binary WebSocket message.** The message must be exactly `5 + length` bytes. A message that is shorter than 5 bytes, has an unknown type byte, or whose length field disagrees with its size is malformed. The reference client drops malformed frames without closing the stream.
- **DATA (`0x01`)** carries one application payload: first the wallet's public key, then the JSON envelopes. The receiver strips the 5-byte header.
- **PING (`0x02`)** is a reachability probe. The receiver answers with a PONG carrying the same payload.
- **PONG (`0x03`)** answers a PING. A PONG echoes the PING payload, so the sender can match it by payload.
Example: the wallet's first frame, a DATA frame whose payload is its 43-character public key:
```
01 00 00 00 2b 57 58 6d 61 ... // 0x01 DATA, length 0x2b = 43, then "WXma..."
```
#### Bare frames
A bare frame is the application payload with no header. It exists for compatibility with older wallets. New implementations should use typed frames.
### Encrypted envelope
Every request and every response is one application payload (a DATA payload in typed mode): the UTF-8 bytes of this JSON object.
```json
{
"ciphertextB64": "",
"nonceB64": ""
}
```
- `ciphertextB64` is the ChaCha20-Poly1305 output under `sym_key`: the ciphertext with the 16-byte Poly1305 tag appended.
- `nonceB64` is the 12-byte nonce. Each message gets a fresh random nonce from a CSPRNG. Never reuse a nonce with the same key.
- No associated data (AAD) is used.
- The plaintext is the UTF-8 bytes of one JSON message from the next section.
The key names are camelCase (`ciphertextB64`, `nonceB64`). The deprecated relay used `ciphertext` and `nonce`; those names are not accepted on the pair bridge path.
While waiting for a response, the reference client skips any frame that does not parse as an envelope, does not decrypt, or does not carry the `request_id` it is waiting for.
### Plaintext messages
Every plaintext is a JSON object tagged by `type` (snake_case). Requests go from dapp to wallet, responses from wallet to dapp.
```ts
type Plaintext =
// requests (dapp to wallet)
| { type: 'get_accounts'; request_id: string; origin: string }
| { type: 'sign_message'; message: string; address: string; request_id: string; origin: string;
protocol?: string }
| { type: 'sign_psbt'; psbt_hex: string; addresses: string[]; request_id: string; origin: string }
// responses (wallet to dapp)
| { type: 'accounts'; request_id: string; addresses: string[] }
| { type: 'result'; request_id: string; result: string }
| { type: 'error'; request_id: string;
code: 'user_rejected' | 'permission_denied' | 'internal' | string; message: string };
```
#### Common fields
| Field | Type | Notes |
| --- | --- | --- |
| `request_id` | string | Chosen by the dapp; the reference client uses a random UUID v4 (`crypto.randomUUID()`). The response carries the same value. |
| `origin` | string | The dapp's origin, repeated in every request. |
#### `get_accounts`
Asks for the wallet's addresses. Success response: `accounts`, with `addresses: string[]`.
```json
{"type":"get_accounts","request_id":"6f1c0a52-3c1e-4f0e-9d7a-2b5e8c1d4a90","origin":"https://app.example.com"}
{"type":"accounts","request_id":"6f1c0a52-3c1e-4f0e-9d7a-2b5e8c1d4a90","addresses":["bc1p..."]}
```
#### `sign_message`
| Field | Type | Notes |
| --- | --- | --- |
| `message` | string | The message to sign. |
| `address` | string | The address to sign with. |
| `protocol` | string, optional | The signature shape, for example `bip322-simple` or `bip137`. When the key is absent, the wallet applies its own default. A dapp that derives keys or identities from the signature bytes should pin it rather than inherit the default. The reference client leaves the key out entirely when no protocol is given (it does not send `null`). |
Success response: `result`, where `result` is the signature string.
```json
{"type":"sign_message","message":"vote:42","address":"bc1p...","protocol":"bip322-simple","request_id":"...","origin":"https://app.example.com"}
{"type":"result","request_id":"...","result":""}
```
#### `sign_psbt`
| Field | Type | Notes |
| --- | --- | --- |
| `psbt_hex` | string | The unsigned PSBT as hex. The reference client strips a leading `0x` before sending. |
| `addresses` | string[] | Sent as an empty array unless the caller supplies addresses. |
Success response: `result`, a string holding the signed PSBT as the wallet returns it. The reference client passes it through without decoding it.
#### `error`
Any request can be answered with `error` instead of a success response.
The defined `code` values are `user_rejected` (the user declined on the phone), `permission_denied` and `internal`. The field is an open string, so treat any other value as a generic failure. `message` is human-readable.
#### One request at a time
The channel has no multiplexing. The reference client sends one request, waits up to 5 minutes for the matching response (the user may be approving on the phone), and only then sends the next. A response whose `request_id` does not match, for example a late answer to a request that already timed out, is dropped.
### Liveness and reconnect
#### Idle sockets
The bridge drops a socket that stays idle for about two minutes. Once paired, send a PING (type `0x02`) at least every 60 seconds to keep the session open; the other side answers with a PONG. The reference client does not send PINGs; it relies on the reconnect below instead.
While the dapp is still waiting for the wallet to dial, there is no peer to ping. If the listen socket closes in that phase, the reference client opens a new one and sends the same `listen` frame under the same peer name, so the QR code on screen stays valid. It keeps doing so until the dial timeout expires (5 minutes by default) or the pairing is cancelled.
#### Reconnect after pairing
When the socket drops after pairing, the reference client keeps the pairing (keypair, symmetric key, peer name, pairing code) and reconnects:
- **Re-attach.** Open a new socket, send the identical `listen` frame, and wait only for `ready`. An `incoming` may not be sent again on a re-attach, and the data phase ignores text frames.
- **Backoff.** Retry with exponential backoff and jitter, and give up after a few minutes of continuous disconnection. In a browser, retry right away when the page becomes visible again or the network comes back.
- **Resend.** Send again any frame that could not be written while the socket was down. A request sent across a drop can be lost, so a request that gets no answer should be retried by the user.
Nothing is persisted on the dapp side. Closing the tab or process ends the pairing, and the user pairs again.
### Reference client
:::note
The reference client `@subfrost/wc` is not published to npm yet. Until it is, implement the protocol on this page, or ask the SUBFROST team for the package.
:::
The reference client implements the dapp (LISTEN) side in TypeScript. It depends on the `@noble` crypto libraries and `@scure/base`, plus vendored WebAssembly modules (so the bundler must handle `.wasm` imports), and it runs in the browser, in Node (with a `WebSocket` global) and in React Native.
```ts
import { connect } from '@subfrost/wc';
const { pairingUri, pairingCode, accepted, cancel } = connect({
origin: window.location.origin,
// bridgeUrl: 'wss://pair.subfrost.io/v1/pair', // default
// incomingTimeoutMs: 5 * 60_000, // how long to wait for the dial
// onTransportState: (state, detail) => { ... }, // 'open' | 'retrying' | 'closed'
});
// Render pairingUri as a QR code and show pairingCode next to it.
// cancel() aborts the pairing and closes the socket.
const session = await accepted; // wallet dialed in, key derived
const [addr] = await session.getAccounts();
// BIP-322 signature, with the shape pinned instead of left to the wallet default
const sig = await session.signMessage('vote:42', addr, 'bip322-simple');
// Sign a PSBT given as hex; the second argument (addresses) is optional
const signed = await session.signPsbt(unsignedPsbtHex, [addr]);
await session.disconnect();
```
When the wallet answers with an `error` message, the session methods reject with an `Error` whose message is `wc:`, for example `wc:user_rejected ...`. A request that gets no answer within 5 minutes, or whose connection gives up reconnecting, rejects with an `Error` that is not a wallet `error` message; do not parse its text. Match `wc:user_rejected` for a refusal and treat any other rejection as a generic failure.
### Security model
**What the bridge sees.** Peer names (from `listen` and `dial`), connection timing and frame sizes, the wallet's X25519 public key (the first binary frame is not encrypted), and the encrypted envelopes.
**What the bridge does not see.** The pairing URI travels by QR code or deep link, never through the bridge, so the bridge never learns the dapp's public key, the pairing code or the origin. It holds neither private key, so it cannot compute the shared secret, and it cannot read requests or responses or alter an envelope without failing the Poly1305 tag. Substituting its own public key for the wallet's does not help either: the dapp would derive a key from it, but reproducing that key needs the dapp's public key and the pairing code, which the bridge never sees.
**Treat the pairing URI as a secret.** Anyone who holds the URI can dial the listening dapp and play the wallet. Show the QR code only to the user, and do not log or share the deep link.
**Origin.** The dapp names its origin in the URI, and every request repeats it inside the encrypted payload, so the bridge cannot change it in transit. It tells the wallet which site is asking. It is asserted by whoever built the URI, though: the protocol does not verify it against the real web origin, so a malicious page can claim any origin in a URI it builds itself. A wallet should present it as the site's claim, not as a verified identity.
**Pairing code.** The code is part of the key derivation, so only a wallet that read it from the URI can decrypt anything. The dapp shows it next to the QR code so the user can confirm it visually.
**Approval on the phone.** Signing requests wait for the user to approve them on the phone. A refusal comes back as an `error` with code `user_rejected`. The reference client gives each request 5 minutes before it gives up.
**Ephemeral keys.** Every pairing uses a fresh X25519 keypair and a fresh peer name. On the dapp side they live in memory only.
### Changes from the previous protocol
The classic relay at `wss://wc.subfrost.io`, with its HTTP session endpoints and FCM wake pushes to the phone, is deprecated. It stays up only for older wallet builds; current SUBFROST wallet builds pair through the pair bridge and do not use it. Integrations built on the relay must move to the pair bridge described on this page.
| | Deprecated relay | Pair bridge |
| --- | --- | --- |
| Pairing URI | `subfrost://wc/?key=&relay=&origin=` | `subfrost://wc/?key=&code=&bridge=&origin=&mode=cli` |
| Rendezvous | Random UUID topic on the relay | `fr1<...>.peer` name, dapp LISTENs and wallet DIALs |
| HKDF info | The topic | `:` |
| HKDF salt | `subfrost-wc-v1` | `subfrost-wc-v1` (unchanged) |
| Envelope keys | `ciphertext`, `nonce` | `ciphertextB64`, `nonceB64` |
| Transport | Relay HTTP endpoints and WebSocket, push wake-up | One WebSocket per side through the bridge, framed binary messages |
The plaintext message types (`get_accounts`, `sign_message`, `sign_psbt`, `accounts`, `result`, `error`) are the same on both carriers.
---
## alkanes_* Methods
> **Prefer `metashrew_view`.** The `alkanes_*` JSON-RPC methods are
> convenience wrappers and may diverge from canonical `metashrew_view`
> semantics over time. New integrations should call
> [`metashrew_view`](./metashrew) directly with the
> appropriate view function name. The convenience methods below
> remain available for ergonomic reasons; the mapping is documented
> per-method.
The `alkanes_*` namespace provides access to the Alkanes protocol, a metaprotocol smart contract system built on Bitcoin. Alkanes is a variant of protorunes, and therefore a subprotocol of the runes protocol on Bitcoin.
### Overview
Alkanes are smart contracts that run on Bitcoin, indexed via Metashrew. Each `alkanes_*` method is a thin convenience wrapper that:
1. Encodes the input arguments into a `MessageContextParcel` protobuf,
2. Calls `metashrew_view` with the matching view function name,
3. Decodes the result.
The first two columns of the table below are the canonical
`metashrew_view` mapping, call them directly for stable behavior and
broader feature coverage. See
[Reading Alkane Metadata](../guides/alkane-metadata) for a
worked example of the `meta` view function.
| Convenience method | `metashrew_view` function | Notes |
|----------------------------------|---------------------------|-------|
| `alkanes_protorunesbyaddress` | `protorunesbyaddress` | DEPRECATED in alkanes v3.0, drop and use the esplora UTXO API + `protorunesbyoutpoint` on each utxo. |
| `alkanes_getbytecode` | `getbytecode` | |
| `alkanes_simulate` | `simulate` | Lower-level than the convenience method, use this for any read-only contract evaluation. |
| `alkanes_meta` | `meta` | The `metashrew_view "meta"` form returns the full ABI from the contract's `__meta` wasm export. The convenience `alkanes_meta` returns a narrower token-shape and SHOULD NOT be used for non-token alkanes (AMM pools, factories, etc.). |
| `alkanes_trace` | `trace` | |
| `alkanes_traceblock` | `traceblock` | |
| (none, call `metashrew_view`) | `simulateprotostones` | Traced protostone simulation. See [Simulation view functions](#simulation-view-functions-with-trace). |
| (none, call `metashrew_view`) | `simulatetransaction` | Traced transaction simulation (raw tx or PSBT). See below. |
| (none, call `metashrew_view`) | `simulateblock` | Traced whole-block simulation. See below. |
### Methods
#### alkanes_protorunesbyaddress
Get all alkane tokens held by an address.
**Parameters:**
- **0** (object): Request object with `address` and `protocolTag`
- **1** (string): Block tag (optional, default: "latest")
**Request Object:**
```json
{
"address": "bc1q...",
"protocolTag": "1"
}
```
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "alkanes_protorunesbyaddress",
"params": [
{
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"protocolTag": "1"
}
],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"outpoints": [
{
"outpoint": {
"txid": "abc123def456...",
"vout": 0
},
"runes": [
{
"id": { "block": 840000, "tx": 1 },
"amount": "1000000000"
}
]
}
]
},
"id": 1
}
```
**Lua Example:**
```lua
local address = args[1]
local result = _RPC.alkanes_protorunesbyaddress({
address = address,
protocolTag = "1"
})
return { result_type = type(result) }
```
---
#### alkanes_getbytecode
Get the bytecode of an alkane contract.
**Parameters:**
- **0** (object): Alkane ID with `block` and `tx`
- **1** (string): Block tag (optional)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "alkanes_getbytecode",
"params": [
{ "block": 840000, "tx": 1 }
],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"bytecode": "0061736d01000000..."
},
"id": 1
}
```
> **Note:** This method is available via JSON-RPC. Lua support may vary.
---
#### alkanes_simulate
Simulate an alkane transaction.
**Parameters:**
- **0** (object): Simulation request
- **1** (string): Block tag (optional)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "alkanes_simulate",
"params": [
{
"alkaneId": { "block": 840000, "tx": 1 },
"inputs": ["0x..."],
"target": { "block": 840000, "tx": 2 },
"pointer": 0,
"refundPointer": 0,
"vout": 0,
"data": "0x..."
}
],
"id": 1
}
```
---
#### alkanes_meta
Get token-shaped metadata about an alkane contract.
> **Use `metashrew_view "meta"` instead** for any non-token alkane (AMM
> pool, factory, DAO module). The `alkanes_meta` convenience method
> returns a narrower `{name, symbol, decimals, totalSupply}` shape that
> assumes token semantics; the canonical `meta` view function returns
> the contract's full `__meta` ABI export: names, opcodes, parameter
> shapes, return types. See
> [Reading Alkane Metadata](../guides/alkane-metadata).
**Parameters:**
- **0** (object): Alkane ID with `block` and `tx`
- **1** (string): Block tag (optional)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "alkanes_meta",
"params": [
{ "block": 840000, "tx": 1 }
],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"name": "Example Token",
"symbol": "EXT",
"decimals": 8,
"totalSupply": "21000000000000000"
},
"id": 1
}
```
> **Note:** This method is available via JSON-RPC. Lua support may vary.
---
### Simulation view functions (with trace)
Beyond the single `alkanes_simulate` / `simulate` pair above, the indexer exposes three richer simulation view functions, available in the current release line (alkanes-rs `v2.2.1-rc.5`, running under metashrew `v9.0.5-rc.13`). Unlike `simulate`, which returns only the final result, these return a full execution **trace** for every protostone, the fuel used, the storage slots touched, and the final balances per output. They run read-only against indexed state, so you can preview exactly what a transaction or block would do before broadcasting it. They replace the older, slower preview path.
Invoke them through [`metashrew_view`](./metashrew) by name, with a protobuf-encoded request (hex) and a block tag; the response is a protobuf-encoded result (hex):
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": ["simulateblock", "0x", "latest"],
"id": 1
}
```
All three accept an optional `storage_overrides` field: a list of storage slots (keyed by alkane id) to override before running, for what-if simulation.
#### simulateprotostones
Simulate one or more protostones directly, without needing a fully formed transaction.
Request (`SimulateProtostonesRequest`):
| Field | Type | Notes |
|---|---|---|
| `height` | u64 | Block height to simulate at. |
| `alkane_inputs` | AlkaneTransfer[] | Incoming alkanes the first protostone sees. |
| `protostones` | bytes | Enciphered protostones (the same bytes a runestone's protocol field carries). |
| `transaction` | bytes | Optional. A caller-supplied transaction to carry the protostones; if omitted, one is synthesized. |
| `block` | bytes | Optional. A caller-supplied block; if omitted, a minimal single-transaction block is synthesized. |
| `storage_overrides` | list | Optional what-if storage overrides (see above). |
#### simulatetransaction
Simulate a complete transaction. The `transaction` field accepts either a raw consensus-encoded transaction or a PSBT (PSBT is tried first, since it is the common mobile-wallet input). The transaction runs inside an auto-synthesized block whose header is chained off the previous indexed block.
Request (`SimulateTransactionRequest`):
| Field | Type | Notes |
|---|---|---|
| `height` | u64 | Block height to simulate at. |
| `transaction` | bytes | Raw transaction or PSBT. |
| `storage_overrides` | list | Optional what-if storage overrides. |
#### simulateblock
Simulate an entire block of transactions in order. This is what powers evaluating a set of mempool transactions together, in a block you assemble yourself.
Request (`SimulateBlockRequest`):
| Field | Type | Notes |
|---|---|---|
| `height` | u64 | Block height to simulate at. |
| `block` | bytes | Consensus-encoded block. |
| `storage_overrides` | list | Optional what-if storage overrides. |
Response (`SimulateBlockResponse`): `block_hash`, `height`, `total_fuel_used`, and `txs`, which holds one entry per transaction in block order. Coinbase and no-runestone transactions surface as empty entries with `error = "no_runestone"`, so `txs[i]` stays aligned with the block's `txdata[i]`.
#### Per-transaction response shape
`simulateprotostones` and `simulatetransaction` return one per-transaction result; `simulateblock` returns one per transaction. Each result has:
| Field | Type | Notes |
|---|---|---|
| `txid` | string | |
| `height` | u64 | |
| `protostones` | ProtostoneExecution[] | Per protostone: `index`, the synthetic `outpoint` (txid plus shadow vout), the execution `trace`, `fuel_used`, and `touched_storage` (final value of every storage slot the protostone wrote, keyed by the owning alkane). |
| `final_balances_by_vout` | VoutBalances[] | Final alkane balances at each output. |
| `total_fuel_used` | u64 | |
| `used_transaction` | bytes | The exact transaction that was run. |
| `used_block` | bytes | The exact block that was run (synthesized when not supplied). |
| `error` | string | Empty on success. |
---
### Alkane Identifiers
Alkanes are identified by their etching location:
```json
{ "block": 840000, "tx": 1 }
```
This represents:
- **block**: The block height where the alkane was created
- **tx**: The transaction index within that block
String format: `840000:1`
---
### Protocol Tag
For Alkanes, the protocol tag is always `"1"`:
```json
{ "protocolTag": "1" }
```
---
:::note[JSON convenience form vs raw metashrew_view]
The gateway exposes one JSON convenience method, `alkanes_simulate`, which takes a single object parameter (`alkaneId`, `inputs`, `target`, `pointer`, `refundPointer`, `vout`, `data`) and wraps the `simulate` view. There is no `alkanes_simulateblock` convenience method: `simulateprotostones`, `simulatetransaction`, and `simulateblock` are reached through `metashrew_view` with a hex-encoded protobuf input, using the field tables above. For new integrations the canonical path is `metashrew_view` directly, since the `alkanes_*` methods are documented as thin convenience wrappers over it.
:::
---
## Bitcoin Core RPC
## btc_* Methods (Bitcoin Core)
The `btc_*` namespace provides passthrough access to Bitcoin Core RPC methods. These are the standard bitcoind JSON-RPC methods with a `btc_` prefix.
### Block Methods
#### btc_getblockcount
Get the current block height.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getblockcount",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": 925736,
"id": 1
}
```
**Lua Example:**
```lua
local height = _RPC.btc_getblockcount()
return { current_height = height }
```
Returns the number of blocks in the longest blockchain.
---
#### btc_getblockhash
Get the block hash at a specific height.
**Parameters:**
- **0** (number): Block height
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getblockhash",
"params": [840000],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5",
"id": 1
}
```
**Lua Example:**
```lua
local hash = _RPC.btc_getblockhash(840000)
return { block_hash = hash }
```
Returns the hash of block at the specified height.
---
#### btc_getbestblockhash
Get the hash of the current best block.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getbestblockhash",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "000000000000000000011e795c61e6d795a3b35ac5ed8543de4cf7a7ced9b731",
"id": 1
}
```
**Lua Example:**
```lua
local best_hash = _RPC.btc_getbestblockhash()
return { tip_hash = best_hash }
```
Returns the hash of the best (tip) block in the most-work chain.
---
#### btc_getblock
Get block data by hash.
**Parameters:**
- **0** (string): Block hash
- **1** (number, optional): Verbosity (0=hex, 1=JSON, 2=JSON with full tx)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getblock",
"params": ["0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5", 1],
"id": 1
}
```
**Lua Example:**
```lua
local height = _RPC.btc_getblockcount()
local hash = _RPC.btc_getblockhash(height)
local block = _RPC.btc_getblock(hash, 1)
return {
height = block.height,
time = block.time,
tx_count = #block.tx
}
```
Gets block data. Verbosity 0=hex, 1=JSON, 2=JSON with tx details.
---
#### btc_getblockheader
Get block header data.
**Parameters:**
- **0** (string): Block hash
- **1** (boolean, optional): verbose (true for JSON, false for hex)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getblockheader",
"params": ["0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5", true],
"id": 1
}
```
**Lua Example:**
```lua
local hash = _RPC.btc_getbestblockhash()
local header = _RPC.btc_getblockheader(hash, true)
return {
height = header.height,
time = header.time,
bits = header.bits,
nonce = header.nonce
}
```
---
### Blockchain Info
#### btc_getblockchaininfo
Get blockchain state information.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getblockchaininfo",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"chain": "main",
"blocks": 925737,
"headers": 925737,
"bestblockhash": "000000000000000000001d43fb868a7cfd0581192f648e7bf000c94fe94c6c03",
"difficulty": 149301205959699.9,
"time": 1764440776,
"mediantime": 1764438102,
"verificationprogress": 1,
"initialblockdownload": false,
"chainwork": "0000000000000000000000000000000000000000f97a9cd4c701a35ab19f92d8",
"size_on_disk": 800797064286,
"pruned": false
},
"id": 1
}
```
**Lua Example:**
```lua
local info = _RPC.btc_getblockchaininfo()
return {
chain = info.chain,
height = info.blocks,
difficulty = info.difficulty,
synced = info.verificationprogress == 1
}
```
Returns info about the current state of the blockchain.
---
#### btc_getdifficulty
Get the current difficulty.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getdifficulty",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": 149301205959699.9,
"id": 1
}
```
**Lua Example:**
```lua
local difficulty = _RPC.btc_getdifficulty()
return {
difficulty = difficulty,
difficulty_trillion = difficulty / 1e12
}
```
Returns the proof-of-work difficulty as a multiple of minimum difficulty.
---
### Transaction Methods
#### btc_getrawtransaction
Get raw transaction data.
**Parameters:**
- **0** (string): Transaction ID
- **1** (boolean, optional): verbose (true for decoded JSON)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getrawtransaction",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098", true],
"id": 1
}
```
**Lua Example:**
```lua
local txid = args[1]
local tx = _RPC.btc_getrawtransaction(txid, true)
return {
txid = tx.txid,
size = tx.size,
vsize = tx.vsize,
vin_count = #tx.vin,
vout_count = #tx.vout
}
```
---
#### btc_sendrawtransaction
Broadcast a signed transaction.
**Parameters:**
- **0** (string): The signed transaction hex
- **1** (number, optional): Maximum fee rate in BTC/kvB
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_sendrawtransaction",
"params": ["0100000001..."],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "abc123def456...",
"id": 1
}
```
**Lua Example:**
```lua
local hex = args[1]
local txid = _RPC.btc_sendrawtransaction(hex)
return { broadcast_txid = txid }
```
---
### Mempool Methods
#### btc_getmempoolinfo
Get mempool statistics.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getmempoolinfo",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"loaded": true,
"size": 15381,
"bytes": 14475763,
"usage": 76718160,
"total_fee": 0.02045846,
"maxmempool": 2000000000,
"mempoolminfee": 0.0,
"minrelaytxfee": 0.0,
"incrementalrelayfee": 0.000001,
"unbroadcastcount": 0,
"fullrbf": true
},
"id": 1
}
```
**Lua Example:**
```lua
local mempool = _RPC.btc_getmempoolinfo()
return {
tx_count = mempool.size,
size_mb = mempool.bytes / 1000000,
total_fees_btc = mempool.total_fee
}
```
Returns details on the active state of the mempool.
---
#### btc_getrawmempool
Get all mempool transaction IDs.
**Parameters:**
- **0** (boolean, optional): verbose (true for detailed info per tx)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_getrawmempool",
"params": [false],
"id": 1
}
```
**Lua Example:**
```lua
local txids = _RPC.btc_getrawmempool(false)
return {
mempool_count = #txids
}
```
Returns all transaction IDs in the mempool.
---
### Fee Estimation
#### btc_estimatesmartfee
Estimate fee for confirmation within N blocks.
**Parameters:**
- **0** (number): Confirmation target in blocks
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "btc_estimatesmartfee",
"params": [6],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"feerate": 0.00001013,
"blocks": 6
},
"id": 1
}
```
> **Note:** This method is available via JSON-RPC but not in Lua scripts. Use `esplora_feeestimates` in Lua instead.
Estimates the fee per vbyte needed for confirmation within conf_target blocks.
---
## BRC20 Programmable Module (JSON-RPC)
The BRC2.0 Programmable Module provides smart contract execution capabilities for BRC-20 indexers, running on a custom EVM execution engine using [revm](https://github.com/bluealloy/revm). This allows inscribing smart contracts and function calls on the Bitcoin blockchain.
See the [BRC2.0 Proposal](https://github.com/bestinslot-xyz/brc20-prog-module-proposal) for detailed protocol information.
### Endpoint
```
POST https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog
```
Also available on other networks:
- Signet: `https://signet.subfrost.io/v4/jsonrpc/brc20-prog`
- Regtest: `https://regtest.subfrost.io/v4/jsonrpc/brc20-prog`
With API key: `https://mainnet.subfrost.io/v4/{api_key}/brc20-prog`
---
### Ethereum-Compatible Methods
BRC2.0 implements the [Ethereum JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/) for read operations.
#### `eth_blockNumber`
Returns the latest indexed block height.
```json
{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 1
}
```
#### `eth_getBlockByNumber`
Returns block information with all indexed transactions.
```json
{
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": ["0x1", true],
"id": 1
}
```
#### `eth_getBlockByHash`
Returns block information by block hash.
```json
{
"jsonrpc": "2.0",
"method": "eth_getBlockByHash",
"params": ["0x...", true],
"id": 1
}
```
#### `eth_getTransactionReceipt`
Returns the transaction receipt including logs and status.
```json
{
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": ["0x..."],
"id": 1
}
```
#### `eth_call`
Interact with deployed contracts (read-only).
```json
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{
"to": "0x...",
"data": "0x..."
}, "latest"],
"id": 1
}
```
#### `eth_estimateGas`
Estimate gas for a transaction.
```json
{
"jsonrpc": "2.0",
"method": "eth_estimateGas",
"params": [{
"to": "0x...",
"data": "0x..."
}],
"id": 1
}
```
---
### Debug Methods
#### `debug_traceTransaction`
Returns a callTracer result for a transaction. Requires `EVM_RECORD_TRACES=true` on the server.
```json
{
"jsonrpc": "2.0",
"method": "debug_traceTransaction",
"params": ["0x...", {"tracer": "callTracer"}],
"id": 1
}
```
---
### Transaction Pool
#### `txpool_content`
Returns pending transactions that were sent out of order.
```json
{
"jsonrpc": "2.0",
"method": "txpool_content",
"params": [],
"id": 1
}
```
---
### BRC-20 Balance Method
#### `brc20_balance`
Returns the current BRC-20 balance (in-module) for a given pkscript and ticker.
**Parameters:**
- `pkscript` (string): Bitcoin pkscript (hex)
- `ticker` (string): BRC-20 ticker
```json
{
"jsonrpc": "2.0",
"method": "brc20_balance",
"params": {
"pkscript": "0014...",
"ticker": "ordi"
},
"id": 1
}
```
---
### Receipt Lookup Methods
#### `brc20_getTxReceiptByInscriptionId`
Returns the transaction receipt for a given inscription ID.
```json
{
"jsonrpc": "2.0",
"method": "brc20_getTxReceiptByInscriptionId",
"params": ["abc123i0"],
"id": 1
}
```
#### `brc20_getInscriptionIdByTxHash`
Returns the inscription ID for a given transaction hash.
```json
{
"jsonrpc": "2.0",
"method": "brc20_getInscriptionIdByTxHash",
"params": ["0x..."],
"id": 1
}
```
---
### Precompiled Contracts
BRC2.0 includes precompiled contracts for Bitcoin-specific operations:
- **BIP322_Verifier**: `0x00000000000000000000000000000000000000fe`
- **BTC_Transaction**: `0x00000000000000000000000000000000000000fd`
- **BTC_LastSatLoc**: `0x00000000000000000000000000000000000000fc`
- **BTC_LockedPkScript**: `0x00000000000000000000000000000000000000fb`
#### BIP322 Verifier
Verify BIP-322 signatures. Supports P2TR, P2WPKH, and P2SH-P2WPKH single-sig addresses.
```solidity
interface IBIP322_Verifier {
function verify(
bytes calldata pkscript,
bytes calldata message,
bytes calldata signature
) external returns (bool success);
}
```
#### BTC Transaction
Get Bitcoin transaction details.
```solidity
interface IBTC_Transaction {
function getTxDetails(bytes32 txid) external view returns (
uint256 block_height,
bytes32[] memory vin_txids,
uint256[] memory vin_vouts,
bytes[] memory vin_scriptPubKeys,
uint256[] memory vin_values,
bytes[] memory vout_scriptPubKeys,
uint256[] memory vout_values
);
}
```
#### BTC Last Sat Location
Track satoshi locations using ordinal theory.
```solidity
interface IBTC_LastSatLoc {
function getLastSatLocation(
bytes32 txid,
uint256 vout,
uint256 sat
) external view returns (
bytes32 last_txid,
uint256 last_vout,
uint256 last_sat,
bytes memory old_pkscript,
bytes memory new_pkscript
);
}
```
#### BTC Locked PkScript
Calculate lock pkscripts for time-locked outputs.
```solidity
interface IBTC_LockedPkscript {
function getLockedPkscript(
bytes calldata pkscript,
uint256 lock_block_count
) external view returns (bytes memory locked_pkscript);
}
```
---
### BRC20_Controller Contract
The `BRC20_Controller` contract is deployed at `0xc54dd4581af2dbf18e4d90840226756e9d2b3cdb` and handles BRC-20 deposits, transfers, and withdrawals within the programmable module.
---
### Error Handling
Standard JSON-RPC 2.0 errors are returned:
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request"
},
"id": 1
}
```
---
## 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)](https://curve.readthedocs.io/) 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`:
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": ["", "", "latest"],
"id": 1
}
```
- **`view_name`**: one of the views below, e.g. `getprice`
- **`request_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 read `found` (or `ok`), never the absence of an error.
#### Generating a client
Save the [schema](#schema) as `cryptoswap.proto` and generate:
```bash
# 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
```
```python
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)
```
```ts
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 {
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
| 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:
- **`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 (e.g.
`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
```protobuf
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;
}
```
---
## esplora_* Methods
The `esplora_*` namespace provides access to Electrs/Esplora block explorer functionality. These methods mirror the Esplora REST API.
### Address Methods
#### esplora_address
Get address information including chain stats.
**Parameters:**
- **0** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_address",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"address": "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h",
"chain_stats": {
"funded_txo_count": 2265512,
"funded_txo_sum": 5519171660624754,
"spent_txo_count": 2264780,
"spent_txo_sum": 5517676571260341,
"tx_count": 2058518
},
"mempool_stats": {
"funded_txo_count": 8,
"funded_txo_sum": 9648555949,
"spent_txo_count": 8,
"spent_txo_sum": 9670382958,
"tx_count": 8
}
},
"id": 1
}
```
**Lua Example:**
```lua
local address = args[1]
local info = _RPC.esplora_address(address)
local stats = info.chain_stats
return {
address = info.address,
balance = stats.funded_txo_sum - stats.spent_txo_sum,
tx_count = stats.tx_count,
total_received = stats.funded_txo_sum,
total_sent = stats.spent_txo_sum
}
```
Gets address statistics including funded/spent amounts and transaction count.
---
#### esplora_address::utxo
Get unspent transaction outputs for an address.
**Parameters:**
- **0** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_address::utxo",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": [
{
"txid": "b875d7c6d8de34b5ac2d1deb8aecc0856190e1310ea7d71e9b7fd866313e648c",
"vout": 3,
"value": 1833859462,
"status": {
"confirmed": true,
"block_height": 925724,
"block_hash": "000000000000000000001a157def5fb6e2012803c07647e9f1f964bb28d04d05",
"block_time": 1764433657
}
}
],
"id": 1
}
```
**Lua Example:**
```lua
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
local total = 0
local confirmed = 0
for _, utxo in ipairs(utxos) do
total = total + utxo.value
if utxo.status.confirmed then
confirmed = confirmed + utxo.value
end
end
return {
address = address,
utxo_count = #utxos,
total_sats = total,
confirmed_sats = confirmed,
unconfirmed_sats = total - confirmed
}
```
Gets all UTXOs for an address with confirmation status.
---
#### esplora_address::txs
Get transaction history for an address.
**Parameters:**
- **0** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_address::txs",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
**Lua Example:**
```lua
local address = args[1]
local txs = _RPC.esplora_addresstxs(address)
local result = {}
for i = 1, math.min(#txs, 10) do
local tx = txs[i]
table.insert(result, {
txid = tx.txid,
confirmed = tx.status.confirmed,
fee = tx.fee
})
end
return { recent_txs = result, total = #txs }
```
Gets confirmed and unconfirmed transactions for an address.
---
### Transaction Methods
#### esplora_tx
Get full transaction details.
**Parameters:**
- **0** (string): Transaction ID
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_tx",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"txid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
"version": 1,
"locktime": 0,
"size": 134,
"weight": 536,
"fee": 0,
"vin": [
{
"txid": "0000000000000000000000000000000000000000000000000000000000000000",
"vout": 4294967295,
"is_coinbase": true,
"scriptsig": "04ffff001d0104",
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey_type": "p2pk",
"value": 5000000000
}
],
"status": {
"confirmed": true,
"block_height": 1,
"block_hash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048",
"block_time": 1231469665
}
},
"id": 1
}
```
**Lua Example:**
```lua
local txid = args[1]
local tx = _RPC.esplora_tx(txid)
local total_out = 0
for _, vout in ipairs(tx.vout) do
total_out = total_out + vout.value
end
return {
txid = tx.txid,
fee = tx.fee,
size = tx.size,
weight = tx.weight,
fee_rate = tx.weight > 0 and (tx.fee * 4 / tx.weight) or 0,
confirmed = tx.status.confirmed,
block_height = tx.status.block_height,
input_count = #tx.vin,
output_count = #tx.vout,
total_output = total_out
}
```
Gets complete transaction data including inputs, outputs, and fee.
---
#### esplora_tx::status
Get transaction confirmation status.
**Parameters:**
- **0** (string): Transaction ID
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_tx::status",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"],
"id": 1
}
```
**Lua Example:**
```lua
local txid = args[1]
local status = _RPC.esplora_txstatus(txid)
local current = _RPC.btc_getblockcount()
return {
confirmed = status.confirmed,
block_height = status.block_height,
confirmations = status.confirmed and (current - status.block_height + 1) or 0
}
```
Checks if a transaction is confirmed and gets block details.
---
#### esplora_tx::hex
Get raw transaction hex.
**Parameters:**
- **0** (string): Transaction ID
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_tx::hex",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"],
"id": 1
}
```
**Lua Example:**
```lua
local txid = args[1]
local hex = _RPC.esplora_txhex(txid)
return {
txid = txid,
hex = hex,
size_bytes = #hex / 2
}
```
Gets the raw transaction in hexadecimal format.
---
#### esplora_tx::outspends
Get spend status for all outputs of a transaction.
**Parameters:**
- **0** (string): Transaction ID
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_tx::outspends",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098"],
"id": 1
}
```
**Lua Example:**
```lua
local txid = args[1]
local outspends = _RPC.esplora_txoutspends(txid)
local spent = 0
local unspent = 0
for _, os in ipairs(outspends) do
if os.spent then
spent = spent + 1
else
unspent = unspent + 1
end
end
return {
txid = txid,
outputs = #outspends,
spent = spent,
unspent = unspent
}
```
Checks which outputs have been spent and by which transactions.
---
### Block Methods
#### esplora_block
Get block details by hash.
**Parameters:**
- **0** (string): Block hash
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_block",
"params": ["0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"id": "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5",
"height": 840000,
"version": 710926336,
"timestamp": 1713571767,
"bits": 386089497,
"nonce": 3932395645,
"difficulty": 86388558925171.02,
"merkle_root": "031b417c3a1828ddf3d6527fc210daafcc9218e81f98257f88d4d43bd7a5894f",
"tx_count": 3050,
"size": 2325617,
"weight": 3993281,
"previousblockhash": "0000000000000000000172014ba58d66455762add0512355ad651207918494ab"
},
"id": 1
}
```
**Lua Example:**
```lua
local hash = args[1]
local block = _RPC.esplora_block(hash)
return {
height = block.height,
tx_count = block.tx_count,
size_kb = block.size / 1000,
weight = block.weight,
timestamp = block.timestamp,
difficulty = block.difficulty
}
```
Gets block information including header data and transaction count.
---
#### esplora_block-height
Get block hash at a specific height.
**Parameters:**
- **0** (number): Block height
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_block-height",
"params": [840000],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5",
"id": 1
}
```
**Lua Example:**
```lua
local height = args[1] or _RPC.btc_getblockcount()
local hash = _RPC.esplora_blockheight(height)
return { height = height, hash = hash }
```
Converts a block height to its hash.
---
#### esplora_block::txids
Get all transaction IDs in a block.
**Parameters:**
- **0** (string): Block hash
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_block::txids",
"params": ["0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5"],
"id": 1
}
```
**Lua Example:**
```lua
local hash = args[1]
local txids = _RPC.esplora_blocktxids(hash)
return {
block_hash = hash,
tx_count = #txids,
coinbase_tx = txids[1],
last_tx = txids[#txids]
}
```
Gets the list of transaction IDs included in a block.
---
### Mempool Methods
#### esplora_mempool
Get mempool statistics.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_mempool",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"count": 19178,
"fee_histogram": [
[2.325, 50053],
[1.516, 96335],
[1.077, 50087]
]
},
"id": 1
}
```
**Lua Example:**
```lua
local mempool = _RPC.esplora_mempool()
return {
tx_count = mempool.count,
fee_buckets = #mempool.fee_histogram
}
```
Gets current mempool size, fees, and fee histogram.
---
#### esplora_mempool::recent
Get recent mempool transactions.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_mempool::recent",
"params": [],
"id": 1
}
```
**Lua Example:**
```lua
local recent = _RPC.esplora_mempoolrecent()
local result = {}
for i = 1, math.min(#recent, 5) do
table.insert(result, {
txid = recent[i].txid,
fee = recent[i].fee,
size = recent[i].vsize
})
end
return { recent_txs = result }
```
Gets the most recently added mempool transactions.
---
### Fee Estimation
#### esplora_fee-estimates
Get fee estimates for different confirmation targets.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "esplora_fee-estimates",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"1": 1.013,
"2": 1.013,
"3": 1.013,
"6": 1.013,
"25": 1.013,
"144": 0.734,
"504": 0.734,
"1008": 0.734
},
"id": 1
}
```
**Lua Example:**
```lua
local fees = _RPC.esplora_feeestimates()
return {
next_block = fees["1"],
within_hour = fees["6"],
within_day = fees["144"],
economy = fees["1008"]
}
```
Gets recommended fee rates (sat/vB) for various block targets.
---
## Espo JSON-RPC API
The Espo indexer provides a JSON-RPC API for querying alkanes data and AMM analytics. The API is organized into two modules: **Essentials** for core data and **AMM Data** for trading analytics.
### Endpoint
Espo runs on a separate endpoint from the main JSON-RPC:
```
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/espo
```
The API key is required: a request to `https://mainnet.subfrost.io/v4/espo` without a key returns HTTP 401 with `{"code":"INVALID_API_KEY"}`.
### Essentials Module
Core alkanes data including balances, holders, and storage keys.
#### ping
Ping the Espo server to verify connectivity.
**Parameters:** None
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ping",
"params": {}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": "pong"
}
```
---
#### get_espo_height
Get the current block height processed by the Espo indexer.
**Parameters:** None
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_espo_height",
"params": {}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"height": 825000
}
}
```
---
#### get_address_balances
Get alkanes balances for a Bitcoin address with optional outpoint details.
**Parameters:**
- **address** (string): Bitcoin address
- **include_outpoints** (boolean, optional): Include detailed UTXO information (default: false)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_address_balances",
"params": {
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"include_outpoints": true
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"balances": {
"840000:1": "1000000",
"840000:2": "500000"
},
"outpoints": [
{
"outpoint": "abc123def456...:0",
"entries": [
{
"alkane": "840000:1",
"amount": "1000000"
}
]
}
]
}
}
```
**CLI Equivalent:**
```bash
alkanes-cli espo get-address-balances bc1q... --include-outpoints
```
---
#### get_address_outpoints
Get all outpoints containing alkanes for an address.
**Parameters:**
- **address** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_address_outpoints",
"params": {
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"outpoints": [
{
"outpoint": "txid:0",
"entries": [
{
"alkane": "840000:1",
"amount": "1000000"
}
]
}
]
}
}
```
---
#### get_outpoint_balances
Get alkanes balances at a specific UTXO outpoint.
**Parameters:**
- **outpoint** (string): Outpoint in format "txid:vout"
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_outpoint_balances",
"params": {
"outpoint": "abc123def456...:0"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"outpoint": "abc123def456...:0",
"items": [
{
"outpoint": "abc123def456...:0",
"entries": [
{
"alkane": "840000:1",
"amount": "1000000"
}
]
}
]
}
}
```
---
#### get_holders
Get paginated list of holders for an alkane token.
**Parameters:**
- **alkane** (string): Alkane ID in format "block:tx"
- **page** (number, optional): Page number (default: 0)
- **limit** (number, optional): Items per page (default: 100)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_holders",
"params": {
"alkane": "2:0",
"page": 0,
"limit": 100
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"alkane": "2:0",
"page": 0,
"limit": 100,
"total": 1523,
"has_more": true,
"items": [
{
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": "10000000"
},
{
"address": "bc1q2j3k4l5m6n7o8p9q0r1s2t3u4v5w6x7y8z9",
"amount": "5000000"
}
]
}
}
```
**CLI Equivalent:**
```bash
alkanes-cli espo get-holders 2:0 --page 0 --limit 100
```
---
#### get_holders_count
Get the total number of unique holders for an alkane token.
**Parameters:**
- **alkane** (string): Alkane ID in format "block:tx"
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_holders_count",
"params": {
"alkane": "2:0"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"count": 1523
}
}
```
---
#### get_keys
Get storage keys for an alkane contract with pagination and UTF-8 decoding.
**Parameters:**
- **alkane** (string): Alkane ID in format "block:tx"
- **page** (number, optional): Page number (default: 0)
- **limit** (number, optional): Items per page (default: 100)
- **try_decode_utf8** (boolean, optional): Attempt UTF-8 decoding (default: true)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "get_keys",
"params": {
"alkane": "2:0",
"page": 0,
"limit": 100,
"try_decode_utf8": true
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"alkane": "2:0",
"page": 0,
"limit": 100,
"total": 450,
"has_more": true,
"items": {
"0x6e616d65": {
"hex": "0x6e616d65",
"utf8": "name"
},
"0x73796d626f6c": {
"hex": "0x73796d626f6c",
"utf8": "symbol"
}
}
}
}
```
---
### AMM Data Module
Trading and liquidity analytics for AMM pools.
#### ammdata.ping
Ping the AMM Data module to verify connectivity.
**Parameters:** None
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.ping",
"params": {}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": "pong"
}
```
---
#### ammdata.get_candles
Get OHLCV (Open, High, Low, Close, Volume) candlestick data for a liquidity pool.
**Parameters:**
- **pool** (string): Pool ID in format "block:tx"
- **timeframe** (string, optional): Candle interval ("10m", "1h", "1d", "1w", "1M")
- **side** (string, optional): Price side ("base" or "quote")
- **limit** (number, optional): Number of candles (default: 100)
- **page** (number, optional): Page number (default: 0)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.get_candles",
"params": {
"pool": "840100:5",
"timeframe": "1h",
"side": "base",
"limit": 100,
"page": 0
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"pool": "840100:5",
"timeframe": "1h",
"side": "base",
"page": 0,
"limit": 100,
"total": 2400,
"has_more": true,
"candles": [
{
"open_time": "1702645200",
"close_time": "1702648800",
"open": "1000000",
"high": "1050000",
"low": "990000",
"close": "1020000",
"volume": "5000000",
"trades": 45
},
{
"open_time": "1702648800",
"close_time": "1702652400",
"open": "1020000",
"high": "1080000",
"low": "1010000",
"close": "1050000",
"volume": "6000000",
"trades": 52
}
]
}
}
```
**CLI Equivalent:**
```bash
alkanes-cli espo get-candles 840100:5 --timeframe 1h --side base --limit 100
```
---
#### ammdata.get_trades
Get trade history for a pool with filtering and sorting options.
**Parameters:**
- **pool** (string): Pool ID in format "block:tx"
- **limit** (number, optional): Number of trades (default: 100)
- **page** (number, optional): Page number (default: 0)
- **side** (string, optional): Price side ("base" or "quote")
- **filter_side** (string, optional): Filter by trade side ("buy", "sell", or "all")
- **sort** (string, optional): Sort field
- **dir** (string, optional): Sort direction ("asc" or "desc")
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.get_trades",
"params": {
"pool": "840100:5",
"limit": 50,
"page": 0,
"side": "base",
"filter_side": "buy",
"sort": "timestamp",
"dir": "desc"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"pool": "840100:5",
"side": "base",
"filter_side": "buy",
"sort": "timestamp",
"dir": "desc",
"page": 0,
"limit": 50,
"total": 850,
"has_more": true,
"trades": [
{
"txid": "abc123...",
"vout": 0,
"timestamp": "1702645200",
"block_height": 825000,
"side": "buy",
"amount_in": "1000000",
"amount_out": "950000",
"price": "0.95"
}
]
}
}
```
---
#### ammdata.get_pools
Get all AMM pools with pagination.
**Parameters:**
- **limit** (number, optional): Number of pools (default: 100)
- **page** (number, optional): Page number (default: 0)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.get_pools",
"params": {
"limit": 100,
"page": 0
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"page": 0,
"limit": 100,
"total": 45,
"has_more": false,
"pools": {
"840100:5": {
"token0": "840000:1",
"token1": "840000:2",
"reserve0": "10000000",
"reserve1": "15000000",
"total_supply": "12247449"
}
}
}
}
```
---
#### ammdata.find_best_swap_path
Find the optimal multi-hop swap path between two tokens using advanced routing algorithms.
**Parameters:**
- **token_in** (string): Input token ID
- **token_out** (string): Output token ID
- **mode** (string, optional): Swap mode ("exact_in", "exact_out", or "implicit")
- **amount_in** (string, optional): Input amount (for exact_in mode)
- **amount_out** (string, optional): Output amount (for exact_out mode)
- **amount_out_min** (string, optional): Minimum acceptable output
- **amount_in_max** (string, optional): Maximum acceptable input
- **available_in** (string, optional): Available input amount
- **fee_bps** (number, optional): Fee in basis points (default: 30, i.e., 0.3%)
- **max_hops** (number, optional): Maximum number of hops (default: 3)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.find_best_swap_path",
"params": {
"token_in": "840000:1",
"token_out": "840000:2",
"mode": "exact_in",
"amount_in": "1000000",
"amount_out_min": "900000",
"fee_bps": 30,
"max_hops": 3
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"mode": "exact_in",
"token_in": "840000:1",
"token_out": "840000:2",
"fee_bps": 30,
"max_hops": 3,
"amount_in": "1000000",
"amount_out": "950000",
"hops": [
{
"pool": "840100:5",
"token_in": "840000:1",
"token_out": "840000:3",
"amount_in": "1000000",
"amount_out": "1500000"
},
{
"pool": "840100:6",
"token_in": "840000:3",
"token_out": "840000:2",
"amount_in": "1500000",
"amount_out": "950000"
}
]
}
}
```
**CLI Equivalent:**
```bash
alkanes-cli espo find-best-swap-path 840000:1 840000:2 \
--mode exact_in --amount-in 1000000 --amount-out-min 900000
```
---
#### ammdata.get_best_mev_swap
Find the best MEV (Maximal Extractable Value) arbitrage opportunity for a token across all available pools.
**Parameters:**
- **token** (string): Token ID to find arbitrage for
- **fee_bps** (number, optional): Fee in basis points (default: 30)
- **max_hops** (number, optional): Maximum number of hops (default: 3)
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "ammdata.get_best_mev_swap",
"params": {
"token": "840000:1",
"fee_bps": 30,
"max_hops": 3
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"ok": true,
"token": "840000:1",
"fee_bps": 30,
"max_hops": 3,
"amount_in": "1000000",
"amount_out": "1050000",
"profit": "50000",
"hops": [
{
"pool": "840100:5",
"token_in": "840000:1",
"token_out": "840000:2",
"amount_in": "1000000",
"amount_out": "2000000"
},
{
"pool": "840100:6",
"token_in": "840000:2",
"token_out": "840000:1",
"amount_in": "2000000",
"amount_out": "1050000"
}
]
}
}
```
---
### Implementation Notes
- All methods use JSON-RPC 2.0 protocol
- AMM Data methods use dot notation (e.g., `ammdata.get_candles`)
- Alkane IDs are in format `block:tx` (e.g., `840000:1`)
- Outpoints are in format `txid:vout`
- Pagination is 0-indexed
- All numeric amounts are returned as strings to preserve precision
- BigInt values are converted to strings in responses
### TypeScript SDK Usage
Use the `@alkanes/ts-sdk` for easy TypeScript/JavaScript integration:
```typescript
import { AlkanesProvider } from '@alkanes/ts-sdk';
// Initialize provider
const provider = new AlkanesProvider({ network: 'mainnet' });
await provider.initialize();
// Essentials Module
const height = await provider.espo.getHeight();
const balances = await provider.espo.getAddressBalances('bc1q...', true);
const holders = await provider.espo.getHolders('2:0', 0, 100);
// AMM Data Module
const candles = await provider.espo.getCandles('840100:5', '1h', 'base', 100, 0);
const path = await provider.espo.findBestSwapPath(
'840000:1',
'840000:2',
'exact_in',
'1000000',
undefined,
'900000'
);
const mev = await provider.espo.getBestMevSwap('840000:1', 30, 3);
```
See [alkanes-cli & @alkanes/ts-sdk](../cli-sdk/overview) for more details on the SDK.
---
## Foreign Exchange (Fiat)
Fiat exchange rates, so you can price and compare things quoted in different
currencies against one unit, e.g. rank a listing priced in **BRL** against one
priced in **USD**. Rates are returned as **decimal strings**, and any currency
the service cannot price right now is reported explicitly rather than guessed.
### Endpoint
```
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/forex
```
Authentication is the same as the rest of the platform: the API key is a path
segment (see [Authentication](../getting-started/authentication)). It is JSON-RPC 2.0 over
`POST`, like the other `/v4/YOUR_API_KEY` methods.
### Rates are decimal strings: do not parse them as numbers
Every rate, amount and result is a JSON **string** holding a decimal value:
```json
{ "EUR": "0.8604", "JPY": "154.324", "VND": "25914.61415083" }
```
This is deliberate. If you `JSON.parse` a rate straight into a float you have
already lost precision before you do any math with it. **Keep rates as strings
and feed them to a decimal library** (`BigDecimal`, `decimal.js`, Python
`Decimal`, Rust `rust_decimal`, …). Do your multiplication, division and
rounding there. On the way in, `amount` may be sent as a decimal string (a plain
JSON number is also accepted); on the way out everything is a string.
### Methods
| Method | Purpose |
|--------|---------|
| `forex_rates` | Rates for one base currency against a set of quote currencies |
| `forex_convert` | Convert an amount from one currency to another |
| `forex_status` | How fresh the data is and which currencies are covered |
| `forex_currencies` | The currencies this deployment covers |
#### `forex_rates`
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "forex_rates",
"params": {
"base": "USD",
"quotes": ["EUR", "GBP", "JPY", "BRL", "NGN"],
"provenance": true
}
}
```
- **`base`**: the currency the rates are expressed *per*. Defaults to `USD`.
- **`quotes`**: the currencies you want a rate for. Omit to get everything covered.
- **`provenance`**: optional; set `true` to include a per-currency provenance
block. Off by default.
Response (`result`):
```json
{
"base": "USD",
"rates": { "EUR": "0.8604", "GBP": "0.7395", "JPY": "154.324", "BRL": "5.0969" },
"missing": ["NGN"],
"missing_reasons": { "NGN": "no_quotes" },
"age_seconds_by_currency": { "EUR": 64, "GBP": 64, "JPY": 20, "BRL": 21 },
"as_of": "2026-09-10T16:57:19Z",
"as_of_unix": 1789059439,
"stale": false
}
```
What each field means for pricing:
- **`rates`**: `{ CCY: "decimal" }`, the amount of `CCY` per **1 unit of `base`**.
With `base: "USD"`, `EUR: "0.8604"` means 0.8604 EUR buys per 1 USD. To go the
other way (USD per 1 EUR) invert it (`1 / 0.8604`) **in your decimal
library**, not as a float. The base's own rate is `"1"`.
- **`missing`**: currencies that could **not** be priced right now. A currency
here is **absent from `rates`** and you should treat it as *no price
available*: surface that to the user, do **not** substitute a stale value or
invent one. This is the whole contract: a rate you get back is one the service
stands behind; anything it is unsure about is named in `missing` instead.
- **`missing_reasons`**: optional short reason per missing currency.
- **`age_seconds_by_currency`**: how old each returned rate is, in seconds.
Apply your own staleness policy **per currency** with this, because different
currencies can have different ages, so a single global age is the wrong thing
to gate on. (The base currency has no entry: its rate is the constant `"1"`.)
- **`as_of` / `as_of_unix`**: timestamp of the freshest rate in the response.
- **`stale`**: `true` if even the freshest rate is older than the deployment's
staleness threshold. A quick "should I trust this at all" flag.
- **`provenance`**: present only when you asked for it; a per-currency block you
can log or display for transparency. Not needed to consume the rates.
#### `forex_convert`
Convert a specific amount between two currencies in one call:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "forex_convert",
"params": { "from": "USD", "to": "BRL", "amount": "100" }
}
```
Returns a `result` decimal string plus the same freshness fields. Unlike
`forex_rates`, `forex_convert` **errors** if it cannot price the pair. A
conversion has no partial answer, so there is no `missing` list here; handle the
JSON-RPC error and show "price unavailable".
#### `forex_status` and `forex_currencies`
`forex_status` reports overall freshness (`age_seconds`, `stale`, `as_of`) and
which currencies are `available`; it does not error on data state, so it is safe
to poll as a health check. `forex_currencies` returns the coverage lists
(`available`, `currencies`, `default_base`, `missing`); call it to discover what
a deployment can price before you request rates.
### Currencies covered
This deployment covers:
```
USD EUR GBP JPY CNY INR CAD AUD CHF BRL
MXN ARS NGN ZAR TRY RUB KRW IDR PHP VND
```
Anything outside this set comes back in `missing` rather than as a rate. Use
`forex_currencies` if you want to read the list programmatically instead of
hard-coding it.
### Pricing checklist
- Keep every rate/amount/result as a **decimal string**; do the math in a decimal
library; never round-trip through a float.
- Treat a currency in **`missing`** as *no price*; never fill it in yourself.
- Rates are **units per base**; invert (in decimal) if you need the other
direction.
- Gate on **`age_seconds_by_currency`** (and `stale`) before trusting a rate for
a quote, and decide freshness per currency, not globally.
---
## 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`:
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": ["", "", "latest"],
"id": 1
}
```
- **`view_name`**: one of the views below, e.g. `frusddepositsbyrecipient`
- **`request_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.**
> `frusddepositsbyrecipient` wants `{ recipient_script = 1 }`, **not** the raw
> script bytes. Passing the raw script returns an empty `0x` that 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_script`** is the raw BTC output script (`scriptPubKey`). For a
taproot recipient that is `5120` ++ the 32-byte x-only key. Decode the address
to its script first; it is not an address string.
- **`fr_amount`** is a big-endian `uint256` with **8 decimals** (matching the
frUSD alkane on Bitcoin): `99701199000` is `997.01199 frUSD`. Parse with a
bignum and divide by `1e8` for display.
- **An empty `DepositsResponse` is not an error.** It means the recipient has no
deposits *yet*: either none were ever made, or the deposit's Ethereum block is
still above `frusdindexheight`. Compare against the index height before telling a
user "not found".
- **`payment_id`** is the vault's monotonic id, stable and unique; use it as your
render key and to detect new deposits. **`confirmed`** is always `true` today (a
record exists only once mined) and is reserved for a future pending variant.
#### Generating a client
Save the [schema](#schema) as `frusd.proto` and generate:
```bash
# 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
```
```python
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())
```
```ts
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 {
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:
1. **Resolve the user's BTC recipient script**, the taproot output they bridged
to. Convert address → `scriptPubKey` (taproot → `5120`).
2. **Call `frusddepositsbyrecipient`.** Render each deposit: amount
(`fr_amount / 1e8`), the Ethereum tx (`tx_hash` → explorer link), `block_number`,
and `payment_id` (stable key).
3. **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`.
4. **Poll ~15s** for a live tracker (ETH block time ≈ 12s). Detect new deposits by
the max `payment_id` seen. Cache by `(recipient, indexHeight)`; deposits are
append-only per recipient, so only refetch when the height moves.
### Schema
```protobuf
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
}
```
---
## metashrew_* Methods
The `metashrew_*` namespace provides access to the Metashrew indexer, which powers custom WASM-based blockchain views.
### Overview
Metashrew is a flexible indexer that runs WebAssembly programs against Bitcoin blockchain data. It enables:
- Custom indexing logic
- Efficient state queries
- Protocol-specific views (Alkanes, Protorunes, etc.)
### Runtime and architecture
metashrew runs as a single binary, `rockshrew-mono`, that bundles the indexer with a local RocksDB database and serves the JSON-RPC view layer from the same process. One binary indexes the chain and answers reads, so the reads are a faithful projection of consensus state rather than a separate database that could drift.
metashrew is the source of truth for on-chain reads. Every value this API returns comes from a view function that the indexer evaluated against the chain, so there is no secondary store to reconcile and no separate read model to fall behind.
The current release line is metashrew `v9.0.5-rc.13`, running the Alkanes indexer from alkanes-rs `v2.2.1-rc.5`.
The runtime is async (wasmtime) and times out long-running requests, and new host functions can be added without changing the host ABI. A newer, unreleased line of work adds Block-STM style parallel block execution to speed up indexing, on the same RocksDB model. Ordering guarantees still hold, but the general "same WASM, same index" property no longer holds automatically under parallel execution, so if you write an indexer that relies on execution order, use serializable semantics.
### Methods
#### metashrew_height
Get the current indexed height of Metashrew.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "metashrew_height",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "925736",
"id": 1
}
```
**Lua Example:**
```lua
local height = tonumber(_RPC.metashrew_height())
return { indexed_height = height }
```
Returns the current block height indexed by Metashrew.
---
#### metashrew_view
Execute a view function on the Metashrew indexer.
**Parameters:**
- **0** (string): View method name
- **1** (any): Input data (method-specific)
- **2** (string): Block tag ("latest" or block number)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": [
"protorunesbyaddress",
{
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"protocolTag": "1"
},
"latest"
],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"outpoints": [
{
"outpoint": {
"txid": "abc123...",
"vout": 0
},
"runes": [
{
"id": { "block": 840000, "tx": 1 },
"amount": "1000000"
}
]
}
]
},
"id": 1
}
```
**Lua Example:**
```lua
local address = args[1]
local result = _RPC.metashrew_view(
"protorunesbyaddress",
{
address = address,
protocolTag = "1"
},
"latest"
)
return { result_type = type(result) }
```
---
### Common View Methods
Every alkanes and protorunes query is a view function invoked through `metashrew_view`. The catalog below lists the functions the indexer exposes. The alkanes-specific ones (simulation, trace, bytecode, metadata) are documented in full on the [`alkanes_* Methods`](./alkanes) page.
| View function | Purpose |
|---|---|
| `simulate` | Read-only evaluate a single protostone or cellpack against current state, returns the final result. |
| `simulateprotostones` | Simulate one or more protostones with full per-protostone execution traces. See [alkanes](./alkanes). |
| `simulatetransaction` | Simulate a whole transaction (raw tx or PSBT) with traces. See [alkanes](./alkanes). |
| `simulateblock` | Simulate a whole block of transactions in order, with per-tx traces. See [alkanes](./alkanes). |
| `trace` | Execution trace for the protostone at a given outpoint. |
| `traceblock` | Execution traces for every protostone in a block. |
| `meta` | Contract ABI, read from the alkane's `__meta` WASM export (see below). |
| `getbytecode` | Raw WASM bytecode of a deployed alkane. |
| `protorunesbyaddress` | Protorunes and alkanes held by an address. Deprecated in alkanes v3, prefer the esplora UTXO API plus `protorunesbyoutpoint`. |
| `protorunesbyoutpoint` | Protorunes and alkanes at a specific outpoint. |
| `getstorageat` | Read a raw storage slot. Cheap: it does not run a full WASM evaluation, it just fetches the value at a storage path. |
| `alkanes_id_to_outpoint` | Resolve an alkane id to the outpoint where it was etched. |
`spendablesbyaddress` is also available as a top-level method (not under `metashrew_view`); it returns the spendable UTXOs for an address.
The following view methods are commonly available through `metashrew_view`:
#### protorunesbyaddress
Get protorunes (including alkanes) held by an address.
**Input:**
```json
{
"address": "bc1q...",
"protocolTag": "1"
}
```
**Lua Example:**
```lua
local result = _RPC.metashrew_view(
"protorunesbyaddress",
{ address = args[1], protocolTag = "1" },
"latest"
)
return { result_type = type(result) }
```
---
#### protorunesbyoutpoint
Get protorunes at a specific outpoint.
**Input:**
```json
{
"txid": "abc123...",
"vout": 0,
"protocolTag": "1"
}
```
---
#### meta
Return the ABI of an alkane contract, extracted directly from the
contract's `__meta` wasm export. The response is a hex-encoded UTF-8
JSON blob whose shape is:
```json
{
"contract": "DIESEL",
"methods": [
{ "name": "mint", "opcode": 77, "params": [], "returns": "void" },
{ "name": "get_name", "opcode": 99, "params": [], "returns": "String" },
{ "name": "get_symbol", "opcode": 100, "params": [], "returns": "String" },
{ "name": "get_total_supply", "opcode": 101, "params": [], "returns": "u128" }
]
}
```
This is the canonical way to discover what a deployed alkane can do:
the `__meta` export is generated by the contract's `MessageDispatch`
enum at build time, so if the contract's dispatch changes, so does the
metadata. AMM pools, factories, DAO modules, anything that doesn't
fit the narrow token shape, should be inspected via `meta`, not via
the `alkanes_meta` convenience method.
**Input:** protobuf-encoded `MessageContextParcel` with `calldata` set
to the encoded alkane id target only (no opcode, the view function
calls `__meta`).
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "metashrew_view",
"params": [
"meta",
"0x2a020200",
"latest"
],
"id": 1
}
```
**Response (raw):**
```json
{
"jsonrpc": "2.0",
"result": "0x7b22636f6e7472616374223a2244...",
"id": 1
}
```
Decode the hex, you get the JSON blob above.
See [Reading Alkane Metadata](../guides/alkane-metadata) for the
full picture: `__meta` semantics, building the calldata, TypeScript and
Rust SDK examples, and the offline-via-bytecode inspection path.
---
#### runebyid
Get rune information by ID.
**Input:**
```json
{
"block": 840000,
"tx": 1
}
```
---
### Block Tags
The `block_tag` parameter controls which state to query:
- **"latest"**: Most recent indexed block
- **"850000"**: Specific block height (as string)
---
### Protocol Tags
For protorune-based queries, the `protocolTag` identifies the protocol:
- **"1"**: Alkanes
- **"13"**: Other protorunes
---
### Sync Status Check
Compare indexer heights to check sync status:
**Lua Example:**
```lua
local metashrew_h = tonumber(_RPC.metashrew_height())
local btc_h = _RPC.btc_getblockcount()
return {
metashrew = metashrew_h,
bitcoin = btc_h,
synced = metashrew_h >= btc_h - 2,
blocks_behind = btc_h - metashrew_h
}
```
---
## ord_* Methods
The `ord_*` namespace provides access to Ordinals protocol data, including inscriptions, runes, and satoshi tracking.
### Block Information
#### ord_blockcount
Get the total block count.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_blockcount",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": 925737,
"id": 1
}
```
**Lua Example:**
```lua
local count = _RPC.ord_blockcount()
return { block_count = count }
```
Returns the total number of blocks indexed.
---
#### ord_blockhash
Get the block hash at a specific height.
**Parameters:**
- **0** (number): Block height
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_blockhash",
"params": [840000],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5",
"id": 1
}
```
**Lua Example:**
```lua
local height = args[1] or 840000
local hash = _RPC.ord_blockhash(height)
return { height = height, hash = hash }
```
Converts a block height to its hash.
---
#### ord_blocktime
Get the timestamp of the latest block.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_blocktime",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": 1764440776,
"id": 1
}
```
**Lua Example:**
```lua
local timestamp = _RPC.ord_blocktime()
return { timestamp = timestamp }
```
Returns the Unix timestamp of the most recent indexed block.
---
#### ord_blockheight
Get the current indexed block height.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_blockheight",
"params": [],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": 925736,
"id": 1
}
```
> **Note:** This method is available via JSON-RPC but may not be available in Lua scripts. Use `ord_blockcount` in Lua instead.
Returns the current block height indexed by the ord indexer.
---
### Inscription Methods
#### ord_inscription
Get inscription details by ID.
**Parameters:**
- **0** (string): Inscription ID (format: `txidi0`)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_inscription",
"params": ["6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"id": "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0",
"number": 0,
"address": "bc1pd96xzyue7yvjf24cmu07xasezg3jpm5tyfem4txad5ke2jas4m7qkhe7dy",
"content_type": "image/png",
"content_length": 793,
"fee": 322,
"height": 767430,
"sat": 1252201400444387,
"satpoint": "717d25914a1191c30f4c1d709e3ec0e1fab79cc7bf92cd12a93bed68db63416a:0:0",
"timestamp": 1671049920,
"value": 606,
"charms": [],
"child_count": 5,
"parents": [],
"rune": null
},
"id": 1
}
```
**Lua Example:**
```lua
local id = args[1]
local insc = _RPC.ord_inscription(id)
return {
id = insc.id,
number = insc.number,
content_type = insc.content_type,
content_length = insc.content_length,
genesis_height = insc.height,
current_owner = insc.address,
sat = insc.sat,
fee = insc.fee
}
```
Gets full details about an Ordinal inscription.
---
#### ord_inscriptions
Get inscriptions with pagination.
**Parameters:**
- **0** (number): Page number
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_inscriptions",
"params": [0],
"id": 1
}
```
**Lua Example:**
```lua
local page = args[1] or 0
local inscriptions = _RPC.ord_inscriptions(page)
return {
page = page,
count = #inscriptions
}
```
Lists inscriptions starting from a specific page.
---
#### ord_inscriptions::block
Get inscriptions in a specific block.
**Parameters:**
- **0** (number): Block height
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_inscriptions::block",
"params": [840000],
"id": 1
}
```
> **Note:** This method is available via JSON-RPC but not in Lua scripts.
Gets all inscriptions created in a specific block.
---
### Rune Methods
#### ord_rune
Get rune information by name.
**Parameters:**
- **0** (string): Rune name (with spacers like •)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_rune",
"params": ["UNCOMMON•GOODS"],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"id": "1:0",
"entry": {
"block": 1,
"burned": 16864,
"divisibility": 0,
"etching": "0000000000000000000000000000000000000000000000000000000000000000",
"mints": 64709945,
"number": 0,
"premine": 0,
"spaced_rune": "UNCOMMON•GOODS",
"symbol": "⧉",
"terms": {
"amount": 1,
"cap": 3.402823669209385e38,
"height": [840000, 1050000]
},
"timestamp": 0,
"turbo": true
},
"mintable": true,
"parent": null
},
"id": 1
}
```
**Lua Example:**
```lua
local name = args[1]
local rune = _RPC.ord_rune(name)
return {
id = rune.id,
name = rune.entry.spaced_rune,
symbol = rune.entry.symbol,
divisibility = rune.entry.divisibility,
total_mints = rune.entry.mints,
mintable = rune.mintable
}
```
Gets details about a specific rune.
---
#### ord_runes
Get all runes with pagination.
**Parameters:**
- **0** (number): Page number
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_runes",
"params": [0],
"id": 1
}
```
**Lua Example:**
```lua
local page = args[1] or 0
local runes = _RPC.ord_runes(page)
return {
page = page,
result_type = type(runes)
}
```
Lists all runes starting from a page number.
---
### Satoshi Methods
#### ord_sat
Get information about a specific satoshi.
**Parameters:**
- **0** (number): Satoshi ordinal number
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_sat",
"params": [1000000000000],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"number": 1000000000000,
"name": "nvoivgmdyqb",
"block": 200,
"cycle": 0,
"epoch": 0,
"period": 0,
"offset": 0,
"rarity": "uncommon",
"percentile": "0.04761904767142859%",
"decimal": "200.0",
"degree": "0°200′200″0‴",
"satpoint": "2b1f06c2401d3b49a33c3f5ad5864c0bc70044c4068f9174546f3cfc1887d5ba:0:0",
"timestamp": 1231753120,
"inscriptions": [],
"charms": ["coin", "uncommon"]
},
"id": 1
}
```
**Lua Example:**
```lua
local sat_num = args[1]
local sat = _RPC.ord_sat(sat_num)
return {
number = sat.number,
name = sat.name,
rarity = sat.rarity,
block = sat.block,
epoch = sat.epoch,
charms = sat.charms
}
```
Gets details about a sat number including rarity and inscriptions.
---
### Output Methods
#### ord_output
Get ordinals data for a specific output.
**Parameters:**
- **0** (string): Outpoint (format: txid:vout)
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_output",
"params": ["0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098:0"],
"id": 1
}
```
**Lua Example:**
```lua
local outpoint = args[1]
local output = _RPC.ord_output(outpoint)
return {
outpoint = outpoint,
address = output.address,
value = output.value
}
```
Gets inscriptions and runes at a specific outpoint.
---
### Address Methods
#### ord_address
Get address ordinals summary.
**Parameters:**
- **0** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_address",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
> **Note:** This method is available via JSON-RPC but not in Lua scripts.
Gets inscription and rune summary for an address.
---
#### ord_address::inscriptions
Get inscriptions owned by an address.
**Parameters:**
- **0** (string): Bitcoin address
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "ord_address::inscriptions",
"params": ["bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h"],
"id": 1
}
```
> **Note:** This method is available via JSON-RPC but not in Lua scripts.
Lists all inscriptions held by an address.
---
## JSON-RPC API Overview
The Subfrost API uses JSON-RPC 2.0 as its primary protocol. All methods are accessible through a unified endpoint with namespace prefixes.
### Endpoint
```
POST https://mainnet.subfrost.io/v4/jsonrpc
```
With API key in path:
```
POST https://mainnet.subfrost.io/v4/
```
#### BRC20-PROG Endpoint
For BRC20 Programmable Module (ETH-compatible JSON-RPC):
```
POST https://mainnet.subfrost.io/v4/jsonrpc/brc20-prog
```
Or with API key:
```
POST https://mainnet.subfrost.io/v4//brc20-prog
```
### Request Format
```json
{
"jsonrpc": "2.0",
"method": "namespace_methodname",
"params": [...],
"id": 1
}
```
- **`jsonrpc`** (string): Must be "2.0"
- **`method`** (string): Method name with namespace prefix
- **`params`** (array): Method parameters
- **`id`** (number/string): Request identifier
### Response Format
#### Success
```json
{
"jsonrpc": "2.0",
"result": { /* method result */ },
"id": 1
}
```
#### Error
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": 1
}
```
### Namespaces
- **`esplora_*`**: Electrs block explorer API (e.g., `esplora_address::utxo`)
- **`ord_*`**: Ordinals protocol (e.g., `ord_inscription`)
- **`metashrew_*`**: Metashrew indexer (e.g., `metashrew_view`)
- **`alkanes_*`**: Alkanes protocol (e.g., `alkanes_protorunesbyaddress`)
- **`btc_*`**: Bitcoin Core RPC (e.g., `btc_getblockcount`)
- **`brc20_*`**: BRC20 Programmable Module (e.g., `brc20_balanceOf`), via the `/brc20-prog` endpoint
- **`subfrost_*`**: FROST threshold signature wallet, regtest development only (e.g., `subfrost_getpublic`, `subfrost_reset`, `subfrost_thieve`)
- **`lua_*`**: Lua script execution (e.g., `lua_evalscript`)
- **`sandshrew_*`**: Alias for lua_* (e.g., `sandshrew_evalscript`)
- **`forex_*`**: Fiat exchange rates (e.g., `forex_rates`), via the `/forex` endpoint. See [Foreign Exchange](./forex).
- **`mempool_*`**: Live Bitcoin and Ethereum mempool (e.g., `mempool_info`), via the `/mempool` and `/ethereum/mempool` endpoints. See [Mempool JSON-RPC](../mempool/jsonrpc).
#### Dedicated endpoints
These are JSON-RPC 2.0 too, but each is served from its own path and indexer:
- **BTC/USD Pool** (`/v4/YOUR_API_KEY/btcusd`): protobuf views over `metashrew_view`. See [BTC/USD Pool](./btcusd).
- **frUSD Deposits** (`/v4/YOUR_API_KEY/ethereum/frusd`): protobuf views over `metashrew_view`, indexed by BTC recipient. See [frUSD Deposits (Ethereum)](./frusd).
- **Espo** (`/v4/YOUR_API_KEY/espo`): alkanes and AMM analytics (e.g., `get_holders`, `ammdata.get_candles`). See [Espo](./espo).
### Method Naming Convention
Methods use underscores and double colons to represent REST paths:
- **`_`**: Namespace separator (e.g., `esplora_address`)
- **`::`**: Path parameter placeholder (e.g., `esplora_address::utxo` maps to `/address/{param}/utxo`)
#### Examples
```
esplora_address::utxo → GET /address/{address}/utxo
ord_inscription → GET /inscription/{id}
btc_getblockcount → bitcoind getblockcount
```
### Batch Requests
Send multiple requests in a single HTTP call:
```json
[
{ "jsonrpc": "2.0", "method": "btc_getblockcount", "params": [], "id": 1 },
{ "jsonrpc": "2.0", "method": "btc_getbestblockhash", "params": [], "id": 2 },
{ "jsonrpc": "2.0", "method": "esplora_fee-estimates", "params": [], "id": 3 }
]
```
Response:
```json
[
{ "jsonrpc": "2.0", "result": 850000, "id": 1 },
{ "jsonrpc": "2.0", "result": "000000000000000000...", "id": 2 },
{ "jsonrpc": "2.0", "result": { "1": 25.5, "6": 15.2 }, "id": 3 }
]
```
### Error Codes
- **-32700**: Parse error (invalid JSON)
- **-32600**: Invalid Request (invalid JSON-RPC request)
- **-32601**: Method not found (unknown method)
- **-32602**: Invalid params (invalid method parameters)
- **-32603**: Internal error (server error)
- **-32000**: Rate limit exceeded
### Using in Lua Scripts
All RPC methods are available in Lua scripts via the `_RPC` global table:
```lua
-- From within a lua_evalscript
local height = _RPC.btc_getblockcount()
local utxos = _RPC.esplora_addressutxo("bc1q...")
local inscription = _RPC.ord_inscription("abc123i0")
return { height = height, utxo_count = #utxos }
```
See [Lua Scripting](../lua/overview) for more details.
### Next Steps
- [esplora_* Methods](./esplora): Block explorer API
- [ord_* Methods](./ord): Ordinals protocol
- [Bitcoin Core RPC](./bitcoind): Full node methods
- [Lua Scripting](../lua/overview): Server-side scripts
---
## lua_evalsaved
Execute a previously saved Lua script by its SHA256 hash.
### Method
`lua_evalsaved`
### Parameters
- **0** (string): Script SHA256 hash (from lua_savescript)
- **1+** (any): Optional arguments (accessible via `args` table)
### Request
```json
{
"jsonrpc": "2.0",
"method": "lua_evalsaved",
"params": [
"a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
],
"id": 1
}
```
### Response
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 1,
"returns": 5,
"error": null,
"runtime": 32
},
"id": 1
}
```
### Response Fields
- **`calls`** (number): Number of RPC calls made during execution
- **`returns`** (any): Return value from the Lua script
- **`error`** (object/null): Error information if execution failed
- **`runtime`** (number): Execution time in milliseconds
### Error: Script Not Found
If the script hash is not in storage:
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 0,
"returns": null,
"error": {
"code": -32602,
"message": "Script not found for hash: a1b2c3d4..."
},
"runtime": 0
},
"id": 1
}
```
### Complete Workflow Example
#### Step 1: Save the Script
```json
{
"jsonrpc": "2.0",
"method": "lua_savescript",
"params": [
"local address = args[1]\nlocal ord_outputs = _RPC.ord_outputs(address)\nlocal inscriptions = {}\nfor _, output in ipairs(ord_outputs) do\n if output.inscriptions then\n for _, insc in ipairs(output.inscriptions) do\n table.insert(inscriptions, insc)\n end\n end\nend\nreturn { count = #inscriptions, inscriptions = inscriptions }"
],
"id": 1
}
```
Response:
```json
{
"jsonrpc": "2.0",
"result": {
"hash": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730"
},
"id": 1
}
```
#### Step 2: Execute Multiple Times
First execution:
```json
{
"jsonrpc": "2.0",
"method": "lua_evalsaved",
"params": [
"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
"bc1pxyz..."
],
"id": 2
}
```
Second execution with different address:
```json
{
"jsonrpc": "2.0",
"method": "lua_evalsaved",
"params": [
"7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730",
"bc1pabc..."
],
"id": 3
}
```
### JavaScript Example with Fallback
```javascript
async function executeScript(scriptCode, ...args) {
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(scriptCode).digest('hex');
// Try evalsaved first
try {
const response = await rpc('lua_evalsaved', [hash, ...args]);
if (!response.error || !response.error.message.includes('not found')) {
return response;
}
} catch (e) {
// Script not saved, continue to save it
}
// Save the script
await rpc('lua_savescript', [scriptCode]);
// Now execute with evalsaved
return await rpc('lua_evalsaved', [hash, ...args]);
}
// Usage
const script = `
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
return #utxos
`;
const result = await executeScript(script, 'bc1q...');
console.log('UTXO count:', result.returns);
```
### Advantages Over lua_evalscript
- **Request size:** lua_evalscript sends full script, lua_evalsaved sends 64-char hash
- **Parse time:** lua_evalscript parses every request, lua_evalsaved parses once on save
- **Network usage:** lua_evalsaved is lower bandwidth
- **Best for:** lua_evalscript for development, lua_evalsaved for production
### Caching Strategy
For production applications:
```javascript
class ScriptCache {
constructor() {
this.scripts = new Map();
this.hashes = new Map();
}
register(name, code) {
const hash = this.computeHash(code);
this.scripts.set(name, { code, hash, saved: false });
this.hashes.set(hash, name);
}
computeHash(code) {
return require('crypto').createHash('sha256').update(code).digest('hex');
}
async ensureSaved(rpcClient, name) {
const script = this.scripts.get(name);
if (!script) throw new Error(`Unknown script: ${name}`);
if (!script.saved) {
await rpcClient.call('lua_savescript', [script.code]);
script.saved = true;
}
return script.hash;
}
async run(rpcClient, name, ...args) {
const hash = await this.ensureSaved(rpcClient, name);
return await rpcClient.call('lua_evalsaved', [hash, ...args]);
}
}
// Initialize on startup
const cache = new ScriptCache();
cache.register('getBalance', `
local addr = args[1]
local utxos = _RPC.esplora_addressutxo(addr)
local sum = 0
for _, u in ipairs(utxos) do sum = sum + u.value end
return sum
`);
// Use throughout application
const balance = await cache.run(rpc, 'getBalance', 'bc1q...');
```
---
## lua_evalscript
Execute a Lua script directly with optional arguments.
### Method
`lua_evalscript`
### Parameters
- **0** (string): Lua script source code
- **1+** (any): Optional arguments (accessible via `args` table)
### Request
```json
{
"jsonrpc": "2.0",
"method": "lua_evalscript",
"params": [
"local height = _RPC.btc_getblockcount()\nreturn { height = height, doubled = height * 2 }",
"arg1",
"arg2"
],
"id": 1
}
```
### Response
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 1,
"returns": {
"height": 925737,
"doubled": 1851474
},
"runtime": 45
},
"id": 1
}
```
### Response Fields
- **`calls`** (number): Number of RPC calls made during execution
- **`returns`** (any): Return value from the Lua script (converted to JSON)
- **`error`** (object/null): Error information if execution failed
- **`runtime`** (number): Execution time in milliseconds
### Error Response
When an error occurs during script execution:
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 2,
"returns": null,
"error": {
"code": -1,
"message": "attempt to index a nil value"
},
"runtime": 15
},
"id": 1
}
```
### Examples
#### Basic Script - Block Info
Get block height and hash:
```lua
local height = _RPC.btc_getblockcount()
local hash = _RPC.btc_getblockhash(height)
return { height = height, hash = hash }
```
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "lua_evalscript",
"params": [
"local height = _RPC.btc_getblockcount()\nlocal hash = _RPC.btc_getblockhash(height)\nreturn { height = height, hash = hash }"
],
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 2,
"returns": {
"height": 925737,
"hash": "000000000000000000001d43fb868a7cfd0581192f648e7bf000c94fe94c6c03"
},
"runtime": 62
},
"id": 1
}
```
---
#### Using Arguments - Address Balance
Calculate address balance from UTXOs:
```lua
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
local total = 0
local confirmed = 0
local unconfirmed = 0
for _, utxo in ipairs(utxos) do
total = total + utxo.value
if utxo.status.confirmed then
confirmed = confirmed + utxo.value
else
unconfirmed = unconfirmed + utxo.value
end
end
return {
address = address,
total_sats = total,
total_btc = total / 100000000,
confirmed_sats = confirmed,
unconfirmed_sats = unconfirmed,
utxo_count = #utxos
}
```
---
#### Multiple RPC Calls - Blockchain Summary
Get comprehensive blockchain info in one request:
```lua
local btc_height = _RPC.btc_getblockcount()
local ord_height = _RPC.ord_blockheight()
local metashrew_height = tonumber(_RPC.metashrew_height())
local fees = _RPC.esplora_feeestimates()
local mempool = _RPC.btc_getmempoolinfo()
return {
heights = {
bitcoin = btc_height,
ord = ord_height,
metashrew = metashrew_height
},
synced = {
ord = ord_height >= btc_height - 2,
metashrew = metashrew_height >= btc_height - 2
},
fees = {
next_block = fees["1"],
within_hour = fees["6"],
economy = fees["144"]
},
mempool = {
tx_count = mempool.size,
size_mb = mempool.bytes / 1000000
}
}
```
---
#### Error Handling with pcall
Safely handle RPC errors:
```lua
local txid = args[1]
local success, tx = pcall(function()
return _RPC.esplora_tx(txid)
end)
if not success then
return {
error = "Transaction not found",
txid = txid
}
end
return {
txid = tx.txid,
fee = tx.fee,
size = tx.size,
confirmed = tx.status.confirmed,
block_height = tx.status.block_height
}
```
---
#### Get Inscription Details
Fetch and process inscription data:
```lua
local inscription_id = args[1]
local insc = _RPC.ord_inscription(inscription_id)
return {
id = insc.id,
number = insc.number,
content_type = insc.content_type,
content_length = insc.content_length,
genesis_height = insc.height,
current_owner = insc.address,
sat = insc.sat,
rarity = insc.charms,
children = insc.child_count
}
```
---
#### Get Rune Info with Supply Calculation
```lua
local rune_name = args[1]
local rune = _RPC.ord_rune(rune_name)
local entry = rune.entry
local supply = entry.mints * (entry.terms and entry.terms.amount or 0)
return {
id = rune.id,
name = entry.spaced_rune,
symbol = entry.symbol,
divisibility = entry.divisibility,
total_mints = entry.mints,
supply = supply,
mintable = rune.mintable,
mint_terms = entry.terms
}
```
---
#### Check Address for Inscriptions and Runes
```lua
local address = args[1]
-- Get ordinals data
local ord_data = _RPC.ord_address(address)
-- Get UTXOs for balance
local utxos = _RPC.esplora_addressutxo(address)
local balance = 0
for _, utxo in ipairs(utxos) do
balance = balance + utxo.value
end
return {
address = address,
balance_sats = balance,
balance_btc = balance / 100000000,
inscriptions = ord_data.inscriptions,
runes = ord_data.runes,
utxo_count = #utxos
}
```
---
### JavaScript Example
```javascript
const script = `
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
local total = 0
for _, utxo in ipairs(utxos) do
total = total + utxo.value
end
return {
address = address,
balance_sats = total,
balance_btc = total / 100000000,
utxo_count = #utxos
}
`;
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': 'YOUR_API_KEY'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'lua_evalscript',
params: [script, 'bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h'],
id: 1
})
});
const data = await response.json();
console.log('Balance:', data.result.returns);
console.log('RPC calls made:', data.result.calls);
console.log('Runtime:', data.result.runtime, 'ms');
```
### Performance Tips
1. **Minimize RPC calls**: Each call adds latency
2. **Use local variables**: Faster than globals
3. **Return only needed data**: Smaller response = faster transfer
4. **Use pcall for error handling**: Prevents script termination on RPC errors
5. **Batch related queries**: Combine multiple checks into one script
---
## Lua Script Examples
Real-world Lua scripts demonstrating common use cases. These examples are from the Subfrost reference implementation.
### multicall.lua
Execute multiple RPC calls in a single batch request.
```lua
-- Execute multiple RPC calls in a single batch
-- Args: array of [method, params] tuples
-- Example: [["btc_getblockcount", []], ["btc_getblockhash", [100]]]
local results = {}
for i, call in ipairs(args) do
if type(call) ~= "table" or #call ~= 2 then
return {
error = "Each multicall entry must be a tuple of [method, params]",
index = i
}
end
local method = call[1]
local params = call[2]
if type(method) ~= "string" then
return { error = "Method name must be a string", index = i }
end
if type(params) ~= "table" then
return { error = "Method params must be an array", index = i }
end
local success, result = pcall(function()
local rpc_func = _RPC[method]
if not rpc_func then
error("Method not found: " .. method)
end
if #params == 0 then
return rpc_func()
elseif #params == 1 then
return rpc_func(params[1])
elseif #params == 2 then
return rpc_func(params[1], params[2])
else
local unpack_func = table.unpack or unpack
return rpc_func(unpack_func(params))
end
end)
if success then
table.insert(results, { result = result })
else
table.insert(results, { error = { message = tostring(result) } })
end
end
return results
```
**Usage:**
```json
{
"jsonrpc": "2.0",
"method": "lua_evalscript",
"params": [
"-- multicall script here --",
[["btc_getblockcount", []], ["btc_getblockhash", [850000]], ["esplora_feeestimates", []]]
],
"id": 1
}
```
---
### balances.lua
Comprehensive balance information for an address, combining UTXOs with ordinal and alkane data.
```lua
-- Comprehensive balance information for an address
-- Args: address, protocol_tag (optional, default: "1")
local address = args[1]
local protocol_tag = args[2] or "1"
-- Get ord and metashrew heights
local ord_height = _RPC.ord_blockheight() or 0
local metashrew_height_str = _RPC.metashrew_height() or "0"
local metashrew_height = tonumber(metashrew_height_str) or 0
local max_indexed_height = math.max(ord_height, metashrew_height)
-- Get UTXOs
local utxos = _RPC.esplora_addressutxo(address) or {}
-- Get protorunes/alkanes data
local protorunes = _RPC.alkanes_protorunesbyaddress({
address = address,
protocolTag = protocol_tag
}) or {}
-- Get ord outputs (inscriptions and runes)
local ord_outputs = _RPC.ord_outputs(address) or {}
-- Build lookup maps
local runes_map = {}
if protorunes.outpoints then
for _, outpoint in ipairs(protorunes.outpoints) do
if outpoint.outpoint and outpoint.runes then
local key = outpoint.outpoint.txid .. ":" .. outpoint.outpoint.vout
runes_map[key] = outpoint.runes
end
end
end
local ord_outputs_map = {}
for _, output in ipairs(ord_outputs) do
if output.outpoint then
ord_outputs_map[output.outpoint] = {
inscriptions = output.inscriptions or {},
ord_runes = output.runes or {}
}
end
end
-- Categorize UTXOs
local spendable = {}
local assets = {}
local pending = {}
for _, utxo in ipairs(utxos) do
local key = utxo.txid .. ":" .. utxo.vout
local height = utxo.status and utxo.status.block_height
local utxo_entry = {
outpoint = key,
value = utxo.value
}
if height then utxo_entry.height = height end
if runes_map[key] then utxo_entry.runes = runes_map[key] end
if ord_outputs_map[key] then
if #ord_outputs_map[key].inscriptions > 0 then
utxo_entry.inscriptions = ord_outputs_map[key].inscriptions
end
if next(ord_outputs_map[key].ord_runes) ~= nil then
utxo_entry.ord_runes = ord_outputs_map[key].ord_runes
end
end
local has_assets = (utxo_entry.runes and #utxo_entry.runes > 0) or
(utxo_entry.inscriptions and #utxo_entry.inscriptions > 0) or
(utxo_entry.ord_runes and next(utxo_entry.ord_runes) ~= nil)
local is_confirmed = height and height <= max_indexed_height
if not is_confirmed then
table.insert(pending, utxo_entry)
elseif has_assets then
table.insert(assets, utxo_entry)
else
table.insert(spendable, utxo_entry)
end
end
return {
spendable = spendable,
assets = assets,
pending = pending,
ordHeight = ord_height,
metashrewHeight = metashrew_height
}
```
---
### address_utxos_with_txs.lua
Batch fetch UTXOs with full transaction details.
```lua
-- Batch fetch UTXOs for an address with full transaction details
-- Args: address
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
if not utxos then
return { utxos = {}, error = "Failed to fetch UTXOs" }
end
local result = { utxos = {}, count = 0 }
for i, utxo in ipairs(utxos) do
local tx_data = _RPC.esplora_tx(utxo.txid)
local utxo_entry = {
txid = utxo.txid,
vout = utxo.vout,
value = utxo.value,
status = utxo.status,
tx = tx_data
}
table.insert(result.utxos, utxo_entry)
result.count = result.count + 1
end
return result
```
---
### batch_utxo_balances.lua
Fetch UTXOs with alkane balances for each.
```lua
-- Batch UTXO balance fetching for alkanes
-- Args: address, protocol_tag (default: 1), block_tag (optional)
local address = args[1]
local protocol_tag = args[2] or 1
local block_tag = args[3]
local utxos = _RPC.esplora_addressutxo(address)
if not utxos then
return { utxos = {}, error = "Failed to fetch UTXOs" }
end
local result = { utxos = {}, count = 0 }
for i, utxo in ipairs(utxos) do
local balance_response = _RPC.protorunes_by_outpoint(
utxo.txid,
utxo.vout,
block_tag,
protocol_tag
)
local utxo_entry = {
txid = utxo.txid,
vout = utxo.vout,
value = utxo.value,
status = utxo.status,
balances = {}
}
if balance_response and balance_response.balance_sheet then
local cached = balance_response.balance_sheet.cached
if cached and cached.balances then
for alkane_id, amount in pairs(cached.balances) do
table.insert(utxo_entry.balances, {
block = alkane_id.block,
tx = alkane_id.tx,
amount = amount
})
end
end
end
table.insert(result.utxos, utxo_entry)
result.count = result.count + 1
end
return result
```
---
### Custom Examples
#### Get Inscription Content
```lua
local inscription_id = args[1]
local inscription = _RPC.ord_inscription(inscription_id)
if not inscription then
return { error = "Inscription not found" }
end
local content = _RPC.ord_content(inscription_id)
return {
id = inscription.id,
number = inscription.number,
content_type = inscription.content_type,
content_length = inscription.content_length,
content_base64 = content,
owner = inscription.address,
sat = inscription.sat
}
```
#### Monitor Rune Minting Progress
```lua
local rune_name = args[1]
local rune = _RPC.ord_rune(rune_name)
if not rune or not rune.entry then
return { error = "Rune not found" }
end
local entry = rune.entry
local mints = tonumber(entry.mints) or 0
local cap = entry.terms and entry.terms.cap or 0
return {
name = entry.spaced_rune,
symbol = entry.symbol,
mints = mints,
cap = cap,
progress_percent = cap > 0 and (mints / cap * 100) or 100,
remaining = cap - mints,
fully_minted = mints >= cap
}
```
#### Check Transaction Confirmation
```lua
local txid = args[1]
local required_confirmations = args[2] or 6
local current_height = _RPC.btc_getblockcount()
local tx = _RPC.esplora_tx(txid)
if not tx then
return { error = "Transaction not found", txid = txid }
end
local confirmations = 0
if tx.status and tx.status.confirmed and tx.status.block_height then
confirmations = current_height - tx.status.block_height + 1
end
return {
txid = txid,
confirmed = tx.status.confirmed,
block_height = tx.status.block_height,
confirmations = confirmations,
meets_requirement = confirmations >= required_confirmations,
required = required_confirmations
}
```
---
## Lua Scripting Overview
Subfrost supports server-side Lua script execution, enabling complex operations with minimal API calls. Scripts have full access to RPC methods through the `_RPC` global table.
### Why Lua Scripting?
1. **Reduce Round Trips**: Execute multiple RPC calls in a single request
2. **Server-side Logic**: Process data before returning
3. **Atomic Operations**: Combine related queries
4. **Reusable Scripts**: Save and execute scripts by hash
### Available Methods
- **`lua_evalscript`**: Execute a Lua script directly
- **`lua_savescript`**: Save a script and get its SHA256 hash
- **`lua_evalsaved`**: Execute a previously saved script
### Quick Example
```json
{
"jsonrpc": "2.0",
"method": "lua_evalscript",
"params": [
"local height = _RPC.btc_getblockcount()\nlocal hash = _RPC.btc_getblockhash(height)\nreturn { height = height, hash = hash }",
"arg1"
],
"id": 1
}
```
Response:
```json
{
"jsonrpc": "2.0",
"result": {
"calls": 2,
"returns": {
"height": 925741,
"hash": "00000000000000000000c68f2b5a8fe69a1b9304ace4f2d8a63eefc5706d6c11"
},
"runtime": 62
},
"id": 1
}
```
### The _RPC Global Table
All RPC methods are available through `_RPC`. The naming follows a flat pattern:
#### Bitcoin Core Methods
```lua
_RPC.btc_getblockcount()
_RPC.btc_getblockhash(height)
_RPC.btc_getbestblockhash()
_RPC.btc_getblock(hash, verbosity)
_RPC.btc_getblockheader(hash, verbose)
_RPC.btc_getblockchaininfo()
_RPC.btc_getdifficulty()
_RPC.btc_getrawtransaction(txid, verbose)
_RPC.btc_sendrawtransaction(hex)
_RPC.btc_getmempoolinfo()
_RPC.btc_getrawmempool(verbose)
```
#### Esplora Methods
```lua
_RPC.esplora_address(address)
_RPC.esplora_addressutxo(address)
_RPC.esplora_addresstxs(address)
_RPC.esplora_tx(txid)
_RPC.esplora_txstatus(txid)
_RPC.esplora_txhex(txid)
_RPC.esplora_txoutspends(txid)
_RPC.esplora_block(hash)
_RPC.esplora_blockheight(height)
_RPC.esplora_blocktxids(hash)
_RPC.esplora_feeestimates()
_RPC.esplora_mempool()
_RPC.esplora_mempoolrecent()
```
#### Ord Methods
```lua
_RPC.ord_blockcount()
_RPC.ord_blockhash(height)
_RPC.ord_blocktime()
_RPC.ord_inscription(id)
_RPC.ord_inscriptions(page)
_RPC.ord_rune(name)
_RPC.ord_runes(page)
_RPC.ord_sat(number)
_RPC.ord_output(outpoint)
```
#### Metashrew/Alkanes Methods
```lua
_RPC.metashrew_height()
_RPC.metashrew_view(method, input, block_tag)
_RPC.alkanes_protorunesbyaddress({ address = "...", protocolTag = "1" })
```
### Accessing Arguments
Arguments passed to the script are available in the `args` table (1-indexed):
```lua
local address = args[1]
local limit = args[2] or 10
local utxos = _RPC.esplora_addressutxo(address)
local result = {}
for i = 1, math.min(#utxos, limit) do
table.insert(result, utxos[i])
end
return result
```
### Response Format
All Lua methods return a standardized response:
```json
{
"calls": 5,
"returns": { /* script return value */ },
"runtime": 123
}
```
- **`calls`** (number): Number of RPC calls made
- **`returns`** (any): Script return value (JSON)
- **`error`** (object/null): Error info if execution failed
- **`runtime`** (number): Execution time in milliseconds
### Error Handling
```lua
local success, result = pcall(function()
return _RPC.btc_getblock("invalidhash", 1)
end)
if not success then
return { error = "Block not found" }
end
return result
```
### Lua Version & Libraries
Scripts run on Lua 5.4 with these libraries:
- Basic operations (`type`, `pairs`, `ipairs`, `tostring`, `tonumber`)
- `table` library (`table.insert`, `table.remove`, `table.sort`, etc.)
- `string` library (`string.sub`, `string.find`, `string.format`, etc.)
- `math` library (`math.floor`, `math.max`, `math.min`, etc.)
- `pcall`/`error` for error handling
**Not available:**
- File I/O (`io` library)
- Network access (except via `_RPC`)
- `os` library
- `debug` library
### Next Steps
- [lua_evalscript](./evalscript): Execute scripts
- [lua_savescript](./savescript): Save reusable scripts
- [lua_evalsaved](./evalsaved): Run saved scripts
- [Example Scripts](./examples): Complete examples
---
## lua_savescript
Save a Lua script to the server and receive its SHA256 hash for future execution.
### Method
`lua_savescript`
### Parameters
- **0** (string): Lua script source code
### Request
```json
{
"jsonrpc": "2.0",
"method": "lua_savescript",
"params": [
"local addr = args[1]\nlocal utxos = _RPC.esplora_addressutxo(addr)\nreturn #utxos"
],
"id": 1
}
```
### Response
```json
{
"jsonrpc": "2.0",
"result": {
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
},
"id": 1
}
```
### Response Fields
- **`hash`** (string): SHA256 hash of the script (64 hex characters)
### Usage Pattern
#### 1. Save the Script
```json
{
"jsonrpc": "2.0",
"method": "lua_savescript",
"params": [
"local address = args[1]\nlocal utxos = _RPC.esplora_addressutxo(address)\nlocal total = 0\nfor _, u in ipairs(utxos) do total = total + u.value end\nreturn { balance = total, count = #utxos }"
],
"id": 1
}
```
Response:
```json
{
"jsonrpc": "2.0",
"result": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"id": 1
}
```
#### 2. Execute the Saved Script
```json
{
"jsonrpc": "2.0",
"method": "lua_evalsaved",
"params": [
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
],
"id": 2
}
```
### Benefits of Saved Scripts
1. **Reduced Bandwidth**: Send 64-char hash instead of full script
2. **Faster Parsing**: Script is pre-parsed on save
3. **Consistent Execution**: Same script runs every time
4. **Version Control**: Hash changes if script changes
### JavaScript Example
```javascript
class LuaScriptManager {
constructor(apiKey) {
this.apiKey = apiKey;
this.savedScripts = new Map();
}
async rpc(method, params) {
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': this.apiKey
},
body: JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 })
});
const data = await response.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
async saveScript(name, code) {
const result = await this.rpc('lua_savescript', [code]);
this.savedScripts.set(name, result.hash);
return result.hash;
}
async runSaved(name, ...args) {
const hash = this.savedScripts.get(name);
if (!hash) throw new Error(`Script "${name}" not saved`);
return await this.rpc('lua_evalsaved', [hash, ...args]);
}
}
// Usage
const scripts = new LuaScriptManager('YOUR_API_KEY');
// Save once
await scripts.saveScript('getBalance', `
local address = args[1]
local utxos = _RPC.esplora_addressutxo(address)
local total = 0
for _, u in ipairs(utxos) do total = total + u.value end
return { balance = total, count = #utxos }
`);
// Run many times
const balance1 = await scripts.runSaved('getBalance', 'bc1q...');
const balance2 = await scripts.runSaved('getBalance', 'bc1p...');
```
### Storage Notes
- Scripts are stored in-memory on the server
- Scripts may be cleared on server restart
- Use client-side caching for production applications
- Consider re-saving scripts on application startup
### Hash Computation
The hash is computed as SHA256 of the script content:
```javascript
const crypto = require('crypto');
function computeScriptHash(script) {
return crypto.createHash('sha256').update(script).digest('hex');
}
// You can pre-compute hashes locally
const script = "return _RPC.btc_getblockcount()";
const hash = computeScriptHash(script);
console.log(hash); // Use this to check if script is already saved
```
### Error Cases
#### Script Too Large
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Script exceeds maximum size limit"
},
"id": 1
}
```
#### Invalid Lua Syntax
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Lua syntax error: unexpected symbol near 'end'"
},
"id": 1
}
```
---
## Ethereum Mempool (Preview)
> **Preview.** Live as of 2026-08-21. The Ethereum sibling of the
> [Bitcoin mempool](./overview): same protocol, ETH transactions.
A live view of the **Ethereum public mempool**, ingested straight off the
`eth` devp2p wire network (not a single node's `eth_subscribe`). You sit among
many peers and often see a transaction (including a SUBFROST `depositAndBridge`)
**before it is mined**, and before any one RPC provider's pool surfaces it.
```
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/mempool # JSON-RPC
GET wss://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/mempool/ws # change stream
```
Both `/v4` and `/v5` work. This is a **public-mempool** view: direct-to-builder
/ private orderflow (Flashbots, exclusive builder RPCs) never propagates over
devp2p, so it structurally misses some gas. That is fine for deposit-tracking, not a
substitute for a builder feed.
### The record
Every transaction is summarised as a `TxRecord`. The full calldata is fetched on
demand, not held resident: you get its length and 4-byte selector.
| field | type | notes |
|---|---|---|
| `hash` | `0x…32` | transaction hash |
| `from` | `0x…20` | recovered signer |
| `to` | `0x…20` \| `null` | `null` for contract creation |
| `nonce` | number | |
| `value` | string | wei (decimal string, may exceed 2⁵³) |
| `gas_limit` | number | |
| `max_fee_per_gas` | string | wei |
| `max_priority_fee_per_gas` | string | wei |
| `tx_type` | number | 0 legacy, 2 EIP-1559, 3 blob, … |
| `input_len` | number | calldata length in bytes |
| `selector` | `0x…4` | first 4 calldata bytes (`""` if none) |
| `first_seen_ms` | number | unix ms we first saw it |
| `seq` | number | monotonic per-instance sequence |
### JSON-RPC
JSON-RPC 2.0. **`params` is an OBJECT**, not a positional array (a positional
array returns `-32602`).
```bash
curl -s https://mainnet.subfrost.io/v4/YOUR_API_KEY/ethereum/mempool \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"mempool_info","params":{}}'
```
| method | params | returns |
|---|---|---|
| `mempool_info` | `{}` | `{count, added_total, removed_total, subscribers, seq}` |
| `mempool_txids` | `{limit}` | `{txids:[0x…], count, seq}` |
| `mempool_entry` | `{txid}` | the `TxRecord`, or `-32000` if not resident |
| `mempool_recent` | `{limit}` | `{entries:[TxRecord], seq}`, newest first |
| error | meaning |
|---|---|
| `-32601` | unknown method |
| `-32602` | invalid params (e.g. positional array) |
| `-32000` | not found: the txid is not in the mempool |
### WebSocket change stream
Connect to `…/ethereum/mempool/ws`. Text frames, one JSON object each; **every
frame carries `seq`** so you can detect gaps.
```
→ {"op":"subscribe","channels":["tx","block"]}
← {"type":"hello","instance":"a3f1c07e","seq":40213}
← {"type":"add","record":{ …TxRecord… },"seq":40214}
← {"type":"remove","hash":"0x…","reason":"mined","seq":40219}
← {"type":"block","number":21234567,"hash":"0x…","removed":41,"seq":40260}
```
- Client ops: `subscribe` / `unsubscribe` (`channels`: `"tx"`, `"block"`) and `ping`.
- Server frames: `hello` (first), `add` (a `TxRecord`), `remove{hash,reason}`,
`block{number,hash,removed}`, `reset{reason,action:"resync"}`, `pong`.
- **Backpressure:** a client too slow to drain falls off the broadcast ring and
receives `reset{action:"resync"}`; the server never buffers on its heap. On a
`reset`, re-fetch state with `mempool_recent` and resume from the new `seq`.
### Tracking a crosschain deposit
To surface "**Deposit pending on Ethereum**" the moment a user's bridge tx hits
the mempool, subscribe to `tx` and match `add` frames whose:
- `to` == the frUSD vault `0x95779e7e1c943042255b8a78273fe6de4823cf06`, and
- `selector` == `0xbedb65ee` (`depositAndBridge`).
That fires *before* the transaction is mined; the confirmed on-chain record then
appears in the [frUSD Deposits index](../json-rpc/frusd) once it lands.
### Caveats
- **Public mempool only**: misses private/builder orderflow (see above).
- **Preview**: the shape is stable but the service is young; treat gaps as
possible and lean on `seq` + `reset` to resync rather than assuming a perfect
stream.
---
## Mempool JSON-RPC
> **Preview.** Live as of 2026-08-11. See the
> [overview](./overview) for the data model.
```
POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool
```
JSON-RPC 2.0. Single requests and arrays (batches) are both accepted.
> **`params` is an OBJECT, not a positional array.** This differs from the
> `esplora_*` and `metashrew_*` families on the same host. A positional array
> returns `-32602`.
```bash
curl -s https://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"mempool_info","params":{}}'
```
### Errors
| code | meaning |
|---|---|
| `-32700` | parse error |
| `-32600` | invalid request |
| `-32601` | unknown method |
| `-32602` | invalid params |
| `-32000` | not found: the txid is not in the mempool |
HTTP-level: `401 INVALID_API_KEY`, `403` when admin is required,
`503 AUTH_UNAVAILABLE` when the key store cannot be consulted.
> **`503 AUTH_UNAVAILABLE` is a refusal, not an outage to retry through.** This
> service authenticates itself rather than relying on an upstream gate, and it
> fails closed: if it cannot check your key it serves nobody.
---
### `mempool_info`
No params. The cheap poll.
```json
{
"count": 142331, "bytes": 78412992, "vbytes": 41203118,
"total_fee": 812340000, "fee_known": 141002, "fee_coverage": 0.9906,
"provisional": 3, "seq": 918273, "instance": "a3f1c07e",
"added_total": 4102773, "removed_total": 3960442, "subscribers": 12
}
```
`fee_coverage` is `0.0..=1.0`. See the [overview](./overview#fees-are-sometimes-absent-never-zero).
---
### `mempool_txids`
```json
{ "limit": 1000 }
```
Clamped to 5000.
```json
{ "txids": ["…"], "count": 142331, "seq": 918273 }
```
> **Order is unspecified and unstable.** It reflects internal slab order, which
> changes as slots recycle. Sort client-side if you need one; do not treat two
> consecutive calls as comparable sequences.
---
### `mempool_entry`
```json
{ "txid": "", "include_hex": false }
```
```json
{
"txid": "…", "wtxid": "…",
"vsize": 141, "weight": 561,
"fee": 1410, "feerate": 10.0,
"first_seen": 1754770000123,
"seq": 918001,
"origin": "p2p", "provisional": false, "rbf": true
}
```
`first_seen` is when **we** first saw it, not when the node did. The two differ
by the arrival latency of whichever source got there first. `fee` and `feerate`
are absent when unknown.
Returns `-32000` when the transaction is not in the mempool.
---
### `mempool_entries`
```json
{ "txids": ["…", "…"], "include_hex": false }
```
At most 5000 per call.
```json
{ "entries": [ … ], "missing": ["…"] }
```
Absent txids come back in `missing` rather than erroring the whole batch.
---
### `mempool_raw`
```json
{ "txid": "…" }
```
```json
{ "hex": "0100…" }
```
---
### `mempool_recent`
```json
{ "limit": 100 }
```
The most recently arrived entries, **newest first**.
```json
{ "entries": [ … ], "seq": 918273 }
```
This is the animation seed: paint from this, then follow the
[WebSocket](./websocket) from the returned `seq`.
---
### `mempool_snapshot`
```json
{ "limit": 1000, "cursor": 0, "include_hex": false }
```
```json
{
"entries": [ … ], "cursor": 917400, "has_more": true,
"seq": 918273, "count": 142331, "instance": "a3f1c07e"
}
```
> **`cursor` is a sequence number, not an offset.** Paging a set that mutates
> under you by offset skips and repeats entries; paging by `seq` is stable
> because a `seq` is never reused. Pass the returned `cursor` back for the next
> page.
Use the returned `seq` and `instance` as your WebSocket `since_seq` to attach
without a gap.
---
### `mempool_feerate_histogram`
```json
{ "buckets": [0, 1, 2, 5, 10, 20, 50] }
```
Bucket **floors** in sat/vB, ascending. Defaults to a 24-bucket geometric scale.
```json
{
"histogram": [[0.0, 0], [1.0, 412000], [2.0, 1840221]],
"fee_coverage": 0.9906,
"count": 142331
}
```
Each pair is `[bucket_floor_sat_per_vb, vbytes]`.
> **Only fee-known entries contribute, so read `fee_coverage` first.** A
> freshly started service has a thin histogram, and deriving an estimate from it
> without checking quotes the floor. The transaction still broadcasts, still
> looks fine, and simply does not confirm.
---
### `observe` *(admin only)*
```
POST /v4/YOUR_ADMIN_API_KEY/mempool/observe
```
**Not** JSON-RPC. Body is `{"hex":"0100…"}` or the raw transaction bytes.
Requires an api key flagged admin. The transaction enters the store with
`origin: "edge"` and `provisional: true`.
> **This does not broadcast.** The transaction is not forwarded to bitcoind;
> the endpoint holds no such capability. It exists so a broadcast made elsewhere
> shows up in the stream immediately rather than after relay latency. Something
> else must actually send it.
---
## Mempool API
> **Preview.** Live as of 2026-08-11 on `mainnet.subfrost.io`. Single replica,
> so `instance` changes on every restart and your `seq` cursor resets with it.
> Handle `hello` as described under
> [reconnecting](./websocket#reconnecting-without-a-gap).
A live view of the Bitcoin mempool, served two ways:
| | |
|---|---|
| **JSON-RPC** | `POST /v4/YOUR_API_KEY/mempool` |
| **WebSocket** | `GET /v4/YOUR_API_KEY/mempool/ws` |
`/v5/YOUR_API_KEY/mempool` is accepted identically.
The WebSocket is the point. Polling a mempool projection every few seconds tells
you what it *was*; a change stream with resumable sequence numbers lets you
animate what it *is*.
### The model
The service peers directly to our own bitcoind and keeps the mempool in memory.
Transactions arrive from two sources, and each entry records which one saw it
first:
| `origin` | source | latency |
|---|---|---|
| `edge` | observed at our edge as a user broadcast it | immediate, **provisional** |
| `p2p` | `inv` → `getdata` → `tx` over the peer connection | seconds |
Ingest is Bitcoin p2p and nothing else. There is no ZMQ subscriber and no
bitcoind JSON-RPC client: the process holds no node credential at all. Two
consequences worth planning around rather than discovering:
* **Announcements cost seconds, not milliseconds.** Core batches `inv` to
inbound peers on a Poisson timer. If you need to see your own broadcast
instantly, that is what the `edge` origin is for.
* **Core never tells us it removed something.** Mined is recovered from blocks,
and an RBF replacement is inferred locally from our own spend index. Node-side
expiry is simply unobservable, so our view can drift *upward* against the
node's; we may hold a transaction it quietly dropped, bounded only by our own
age sweep.
`origin` only ever upgrades in that order. An `edge`-observed transaction is
marked `provisional: true` until the node corroborates it. The user's broadcast
reached *us*, which is not the same as the node having accepted it. Provisional
entries that are never corroborated are removed with reason
`provisional_expired`.
> **Do not treat a provisional entry as confirmed-in-mempool.** It means "we saw
> this transaction", not "the network will relay it". A transaction can be
> malformed, non-standard, or conflict with something already in the pool, and
> you will find out only when it fails to corroborate.
### Sequence numbers
Every entry and every WebSocket frame carries a `seq`, a strictly increasing,
**per-process** counter. It is how you page a mutating set without gaps and how
you reattach a stream without missing changes.
Each process also has an `instance` id. **If `instance` changes, every `seq` you
hold is meaningless**: you are talking to a different process, whose counter
describes a different history. Resync unconditionally. The protocol makes this
detectable on purpose rather than leaving you to notice numbers going backwards.
### Byte order
Every txid, wtxid and block hash on the wire (JSON-RPC and WebSocket alike) is
**display order**, matching what bitcoind's RPC and every block explorer show.
No reversal is needed on your side.
### Fees are sometimes absent, never zero
`fee` and `feerate` are **omitted** from an entry when the absolute fee is not
known, rather than reported as `0`. p2p carries neither fees nor input values, so
a transaction's fee is only known once it is hydrated: immediately when every
input spends another mempool transaction, and otherwise once our own index has
been asked for the missing prevouts.
`mempool_info` reports `fee_coverage`, the fraction of resident entries whose
fee is known.
> **Check `fee_coverage` before deriving a fee estimate.** A freshly started
> service has a thin fee-rate histogram, and a naive read of it quotes the floor.
> Underpaying because the data was incomplete is indistinguishable, at the call
> site, from underpaying because the mempool was empty.
### Where to go next
- **[JSON-RPC methods](./jsonrpc)**: queries, snapshots, the
fee-rate histogram.
- **[WebSocket stream](./websocket)**: frames, resync, backpressure.
The usual pattern is both: seed from `mempool_recent` or `mempool_snapshot`,
then follow the WebSocket from the `seq` it returned.
---
## Mempool WebSocket
> **Preview.** Live as of 2026-08-11. See the
> [overview](./overview) for the data model.
```
wss://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool/ws
```
Plain JSON over WebSocket, no subprotocol. Text frames, one JSON object each.
The api key is in the path and is checked on the upgrade itself.
```js
const ws = new WebSocket(
"wss://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool/ws"
);
```
### Subscribe
```json
{"op":"subscribe","channels":["tx","block","info"],"since_seq":91827,"include_hex":false}
{"op":"unsubscribe","channels":["block"]}
{"op":"ping"}
```
`channels` defaults to all; `["*"]` and `["all"]` are accepted.
| channel | frames |
|---|---|
| `tx` | `add`, `remove` |
| `block` | `block` |
| `info` | reserved |
Control frames (`hello`, `reset`, `pong`) are **never** filtered out, whatever
you subscribed to. Unknown ops and malformed JSON get a `reset` frame carrying
the reason rather than being silently ignored.
### Frames
Every frame carries `seq`.
```json
{"type":"hello","instance":"a3f1c07e","seq":91827,"count":142331,"replayed":false}
{"type":"add","seq":91828,"txid":"…","wtxid":"…","vsize":141,"weight":561,
"fee":1410,"feerate":10.0,"first_seen":1754770000123,
"origin":"p2p","provisional":false,"rbf":true}
{"type":"remove","seq":91829,"txid":"…","vsize":141,"reason":"mined"}
{"type":"block","seq":91830,"height":961541,"hash":"0000…","removed":1843}
{"type":"reset","seq":91831,"reason":"lagged","action":"resync"}
{"type":"pong","seq":91831}
```
#### Why a transaction left
`remove.reason` distinguishes five cases, and they are not interchangeable:
| reason | meaning |
|---|---|
| `mined` | included in a block we received over p2p |
| `replaced` | superseded by a conflicting transaction (BIP125), inferred from our own spend index |
| `evicted` | **we** hit our own cap and dropped the lowest feerate |
| `provisional_expired` | an edge-observed transaction the node never corroborated |
| `expired` | resident longer than our own age bound |
> **`evicted` is about us, not the network.** It means this service hit its own
> memory cap and dropped the lowest-feerate entry. The transaction may be
> perfectly healthy and still in every other node's mempool. Do not surface it
> to a user as "dropped".
There is no `node_removed` and no `reconciled`. Both were artefacts of asking
bitcoind directly (the ZMQ `sequence` `R` label and a `getrawmempool` set
difference), and ingest is now p2p only, so Core never tells us it dropped
anything. `expired` is our own age bound and the only remaining defence against
an entry the node quietly evicted lingering here; it is a *backstop*, not a
report of what the node did.
A `block` frame is an aggregate. Per-transaction `remove` frames with
`reason: "mined"` are still emitted, and always **before** the `block` frame.
### Reconnecting without a gap
`hello` is always the first frame, and it carries `instance`.
1. **`instance` differs from last time** → full resync, unconditionally,
whatever `since_seq` you were about to send. You are talking to a different
process and its sequence numbers describe a different history.
2. **`since_seq` at or beyond the current `seq`** → you are caught up, go live.
3. **`since_seq` behind** → `{"type":"reset","action":"resync"}`. Re-fetch
[`mempool_snapshot`](./jsonrpc#mempool_snapshot), then resubscribe
with the `seq` it returned.
```
connect → read hello
├─ instance changed? → mempool_snapshot (paginate) → subscribe since_seq = snapshot.seq
└─ instance same → subscribe since_seq =
└─ got `reset`? → snapshot, then subscribe again
```
> **Historical replay is not implemented yet**, so case 3 currently always
> resyncs rather than replaying the frames you missed. Write the client to
> handle `reset` regardless: it is also how backpressure is signalled, below,
> and that path is permanent.
### Backpressure
There is **no per-connection outbound queue** beyond a shared broadcast ring. A
client that cannot keep up falls off the back of it and receives:
```json
{"type":"reset","seq":…,"reason":"lagged","action":"resync"}
```
The connection stays open. Resync and carry on.
> **This is deliberate, and it is why the service will not grow a heap on your
> behalf.** Buffering per-client so a slow browser tab never misses a frame is
> exactly how the previous mempool implementation ran out of memory. A `reset`
> you can recover from beats an out-of-memory kill nobody can.
### Worked example
```js
const RPC = "https://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool";
let instance = null, lastSeq = 0;
async function snapshot() {
const r = await fetch(RPC, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0", id: 1, method: "mempool_recent",
params: { limit: 500 },
}),
}).then((r) => r.json());
paint(r.result.entries);
return r.result.seq;
}
function connect() {
const ws = new WebSocket("wss://mainnet.subfrost.io/v4/YOUR_API_KEY/mempool/ws");
ws.onmessage = async (ev) => {
const f = JSON.parse(ev.data);
switch (f.type) {
case "hello":
// A changed instance invalidates every seq we hold.
if (instance && f.instance !== instance) lastSeq = 0;
instance = f.instance;
if (!lastSeq) lastSeq = await snapshot();
ws.send(JSON.stringify({ op: "subscribe", since_seq: lastSeq }));
return;
case "reset":
lastSeq = await snapshot();
ws.send(JSON.stringify({ op: "subscribe", since_seq: lastSeq }));
return;
case "add": lastSeq = f.seq; onAdd(f); return;
case "remove": lastSeq = f.seq; onRemove(f); return;
case "block": lastSeq = f.seq; onBlock(f); return;
}
};
ws.onclose = () => setTimeout(connect, 1000);
}
```
---
## Orderbook (Beta)
> **⚠️ Beta: new features in progress.** The Orderbook API is under active
> development. Endpoints, payload shapes, market identifiers, and semantics may
> change without notice. Treat it as preview; don't build production-critical
> flows on it yet.
The Subfrost **Orderbook API** is a live read + stream surface over the
off-chain **PSBT order books** that settle on Bitcoin via alkanes. It gives you
real-time snapshots (REST) and a firehose (WebSocket) of open orders across two
sources:
- **orbitals**: orbital NFT ↔ DIESEL listings and offers (module / collection markets)
- **lending**: pre-signed loan-offer PSBTs, creditor/debitor (asset-pair markets)
Subfrost is the source of truth for the underlying orders; this API is a
low-latency, read-optimized replica, so a delta you receive on the WebSocket is
always durably queryable via REST.
### Base URL
Every orderbook endpoint lives under your API key:
```
https://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook
```
Authentication is the same as the rest of the platform: the API key is a path
segment (see [Authentication](../getting-started/authentication)). Reads accept any active
key.
### Market model
Markets are addressed by a single flat **topic** string:
```
:::
```
| Scope | Meaning | Example topic |
|-------|---------|---------------|
| `book` | the whole book for a source | `orbitals:mainnet:book:*` |
| `module` | an orbitals module | `orbitals:mainnet:module:fire-pos` |
| `collection` | an orbitals collection (alkane id) | `orbitals:mainnet:collection:2:100` |
| `loan-token` | all lending offers lending a token | `lending:mainnet:loan-token:2:0` |
| `pair` | a directional lending pair `(loan_collateral)` | `lending:mainnet:pair:2:0_32:0` |
> Pair topics are **directional** (`loanId_collateralId`) and are **not**
> sorted. Colons inside the `key` (alkane ids like `2:0`) are part of the topic;
> treat the whole topic as an opaque string.
Every order carries the full set of topics it belongs to, so a single
subscription (e.g. a pair) receives exactly the orders in that market, and the
whole-book topic receives everything.
### The order envelope
All orders (orbitals and lending, listings and offers) are normalized into one
shape:
```json
{
"source": "lending",
"id": "clx…",
"network": "mainnet",
"kind": "offer",
"marketKey": "lending:mainnet:pair:2:0_32:0",
"topics": ["lending:mainnet:book:*", "lending:mainnet:loan-token:2:0", "lending:mainnet:pair:2:0_32:0"],
"side": "creditor",
"makerAddress": "bc1q…",
"status": "open",
"price": "1200",
"priceUnit": "apr-bp",
"assets": [
{ "role": "loan", "tokenId": "2:0", "amountRaw": "1000000" },
{ "role": "collateral", "tokenId": "32:0", "amountRaw": "5" }
],
"psbt": "cHNidP8B…",
"createdAt": 1784592000000000,
"updatedAt": 1784592000000000,
"ext": { "aprBp": "1200", "durationBlocks": "144" }
}
```
| Field | Notes |
|-------|-------|
| `source` | `orbitals` \| `lending` |
| `kind` | `listing` \| `offer` |
| `side` | `sell`/`buy` (orbitals) or `creditor`/`debitor` (lending) |
| `status` | normalized: `open` · `matched` · `pending` · `filled` · `taken` · `cancelled` · `expired` · `removed` |
| `price` / `priceUnit` | orbitals: `DIESEL`; lending: `apr-bp`. String-encoded (may exceed 2⁵³). |
| `assets[]` | `{ role?, tokenId, amountRaw? }`: the token legs of the order |
| `psbt` | base64 PSBT for offers (the signable/settleable transaction) |
| `createdAt` / `updatedAt` | epoch **microseconds** |
| `ext` | source-specific fields (raw status, module data, apr/duration, …) |
Next: [REST endpoints](./rest) · [WebSocket firehose](./websocket).
---
## Orderbook REST
> **⚠️ Beta.** Shapes may change. See the [overview](./overview)
> for the market model and order envelope.
All endpoints are under `https://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook`.
Reads are `GET` and accept any active API key.
### `GET /orderbook/markets`
List the active markets with open-order counts.
```bash
curl "https://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/markets"
```
```json
{
"markets": [
{ "source": "lending", "network": "mainnet",
"market_key": "lending:mainnet:pair:2:0_32:0", "open_orders": 3 },
{ "source": "orbitals", "network": "mainnet",
"market_key": "orbitals:mainnet:module:fire-pos", "open_orders": 12 }
]
}
```
### `GET /orderbook/snapshot?market=`
The current active book for a market topic. Returns non-terminal orders
(`open` / `matched` / `pending`).
```bash
curl "https://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/snapshot?market=lending:mainnet:pair:2:0_32:0"
```
```json
{
"market": "lending:mainnet:pair:2:0_32:0",
"orders": [ /* array of normalized order envelopes */ ]
}
```
Use any topic scope: a whole book (`orbitals:mainnet:book:*`), a module, a
collection, a loan-token, or a pair.
### `GET /orderbook/order/{source}/{id}`
Fetch a single order by its source + id.
```bash
curl "https://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/order/lending/clx123abc"
```
Returns the normalized order envelope, or `404` if unknown.
### Errors
| Status | Meaning |
|--------|---------|
| `401` | invalid / inactive API key |
| `404` | order not found |
---
## Orderbook WebSocket
> **⚠️ Beta.** Protocol may change. See the
> [overview](./overview) for the market model and order envelope.
The WebSocket firehose streams a snapshot of a market followed by live deltas as
orders are created, updated, filled, or removed. It's the low-latency way to
keep a local order book in sync.
### Connect
```
wss://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/ws
```
Plain JSON over WebSocket (no subprotocol). Cloudflare-proxied, so a standard
browser `WebSocket` or any WS client works.
```js
const ws = new WebSocket(
"wss://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/ws"
);
```
### Subscribe
Send a `subscribe` command with a market topic (see the
[market model](./overview#market-model)). You may subscribe to
multiple markets on one connection.
```json
{ "action": "subscribe", "market": "lending:mainnet:pair:2:0_32:0" }
```
Immediately after subscribing you receive a **snapshot** of that market's active
orders, then **deltas** as they happen.
```json
// snapshot
{ "type": "snapshot", "market": "lending:mainnet:pair:2:0_32:0",
"orders": [ /* normalized order envelopes */ ] }
// an order was created or changed
{ "type": "order", "op": "upsert", "order": { /* envelope */ } }
// an order left the active book (filled / cancelled / expired / pruned)
{ "type": "order", "op": "remove",
"source": "lending", "id": "clx123abc",
"marketKey": "lending:mainnet:pair:2:0_32:0" }
```
Unsubscribe at any time:
```json
{ "action": "unsubscribe", "market": "lending:mainnet:pair:2:0_32:0" }
```
### Keep-alive
The server sends `{ "type": "ping" }` every ~25s. Treat a long gap with no
frames as a dropped connection and reconnect (then re-subscribe + re-snapshot).
### Keeping a book in sync
1. Connect and `subscribe` to your market topic(s).
2. Seed your local book from the `snapshot`.
3. Apply each `upsert` (add/replace by `id`) and `remove` (delete by `id`).
4. On reconnect, discard local state and re-seed from a fresh snapshot; the
snapshot is always authoritative.
### Example
```js
const ws = new WebSocket("wss://mainnet.subfrost.io/v4/YOUR_API_KEY/orderbook/ws");
const book = new Map();
ws.onopen = () =>
ws.send(JSON.stringify({ action: "subscribe", market: "orbitals:mainnet:module:fire-pos" }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "snapshot") {
book.clear();
for (const o of msg.orders) book.set(o.id, o);
} else if (msg.type === "order") {
if (msg.op === "upsert") book.set(msg.order.id, msg.order);
else if (msg.op === "remove") book.delete(msg.id);
}
// msg.type === "ping" → keep-alive, ignore
};
```
---
## API Keys
API keys are the primary method for authenticating with the Subfrost API. Each key is unique to your account and can be used to track usage and apply rate limits.
### Creating an API Key
1. Navigate to your [Dashboard](https://api.subfrost.io/dashboard)
2. Click the "Create API Key" button
3. Enter a descriptive name (e.g., "Production Server", "Development")
4. Copy the generated key immediately, it won't be shown again
```
YOUR_API_KEY
```
API keys are 32-character hexadecimal strings.
:::warning Important
Store your API key securely. If compromised, revoke it immediately and create a new one.
:::
### Using API Keys
#### In the URL path
```bash
curl -X POST "https://mainnet.subfrost.io/v4/YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"btc_getblockcount","params":[],"id":1}'
```
#### In the request header
```bash
curl -X POST "https://mainnet.subfrost.io/v4/jsonrpc" \
-H "Content-Type: application/json" \
-H "x-subfrost-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","method":"btc_getblockcount","params":[],"id":1}'
```
#### With the explorer verification API
The same key works for the SUBFROST explorer's [contract verification API](../verification/verify-and-attest), at `https://explorer.subfrost.io/api/v1/{key}/...`. There is nothing extra to enable. That API only reads the key from the path, not from the header.
| Call | Key required |
|------|--------------|
| `POST /api/v1/{key}/verify` | Any active key. A wrong recipe cannot publish anything, so this is open to every valid key. |
| `GET /api/v1/{key}/source/...` | Any active key. See [Source API](../verification/source-api). |
| `POST /api/v1/{key}/attest` | An admin key issued by the SUBFROST team. Contact the team if you have a build proven outside the explorer's sandbox that needs to be recorded. |
### Key management
#### Viewing keys
Your dashboard shows all active keys with:
- Key name
- Creation date
- Last used timestamp
- Request count
#### Revoking keys
1. Go to Dashboard
2. Find the key you want to revoke
3. Click the delete/revoke button
4. Confirm the action
Revoked keys stop working immediately.
#### Key naming best practices
Use descriptive names that indicate:
- Environment (Production, Staging, Development)
- Application (Web App, Mobile App, Backend Service)
- Purpose (Testing, Analytics, Main API)
Examples:
- `production-backend`
- `staging-webapp`
- `dev-local-testing`
### Security best practices
#### Do's
- Store keys in environment variables
- Use different keys for different environments
- Rotate keys periodically
- Monitor key usage for anomalies
- Revoke unused keys
#### Don'ts
- Never commit keys to version control
- Don't embed keys in client-side JavaScript
- Don't share keys between applications
- Don't use production keys for testing
### Environment variables
#### Node.js
```javascript
// .env file
SUBFROST_API_KEY=YOUR_API_KEY
// Usage
const apiKey = process.env.SUBFROST_API_KEY;
```
#### Python
```python
import os
api_key = os.environ.get('SUBFROST_API_KEY')
```
#### Shell
```bash
export SUBFROST_API_KEY="YOUR_API_KEY"
curl -X POST "https://mainnet.subfrost.io/v4/jsonrpc" \
-H "x-subfrost-api-key: $SUBFROST_API_KEY" \
...
```
### Rate limits by key
Each API key has its own rate limit counter. This means:
- Multiple keys mean multiple rate limit buckets
- Useful for isolating applications
- Business plans can set per-key limits
### Programmatic key management
Business customers can manage keys via API:
```json
{
"jsonrpc": "2.0",
"method": "admin_createApiKey",
"params": { "name": "New Key" },
"id": 1
}
```
Contact support for API management access.
---
## CORS Authentication
CORS (Cross-Origin Resource Sharing) authentication allows browser-based applications to access the Subfrost API without exposing API keys in client-side code.
### How it works
1. You register your domain in the Subfrost dashboard
2. When requests come from your domain, the `Origin` header is verified
3. If the origin matches a verified domain, the request is authenticated
4. CORS headers are added to allow browser access
### Setting up CORS
#### Step 1: add your domain
1. Go to the [Domains page](https://api.subfrost.io/domains) in your dashboard
2. Click "Add Domain"
3. Enter your domain (e.g., `https://myapp.com`)
#### Step 2: verify ownership
For security, you may need to verify domain ownership:
**DNS verification:**
Add a TXT record to your domain's DNS:
```
_subfrost.yourdomain.com TXT "subfrost-verify=abc123xyz"
```
**File verification:**
Host a verification file at:
```
https://yourdomain.com/.well-known/subfrost-verify.txt
```
#### Step 3: make requests
Once verified, make requests from your domain:
```javascript
// From https://myapp.com
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'btc_getblockcount',
params: [],
id: 1
})
});
```
### Supported patterns
#### Exact domain match
```
https://myapp.com
```
#### Subdomain wildcard
```
https://*.myapp.com
```
Matches:
- `https://www.myapp.com`
- `https://api.myapp.com`
- `https://staging.myapp.com`
#### Development domains
For local development, you can add:
```
http://localhost:3000
http://localhost:5173
http://127.0.0.1:3000
```
:::note
Localhost domains don't require verification but should be removed before production.
:::
### CORS headers
The API returns appropriate CORS headers:
```
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
```
### Preflight requests
The API handles OPTIONS preflight requests automatically:
```javascript
// Browser automatically sends OPTIONS first
OPTIONS /v4/api HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
// API responds with CORS headers
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
### Security considerations
#### Advantages of CORS auth
- No API keys in browser code
- Origin header can't be spoofed by browsers
- Domain verification prevents unauthorized access
- Works with CSP (Content Security Policy)
#### Limitations
- Only works from browsers
- Server-side requests need API keys
- Doesn't work from browser extensions
#### Best practices
1. Only register production domains
2. Remove localhost entries before production
3. Use exact matches when possible
4. Monitor domain usage in analytics
### Troubleshooting
#### "CORS Error" in browser
Check that:
1. Your domain is registered and verified
2. You're using the correct endpoint (`/v4/api`)
3. The Origin header matches exactly
#### Request works in Postman but not browser
Postman doesn't enforce CORS. Add your domain to the dashboard.
#### Preflight failing
Ensure you're not sending custom headers that require preflight approval.
### Combining with API keys
You can use both CORS and API keys:
```javascript
// CORS for origin verification + API key for rate limit bucket
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': 'YOUR_API_KEY' // Optional
},
body: JSON.stringify({ /* ... */ })
});
```
---
## API Platform Overview
The Subfrost API Platform provides a comprehensive dashboard for managing your API access, monitoring usage, and configuring authentication.
### Dashboard Features
#### API Keys Management
Create and manage multiple API keys for different applications:
- Generate new keys with custom names
- View key usage statistics
- Revoke compromised keys
- Set per-key rate limits (Business)
#### Domain Management
Configure CORS-based authentication:
- Add and verify domains
- Automatic origin verification
- Support for subdomains and wildcards
- Domain-specific usage tracking
#### Usage Analytics
Monitor your API consumption:
- Real-time request counts
- Historical usage graphs
- Method-level breakdown
- Error rate tracking
#### Billing & Plans
Manage your subscription:
- View current plan details
- Upgrade or downgrade
- Payment history
- Usage-based billing (Business)
### Account Types
#### Free Tier
- 1,000 requests per day
- 60 requests per minute
- 1 API key
- Community support
#### Pro Plan ($45-$55/mo)
- 50,000 requests per day
- 600 requests per minute
- 10 API keys
- Email support
- Priority routing
#### Business ($90-$120/mo)
- Unlimited requests
- Unlimited API keys
- Custom rate limits
- Dedicated support
- SLA guarantees
- Custom endpoints
### Security Features
- **Key Rotation**: rotate keys without downtime
- **Domain Verification**: verify domain ownership
- **Usage Alerts**: get notified of unusual activity
- **Audit Logs**: track all API key operations
### Next steps
- [API Keys](./api-keys): manage your keys
- [CORS Authentication](./cors): configure browser access
- [Rate Limits](./rate-limits): understand limits
---
## Rate Limits
Rate limits protect the API from abuse and ensure fair usage for all customers.
### Limits by plan
- **Free**: 60 requests/minute, 1,000/day, burst limit 10
- **Pro**: 600 requests/minute, 50,000/day, burst limit 100
- **Business**: custom limits
### Rate limit headers
Every response includes rate limit information:
```http
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699123456
X-RateLimit-Daily-Limit: 1000
X-RateLimit-Daily-Remaining: 850
```
- **`X-RateLimit-Limit`**: requests allowed per minute
- **`X-RateLimit-Remaining`**: requests remaining this minute
- **`X-RateLimit-Reset`**: Unix timestamp when limit resets
- **`X-RateLimit-Daily-Limit`**: requests allowed per day
- **`X-RateLimit-Daily-Remaining`**: requests remaining today
### Rate limit exceeded
When you exceed the rate limit:
```json
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Rate limit exceeded. Try again in 45 seconds."
},
"id": 1
}
```
HTTP Status: `429 Too Many Requests`
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699123456
```
### Handling rate limits
#### Exponential backoff
```javascript
async function fetchWithRetry(request, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': API_KEY
},
body: JSON.stringify(request)
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
return response.json();
}
throw new Error('Max retries exceeded');
}
```
#### Check before request
```javascript
// Track remaining requests from headers
let remaining = 60;
async function makeRequest(request) {
if (remaining <= 0) {
throw new Error('Rate limit reached, please wait');
}
const response = await fetch('https://mainnet.subfrost.io/v4/jsonrpc', {
/* ... */
});
remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
return response.json();
}
```
### Optimizing API usage
#### Use batch requests
Instead of multiple individual requests:
```javascript
// Bad: 3 requests
await rpc('btc_getblockcount', []);
await rpc('btc_getbestblockhash', []);
await rpc('btc_getblockchaininfo', []);
// Good: 1 request
await rpc([
{ jsonrpc: '2.0', method: 'btc_getblockcount', params: [], id: 1 },
{ jsonrpc: '2.0', method: 'btc_getbestblockhash', params: [], id: 2 },
{ jsonrpc: '2.0', method: 'btc_getblockchaininfo', params: [], id: 3 }
]);
```
#### Use Lua scripts
Combine multiple operations server-side:
```json
{
"jsonrpc": "2.0",
"method": "lua_evalscript",
"params": [
"local height = _RPC.btc_getblockcount()\nlocal hash = _RPC.btc_getblockhash(height)\nlocal block = _RPC.btc_getblock(hash)\nreturn block",
[]
],
"id": 1
}
```
One API call, but executes 3 RPC methods internally.
#### Cache responses
Many blockchain data points don't change frequently:
```javascript
const cache = new Map();
const CACHE_TTL = 10000; // 10 seconds
async function cachedRpc(method, params) {
const key = JSON.stringify({ method, params });
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const data = await rpc(method, params);
cache.set(key, { data, timestamp: Date.now() });
return data;
}
```
### Per-key rate limits
Each API key has its own rate limit bucket:
- Key A: 60/min independent of Key B
- Useful for isolating applications
- Track usage per-key in dashboard
Business customers can configure per-key limits.
### WebSocket connections
For real-time data, consider WebSocket subscriptions (Business):
- Lower rate limit impact
- Push-based updates
- Efficient for high-frequency data
### Requesting higher limits
If you need higher limits:
1. **Pro Plan**: 10x free tier limits ($45-$55/mo)
2. **Business**: custom limits, SLA, dedicated support ($90-$120/mo)
Contact [sales@subfrost.io](mailto:sales@subfrost.io) for Business pricing.
---
## Reorg Endpoint
The `/v4/YOUR_API_KEY/reorg/` endpoint is a fake bitcoind RPC that drives a
`rockshrew-mono` indexer to roll back to an arbitrary block height. It
is useful when an alkanes-rs or metashrew upgrade introduces a
state-changing fork (a hardfork height) and you need to re-index from
just before that fork under the new wasm.
### When to use this
When an alkanes-rs release notes an activation height (for example
`v2.2.0-rc.5`'s OYL-disband activation at h=950564), any indexer that
ran the previous wasm past that height has applied the old behavior
and is now permanently divergent. Replaying the affected range under
the new wasm is the only way to converge.
Wiping the database and resyncing from genesis takes hours or days.
This endpoint is much faster: it forces a targeted rollback at a
specific height via metashrew's built-in startup-heal walk-back, then
the indexer resumes forward under the new wasm from there.
If you would rather start from a freshly indexed snapshot rather than
re-index the affected range yourself, see
[Snapshot Download](./snapshots).
### How it works
`rockshrew-mono` runs a startup-heal walk-back on every boot: starting
from the locally indexed tip, it asks bitcoind for the block hash at
each height and walks down until the remote hash matches the locally
stored hash; that height becomes the rollback target.
This endpoint pretends to be bitcoind and answers `getblockhash(h)`
with a synthesized non-matching hash for every `h > `, and with the
real chain hash for every `h <= `. So the walk-back keeps stepping
down until it reaches ``, finds a match (passthrough to the real
chain), and rolls the database back to that height.
After the rollback completes, the endpoint reports `getblockcount` as
``, so the indexer sits idle at that height. You then point
`--daemon-rpc-url` back at your normal bitcoind and the indexer
resumes forward from ` + 1` under whatever wasm it now has loaded.
### Procedure
#### 1. Stop your indexer
Stop `rockshrew-mono` (`SIGTERM` is fine: it shuts down cleanly).
#### 2. Restart with the endpoint as the daemon URL
```bash
rockshrew-mono \
--daemon-rpc-url https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/950564 \
--max-reorg-depth 2000 \
...other args unchanged...
```
Two flags matter:
- **`--daemon-rpc-url`** points at the reorg endpoint for the target
height of your choice. Substitute `950564` with your target.
- **`--max-reorg-depth`** must exceed `current_tip - target`. The
default is `100`, which is almost certainly too low for any
intentional hardfork rollback. Set it generously: for a target
~1100 blocks below tip, `--max-reorg-depth 2000` gives slack and
costs nothing at runtime.
The URL must carry your API key. A request to the unkeyed
`https://mainnet.subfrost.io/v4/reorg/` returns HTTP 401 with
`{"code":"INVALID_API_KEY"}`.
#### 3. Wait for the rollback to complete
In `rockshrew-mono`'s logs you will see the startup-heal walk-back:
```
startup-heal: pointer scan — __INTERNAL/height=951646 ...
startup-heal: hash mismatch at height 951646; walking back
startup-heal: hash mismatch at height 951645; walking back
...
startup-heal: hash mismatch at height 950565; walking back
startup-heal: all pointers agree at height 950564 and bitcoind confirms; clean state
Reorg detected. Rolling back to height 950563
```
Each step is one HTTP request. A 1000-block walk-back takes 10-30
seconds. After the rollback, the indexer reports tip = `` and sits
idle (because the endpoint says `getblockcount` = ``).
#### 4. Switch back to your normal bitcoind
Stop `rockshrew-mono` again. Restart it with `--daemon-rpc-url`
pointing at your real bitcoind. It will detect the new tip, resume
indexing from ` + 1`, and apply the new wasm's behavior to every
block from the fork onward.
You can drop `--max-reorg-depth` back to its default if you set it
high for the rollback step.
### API reference
The endpoint mimics the bitcoind JSON-RPC interface. Three methods
are synthesized; everything else passes through to the live mainnet
chain via our bitcoind bouncer.
#### `getblockcount`
Returns the target height `` regardless of the real chain tip. This
makes the indexer idle once the rollback completes.
```bash
curl -X POST -H "content-type: application/json" \
--data '{"jsonrpc":"2.0","method":"getblockcount","id":1}' \
https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/950564
# => {"result":950564,"error":null,"id":1,"jsonrpc":"2.0"}
```
#### `getblockhash`
For `h > ` returns a synthetic non-matching hash of the form
`ffffffffffffffffffffffffffffffffffffffffffffffffffffffff{h:016x}`.
For `h <= ` passes through to the real chain.
```bash
# Above target: fake hash
curl -X POST -H "content-type: application/json" \
--data '{"jsonrpc":"2.0","method":"getblockhash","params":[951000],"id":2}' \
https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/950564
# => {"result":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000e82d8",...}
# At or below target: real chain hash
curl -X POST -H "content-type: application/json" \
--data '{"jsonrpc":"2.0","method":"getblockhash","params":[950564],"id":3}' \
https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/950564
# => {"result":"00000000000000000000f289e6259e2377a32bf4703492ec2ba3381438dc863e",...}
```
#### `getbestblockhash`
Returns the real-chain hash at `` (i.e. `getblockhash()`).
#### Everything else
`getblock`, `getblockheader`, `sendrawtransaction`, `getrawmempool`,
etc. pass through unchanged. The startup-heal path only consults
`getblockhash`, so passthrough is safe for normal operation; if your
indexer happens to request `getblock` on a synthetic above-target
hash it will get an upstream error, which is the correct signal that
that block is not on the served chain.
### GET request returns usage
`GET https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/` returns a short
plain-text usage doc, so paste-in-browser users see what to do.
### Networks
Currently mainnet only:
- **mainnet**: `https://mainnet.subfrost.io/v4/YOUR_API_KEY/reorg/`
Signet and regtest will be added if there is demand. File an issue or
contact support.
### Caveats
- **The endpoint is stateless and per-call.** It does nothing
persistent on our side. Two callers pointing different indexers at
different `` values get independently correct behavior at the
same time.
- **It does not protect you from typos.** `--daemon-rpc-url
.../v4/YOUR_API_KEY/reorg/0` will attempt to roll all the way back to height 0.
No safety cap; treat the URL as authoritative.
- **Your wasm must already be the post-fork build before you start
the rollback.** This endpoint replays history; if you walk back to
`950563` and then resume with the old wasm, you will re-create the
same divergence you started with.
- **`--max-reorg-depth` is the load-bearing flag.** Without it set
high enough, the walk-back hits its floor and stops short of ``,
rolling back only `max_reorg_depth` blocks instead of the
`tip - ` you wanted.
### Related
- [Snapshot Download](./snapshots): if you would
rather start from a freshly indexed RocksDB snapshot than re-index
the affected range yourself.
- [Database Rsync Access](./rsync): authenticated rsync
for Business customers needing per-service granularity.
---
## Database Rsync Access
Business customers can sync blockchain database snapshots directly using rsync, enabling fast node bootstrapping and data recovery.
### Overview
The Subfrost rsync service provides authenticated access to indexed blockchain data for:
- **Esplora**: Electrs blockchain index data
- **Ord**: Ordinals/Inscriptions index data
- **Metashrew**: indexer database
- **Bitcoind**: Bitcoin Core blockchain data
Available networks: `mainnet`, `signet`, `regtest`
### Authentication
Rsync access uses your Subfrost API key for authentication. The API key serves as both the username and password.
```bash
# Set your API key as the rsync password
export RSYNC_PASSWORD="your-api-key-here"
# Use the same API key as the username in the rsync URL
rsync -avz rsync://YOUR_API_KEY@rsyncd.subfrost.io/MODULE/ /local/path/
```
### Available modules
Modules are named using the format `{network}-{service}`:
#### Mainnet
- **`mainnet-esplora`**: Electrs/Esplora index (~500GB)
- **`mainnet-ord`**: Ordinals index (~1TB)
- **`mainnet-metashrew`**: indexer database (~200GB)
- **`mainnet-bitcoind`**: Bitcoin Core data (~700GB)
#### Signet
- **`signet-esplora`**: Electrs/Esplora index (~5GB)
- **`signet-ord`**: Ordinals index (~10GB)
- **`signet-metashrew`**: indexer database (~2GB)
- **`signet-bitcoind`**: Bitcoin Core data (~3GB)
#### Regtest
- **`regtest-esplora`**: Electrs/Esplora index (~100MB)
- **`regtest-ord`**: Ordinals index (~200MB)
- **`regtest-metashrew`**: indexer database (~50MB)
- **`regtest-bitcoind`**: Bitcoin Core data (~100MB)
### List available modules
```bash
export RSYNC_PASSWORD="your-api-key"
rsync --list-only rsync://YOUR_API_KEY@rsyncd.subfrost.io/
```
### Basic usage
#### Sync signet Metashrew database
```bash
export RSYNC_PASSWORD="YOUR_API_KEY"
rsync -avz --progress \
rsync://YOUR_API_KEY@rsyncd.subfrost.io/signet-metashrew/ \
/data/metashrew/
```
#### Sync mainnet Ord index
```bash
export RSYNC_PASSWORD="YOUR_API_KEY"
rsync -avz --progress \
rsync://YOUR_API_KEY@rsyncd.subfrost.io/mainnet-ord/ \
/data/ord/
```
#### Preview files (dry run)
```bash
rsync -avz --dry-run \
rsync://YOUR_API_KEY@rsyncd.subfrost.io/signet-esplora/ \
/data/esplora/
```
### Consistent snapshots
For databases that are actively being written to, use a multi-pass rsync approach to ensure consistency:
```bash
#!/bin/bash
# rsync-consistent.sh - Multi-pass rsync for consistent snapshots
API_KEY="your-api-key"
MODULE="mainnet-metashrew"
DEST="/data/metashrew"
export RSYNC_PASSWORD="$API_KEY"
RSYNC_URL="rsync://${API_KEY}@rsyncd.subfrost.io/${MODULE}/"
MAX_PASSES=10
pass=1
while [ $pass -le $MAX_PASSES ]; do
echo "=== Pass $pass of $MAX_PASSES ==="
# Run rsync and capture stats
output=$(rsync -avz --stats "$RSYNC_URL" "$DEST/" 2>&1)
echo "$output"
# Check if any files were transferred
transferred=$(echo "$output" | grep "Number of regular files transferred" | awk '{print $NF}')
if [ "$transferred" = "0" ]; then
echo "No changes detected. Snapshot is consistent."
exit 0
fi
echo "Files changed: $transferred. Running another pass..."
pass=$((pass + 1))
sleep 2
done
echo "Warning: Max passes reached. Database may still be changing."
exit 1
```
### Recommended rsync options
```bash
rsync -avz \
--progress \ # Show transfer progress
--partial \ # Keep partial files for resume
--partial-dir=.rsync-partial \ # Store partials in hidden dir
--delete \ # Remove files not in source
rsync://...
```
#### For large transfers
```bash
rsync -avz \
--progress \
--partial \
--bwlimit=100000 \ # Limit bandwidth to 100MB/s
--timeout=3600 \ # 1 hour timeout
rsync://...
```
### Docker integration
#### Using in Kubernetes init container
```yaml
apiVersion: v1
kind: Pod
spec:
initContainers:
- name: sync-data
image: alpine:3.19
command:
- sh
- -c
- |
apk add --no-cache rsync
export RSYNC_PASSWORD="${SUBFROST_API_KEY}"
rsync -avz --progress \
"rsync://${SUBFROST_API_KEY}@rsyncd.subfrost.io/signet-metashrew/" \
/data/
env:
- name: SUBFROST_API_KEY
valueFrom:
secretKeyRef:
name: subfrost-secrets
key: api-key
volumeMounts:
- name: data
mountPath: /data
containers:
- name: app
# ... your application container
```
#### Docker Compose example
```yaml
services:
metashrew:
image: your-app:latest
volumes:
- metashrew-data:/data
depends_on:
sync-data:
condition: service_completed_successfully
sync-data:
image: alpine:3.19
command: >
sh -c "
apk add --no-cache rsync &&
export RSYNC_PASSWORD=$${SUBFROST_API_KEY} &&
rsync -avz rsync://$${SUBFROST_API_KEY}@rsyncd.subfrost.io/signet-metashrew/ /data/
"
environment:
- SUBFROST_API_KEY=${SUBFROST_API_KEY}
volumes:
- metashrew-data:/data
volumes:
metashrew-data:
```
### Performance tips
1. **Use compression for remote syncs**: add `--compress` for WAN transfers
2. **Limit bandwidth if needed**: use `--bwlimit=KBPS` to avoid saturating your connection
3. **Resume interrupted transfers**: use `--partial --partial-dir=.rsync-partial`
4. **Exclude unnecessary files**: use `--exclude` patterns for files you don't need
5. **Consider incremental syncs**: after initial sync, subsequent syncs transfer only changes
### Troubleshooting
#### Authentication failed
```
@ERROR: authentication failed
```
- Verify your API key is valid and has business access
- Ensure `RSYNC_PASSWORD` environment variable is set
- Check that the username in the URL matches your API key
#### Connection refused
```
rsync: failed to connect to rsyncd.subfrost.io: Connection refused
```
- Check your network connectivity
- Verify DNS resolution: `dig rsyncd.subfrost.io`
- Ensure port 873 is not blocked by firewall
#### Module not found
```
@ERROR: Unknown module 'invalid-module'
```
- Use `--list-only` to see available modules
- Verify module name format: `{network}-{service}`
### Rate limits
Rsync access is subject to your business plan limits:
- Concurrent connections per API key
- Bandwidth limits (if applicable)
- Access to specific networks/modules
Contact support for custom limits or additional access.
### Security considerations
- API keys used for rsync have the same permissions as HTTP API access
- All transfers are authenticated: no anonymous access
- Consider using separate API keys for rsync vs API access
- Rotate keys periodically, especially after sharing for one-time syncs
---
## Snapshot Download
The metashrew RocksDB index can be bootstrapped from a public
snapshot rather than indexed from genesis. For mainnet that is the
difference between hours and weeks.
The snapshot is the verbatim contents of a healthy mainnet metashrew
indexer's `/data` directory, tarred and gzipped. Extract it into a
fresh metashrew data directory and start `rockshrew-mono`: it picks up
from the snapshot's tip and resumes forward from there.
`latest.tar.gz` is republished in place, so **check its size and age
before you plan a transfer** rather than trusting a figure in this
page. One `HEAD` tells you both:
```bash
curl -sIL https://cdn.subfrost.io/snapshots/latest.tar.gz \
| grep -iE 'content-length|last-modified|x-goog-hash'
```
### URL
- **Mainnet**: `https://cdn.subfrost.io/snapshots/latest.tar.gz`
The CDN redirects to a public GCS bucket; the tarball is served with
HTTP/2, range-request support, and no auth. **It is large.** Measured
on 2026-10-01, `content-length` was 377,371,555,210 bytes — 377 GB
(352 GiB) compressed. Plan the transfer from your own `HEAD`, not from
that number.
Bucket listing is not public, so `latest.tar.gz` is the only object you
can discover from outside. There is no index of dated snapshots to
browse.
Signet and regtest are not currently published as public snapshots;
those chains sync from scratch in minutes to hours anyway.
### What the snapshot contains
The tarball expands to a complete RocksDB database produced by a
specific build of metashrew + alkanes-rs. Each refresh corresponds to
a tagged metashrew image; the contents are byte-identical (under our
strict-block-determinism guarantees) to what your own pod would
produce by indexing forward to the same height.
Caveat: the snapshot's on-disk format and indexer wasm version must
match the `rockshrew-mono` binary you intend to run. Mainnet
snapshots currently target metashrew `v9.0.5-rc.13` with alkanes-rs
`v2.2.0-rc.5`. If you are running an older or future version,
contact support before bootstrapping: the SMT layout has been stable
across the v9.0.5-rc.* series but newer versions may diverge.
### Quick start
```bash
mkdir -p /data/metashrew
cd /data/metashrew
curl -L https://cdn.subfrost.io/snapshots/latest.tar.gz \
| tar -xzf - --strip-components=1
# Verify
ls CURRENT *.sst | head
# CURRENT exists and there is at least one *.sst file -> snapshot is intact
# Start rockshrew-mono pointing at /data/metashrew
rockshrew-mono \
--db-path /data/metashrew \
--indexer /path/to/indexer.wasm \
--daemon-rpc-url http://your-bitcoind:8332 \
--auth bitcoinrpc:bitcoinrpc
```
`--strip-components=1` discards the top-level directory in the
tarball so the contents land directly in `/data/metashrew`. The
indexer detects the existing data, reads `__INTERNAL/height` to learn
the snapshot's tip, and resumes forward from that height + 1.
### Streamed extraction (no tmp file, no double disk usage)
The default `curl | tar` pipeline above is already streamed:
nothing is written to disk except the final RocksDB files. So you need
room for the expanded database, not for the tarball as well — but the
expanded database is larger than the download. At the size measured
above, budget well over 400 GB of free space on the target filesystem
and check it before you start, because a pipeline that fills the disk
three hundred gigabytes in leaves you with a partial RocksDB and
nothing to resume from.
For the most reliable transfer (slower links, automatic retries,
parallel reads), use the `gcloud` CLI directly against the underlying
GCS bucket. That is exactly what our internal bootstrap jobs do:
```bash
CLOUDSDK_STORAGE_TRANSPORT=requests \
gcloud storage cp \
--no-user-output-enabled \
gs://subfrost-cdn-bucket/snapshots/latest.tar.gz - \
| tar -xzf - -C /data/metashrew --strip-components=1
```
This bypasses Cloudflare entirely, gets the maximum throughput your
egress allows, and retries on transient I/O failures.
### Verification
Once extraction completes:
```bash
# RocksDB CURRENT marker should exist
test -f /data/metashrew/CURRENT && echo OK
# There should be at least one SST file
ls /data/metashrew/*.sst | wc -l # expect many
# rockshrew-mono will refuse to start if the SMT pointers don't
# agree. Let it run; it logs the indexed tip on startup.
```
If `rockshrew-mono` boots and logs something like:
```
startup-heal: all pointers agree at height NNNNNN and bitcoind confirms; clean state
```
then the snapshot is intact and you are at the snapshot's tip.
### How current is the snapshot?
Read `Last-Modified` on the object and assume nothing else. The
snapshot is not on a guaranteed refresh schedule: when this page was
last checked (2026-10-01) `latest.tar.gz` carried
`Last-Modified: Tue, 02 Jun 2026 02:58:15 GMT`, four months behind the
tip.
That is still a working bootstrap — `rockshrew-mono` resumes from
`__INTERNAL/height` and indexes forward on its own — but the catch-up
is proportional to the gap, so a four-month-old snapshot means roughly
seventeen thousand blocks to index before you are at the tip, not the
few minutes a same-day snapshot would cost. Size that before you
commit to the approach, and compare it against indexing the tail
yourself.
If you need a snapshot pinned to a specific height, or a fresher one
than the object currently carries, contact support.
### Kubernetes bootstrap pattern
This is the same shape our own production pods use. The init
container streams the snapshot into the PVC before the indexer
container starts.
```yaml
apiVersion: v1
kind: Pod
spec:
initContainers:
- name: seed-snapshot
image: google/cloud-sdk:slim
command:
- /bin/bash
- -c
- |
set -euxo pipefail
# Skip if we already have data (idempotent on retry)
if [ -f /data/CURRENT ]; then
echo "already seeded"
exit 0
fi
CLOUDSDK_STORAGE_TRANSPORT=requests \
gcloud storage cp \
--no-user-output-enabled \
gs://subfrost-cdn-bucket/snapshots/latest.tar.gz - \
| tar -xzf - -C /data --strip-components=1
test -f /data/CURRENT
resources:
requests: { cpu: "2", memory: "16Gi" }
limits: { cpu: "4", memory: "32Gi" }
volumeMounts:
- { name: data, mountPath: /data }
containers:
- name: rockshrew-mono
image: your-metashrew-image:v9.0.5-rc.13
args:
- --db-path
- /data
- --indexer
- /metashrew/indexer.wasm
- --daemon-rpc-url
- http://bitcoind.your-namespace:8332
volumeMounts:
- { name: data, mountPath: /data }
volumes:
- name: data
persistentVolumeClaim:
claimName: metashrew-data
```
The init container's `if [ -f /data/CURRENT ]; then exit 0` check
makes the bootstrap idempotent: pod restarts don't redownload, only
the first-ever boot pays the seed cost.
### Combining snapshot + reorg endpoint
If you are bootstrapping for the first time, just use the snapshot.
If you have an indexer that ran an older wasm past a known
hardfork height and is now divergent, the cheapest fix is **NOT** the
snapshot. It is the [reorg endpoint](./reorg), which
forces a targeted rollback to one block before the fork. The
snapshot would re-download the whole 377 GB object; the reorg endpoint
costs a few hundred HTTP requests plus a re-index of the affected
range.
Use the snapshot for: fresh installs, disaster recovery,
horizontally scaling out a new replica.
Use the reorg endpoint for: surgical fixes when you know the
divergence height and only need to replay a bounded range under a
new wasm.
### Caveats
- **Snapshot version vs binary version must match.** Bootstrapping a
v2.1 binary from a v2.2 snapshot will not work; the on-disk schema
is a runtime contract of the indexer wasm.
- **One-way transfer.** Plan for the full size, measured from your own
`HEAD` rather than from this page, plus free local disk for the
expanded database during extraction. Streamed extraction (the
pipelines shown above) means you don't need room for both the
tarball and the database.
- **Check the vintage.** `Last-Modified` has been months behind the
tip. The bootstrap still works, but the catch-up index is
proportional to the gap.
- **Public, unauthenticated.** Anyone can download. Don't put
sensitive data in the same bucket.
### Related
- [Database Rsync Access](./rsync): authenticated rsync
for per-service granularity (esplora, ord, bitcoind, metashrew
separately). Required if you want anything other than mainnet
metashrew.
- [Reorg Endpoint](./reorg): when you already have a
full index but need to surgically replay a range under new wasm.
---
## Alkanes REST Endpoints
Query alkane tokens, contracts, and holdings.
### get-alkanes
List all alkanes with pagination.
**Endpoint:** `POST /get-alkanes`
**Parameters:**
- `limit` (number, optional) - Maximum results (default: 50)
- `offset` (number, optional) - Pagination offset (default: 0)
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-alkanes \
-H "Content-Type: application/json" \
-d '{"limit": 50, "offset": 0}'
```
---
### get-alkane-details
Get detailed information about a specific alkane.
**Endpoint:** `POST /get-alkane-details`
**Parameters:**
- `alkaneId` (object) - The alkane ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-alkane-details \
-H "Content-Type: application/json" \
-d '{"alkaneId": {"block": "4", "tx": "65522"}}'
```
---
### global-alkanes-search
Search for alkanes and pools by name or symbol.
**Endpoint:** `POST /global-alkanes-search`
**Parameters:**
- `searchQuery` (string) - Search query
- `limit` (number, optional) - Maximum results
- `offset` (number, optional) - Pagination offset
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/global-alkanes-search \
-H "Content-Type: application/json" \
-d '{"searchQuery": "diesel", "limit": 20}'
```
---
### get-alkanes-utxo
Get alkane UTXOs for an address.
**Endpoint:** `POST /get-alkanes-utxo`
**Parameters:**
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-alkanes-utxo \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
### get-amm-utxos
Get AMM-related UTXOs for an address.
**Endpoint:** `POST /get-amm-utxos`
**Parameters:**
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-amm-utxos \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
### get-address-outpoints
Get outpoints containing alkanes for an address.
**Endpoint:** `POST /get-address-outpoints`
**Parameters:**
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-outpoints \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
### JavaScript Example
```javascript
class AlkanesClient {
constructor(apiKey, network = 'mainnet') {
this.baseUrl = `https://${network}.subfrost.io/v4/api`;
this.apiKey = apiKey;
}
async post(endpoint, body = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': this.apiKey
},
body: JSON.stringify(body)
});
const data = await response.json();
if (data.statusCode !== 200) throw new Error(data.error);
return data.data;
}
async getAllAlkanes(limit = 50, offset = 0) {
return this.post('/get-alkanes', { limit, offset });
}
async getAlkaneDetails(block, tx) {
return this.post('/get-alkane-details', {
alkaneId: { block: String(block), tx: String(tx) }
});
}
async searchAlkanes(query) {
return this.post('/global-alkanes-search', { query });
}
}
// Usage
const client = new AlkanesClient('your-api-key');
const alkanes = await client.getAllAlkanes(10, 0);
console.log('Alkanes:', alkanes);
const details = await client.getAlkaneDetails(4, 65522);
console.log('Details:', details);
```
---
## Bitcoin Data REST Endpoints
Query Bitcoin price data, balances, and market information.
### Bitcoin Price Endpoints
These endpoints provide real-time Bitcoin price data from Uniswap V3 WBTC/USDC pool.
#### get-bitcoin-price
Get the current Bitcoin price in USD.
**Endpoint:** `POST /get-bitcoin-price`
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-price \
-H "Content-Type: application/json" \
-d '{}'
```
---
#### get-bitcoin-market-weekly
Get 52-week high/low and current price data.
**Endpoint:** `POST /get-bitcoin-market-weekly`
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-market-weekly \
-H "Content-Type: application/json" \
-d '{}'
```
---
#### get-bitcoin-markets
Get market summary with volume and trust score.
**Endpoint:** `POST /get-bitcoin-markets`
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-markets \
-H "Content-Type: application/json" \
-d '{}'
```
---
#### get-bitcoin-market-chart
Get historical price data.
**Endpoint:** `POST /get-bitcoin-market-chart`
**Parameters:**
- `days` (string) - Number of days of data ("1", "7", "30", "365")
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-market-chart \
-H "Content-Type: application/json" \
-d '{"days": "30"}'
```
---
### Balance & UTXO Endpoints
#### get-address-balance
Get BTC balance for an address.
**Endpoint:** `POST /get-address-balance`
**Parameters:**
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-balance \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
#### get-taproot-balance
Get balance for a taproot address.
**Endpoint:** `POST /get-taproot-balance`
**Parameters:**
- `address` (string) - Taproot address (bc1p...)
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-taproot-balance \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
#### get-address-utxos
Get UTXOs for an address.
**Endpoint:** `POST /get-address-utxos`
**Parameters:**
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-utxos \
-H "Content-Type: application/json" \
-d '{"address": "bc1p..."}'
```
---
#### get-account-utxos
Get UTXOs for an account (all addresses).
**Endpoint:** `POST /get-account-utxos`
**Parameters:**
- `account` (string) - Account identifier
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-account-utxos \
-H "Content-Type: application/json" \
-d '{"account": "your-account-id"}'
```
---
#### get-account-balance
Get total balance for an account.
**Endpoint:** `POST /get-account-balance`
**Parameters:**
- `account` (string) - Account identifier
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-account-balance \
-H "Content-Type: application/json" \
-d '{"account": "your-account-id"}'
```
---
#### get-taproot-history
Get transaction history for a taproot address.
**Endpoint:** `POST /get-taproot-history`
**Parameters:**
- `taprootAddress` (string) - Taproot address
- `totalTxs` (number) - Maximum transactions to return
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-taproot-history \
-H "Content-Type: application/json" \
-d '{"taprootAddress": "bc1p...", "totalTxs": 50}'
```
---
#### get-intent-history
Get intent history for an address.
**Endpoint:** `POST /get-intent-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `limit` (number, optional) - Maximum results
- `page` (number, optional) - Page number
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-intent-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "limit": 20}'
```
---
### JavaScript Example
```javascript
class BitcoinClient {
constructor(network = 'mainnet') {
this.baseUrl = `https://${network}.subfrost.io/v4/api`;
}
async post(endpoint, body = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await response.json();
if (data.statusCode !== 200) throw new Error(data.error);
return data.data;
}
async getPrice() {
const data = await this.post('/get-bitcoin-price');
return data.bitcoin.usd;
}
async getMarketData() {
return this.post('/get-bitcoin-market-weekly');
}
async getPriceHistory(days = '7') {
return this.post('/get-bitcoin-market-chart', { days });
}
}
// Usage
const btc = new BitcoinClient();
const price = await btc.getPrice();
console.log(`Current BTC price: $${price.toLocaleString()}`);
const { market_data } = await btc.getMarketData();
console.log(`52-week high: $${market_data.high_52w.usd.toLocaleString()}`);
console.log(`52-week low: $${market_data.low_52w.usd.toLocaleString()}`);
const history = await btc.getPriceHistory('30');
console.log(`Got ${history.prices.length} price points`);
```
### Response Formats
#### get-bitcoin-price Response
```json
{
"statusCode": 200,
"data": {
"bitcoin": {
"usd": 97500.25
}
}
}
```
#### get-bitcoin-market-weekly Response
```json
{
"statusCode": 200,
"data": {
"market_data": {
"current_price": { "usd": 97500.25 },
"high_52w": { "usd": 135000.00 },
"low_52w": { "usd": 42000.00 }
}
}
}
```
#### get-bitcoin-markets Response
```json
{
"statusCode": 200,
"data": [
{
"base": "BTC",
"target": "USD",
"name": "Uniswap V3 (WBTC/USDC)",
"last": 97500.25,
"volume": 45000000000,
"trust_score": "green"
}
]
}
```
#### get-bitcoin-market-chart Response
```json
{
"statusCode": 200,
"data": {
"prices": [[1699123456000, 97500.25], ...],
"market_caps": [[1699123456000, 1900000000000], ...],
"total_volumes": [[1699123456000, 45000000000], ...]
}
}
```
---
## History REST Endpoints
Query transaction history for AMM operations including swaps, mints, burns, and wraps.
### get-pool-swap-history
Get swap transaction history for a specific pool.
**Endpoint:** `POST /get-pool-swap-history`
**Parameters:**
- `poolId` (object) - Pool ID with `block` and `tx` fields
- `count` (number, optional) - Limit results (default: 50)
- `offset` (number, optional) - Pagination offset
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pool-swap-history \
-H "Content-Type: application/json" \
-d '{"poolId": {"block": "4", "tx": "1"}, "count": 50}'
```
---
### get-token-swap-history
Get swap history for a specific token across all pools.
**Endpoint:** `POST /get-token-swap-history`
**Parameters:**
- `alkaneId` (object) - Token ID with `block` and `tx` fields
- `count` (number, optional) - Limit results (default: 50)
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-token-swap-history \
-H "Content-Type: application/json" \
-d '{"alkaneId": {"block": "4", "tx": "65522"}, "count": 50}'
```
---
### get-pool-mint-history
Get liquidity add (mint) history for a pool.
**Endpoint:** `POST /get-pool-mint-history`
**Parameters:**
- `poolId` (object) - Pool ID with `block` and `tx` fields
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pool-mint-history \
-H "Content-Type: application/json" \
-d '{"poolId": {"block": "4", "tx": "1"}, "count": 50}'
```
---
### get-pool-burn-history
Get liquidity remove (burn) history for a pool.
**Endpoint:** `POST /get-pool-burn-history`
**Parameters:**
- `poolId` (object) - Pool ID with `block` and `tx` fields
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pool-burn-history \
-H "Content-Type: application/json" \
-d '{"poolId": {"block": "4", "tx": "1"}, "count": 50}'
```
---
### get-pool-creation-history
Get pool creation history.
**Endpoint:** `POST /get-pool-creation-history`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pool-creation-history \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}, "count": 50}'
```
---
### get-all-wrap-history
Get all BTC wrap transactions.
**Endpoint:** `POST /get-all-wrap-history`
**Parameters:**
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-wrap-history \
-H "Content-Type: application/json" \
-d '{"count": 50}'
```
---
### get-all-unwrap-history
Get all BTC unwrap transactions.
**Endpoint:** `POST /get-all-unwrap-history`
**Parameters:**
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-unwrap-history \
-H "Content-Type: application/json" \
-d '{"count": 50}'
```
---
### get-address-wrap-history
Get wrap history for a specific address.
**Endpoint:** `POST /get-address-wrap-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `count` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-wrap-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "count": 50}'
```
---
### get-total-unwrap-amount
Get the total amount of BTC that has been unwrapped.
**Endpoint:** `POST /get-total-unwrap-amount`
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-total-unwrap-amount \
-H "Content-Type: application/json" \
-d '{}'
```
---
### get-address-unwrap-history
Get unwrap history for a specific address.
**Endpoint:** `POST /get-address-unwrap-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `limit` (number, optional) - Limit results
- `page` (number, optional) - Page number
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-unwrap-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "limit": 50}'
```
---
### get-address-swap-history-for-pool
Get swap history for an address in a specific pool.
**Endpoint:** `POST /get-address-swap-history-for-pool`
**Parameters:**
- `address` (string) - Bitcoin address
- `poolId` (object) - Pool ID with `block` and `tx` fields
- `limit` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-swap-history-for-pool \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "poolId": {"block": "4", "tx": "1"}, "limit": 50}'
```
---
### get-address-swap-history-for-token
Get swap history for an address involving a specific token.
**Endpoint:** `POST /get-address-swap-history-for-token`
**Parameters:**
- `address` (string) - Bitcoin address
- `tokenId` (object) - Token ID with `block` and `tx` fields
- `limit` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-swap-history-for-token \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "tokenId": {"block": "4", "tx": "65522"}, "limit": 50}'
```
---
### get-address-pool-creation-history
Get pool creation history for an address.
**Endpoint:** `POST /get-address-pool-creation-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `limit` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-pool-creation-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "limit": 50}'
```
---
### get-address-pool-mint-history
Get liquidity add (mint) history for an address.
**Endpoint:** `POST /get-address-pool-mint-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `limit` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-pool-mint-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "limit": 50}'
```
---
### get-address-pool-burn-history
Get liquidity remove (burn) history for an address.
**Endpoint:** `POST /get-address-pool-burn-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `limit` (number, optional) - Limit results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-address-pool-burn-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "limit": 50}'
```
---
### get-all-amm-tx-history
Get all AMM transactions (swaps, mints, burns, etc.).
**Endpoint:** `POST /get-all-amm-tx-history`
**Parameters:**
- `count` (number, optional) - Limit results
- `transactionType` (string, optional) - Filter by type: swap, mint, burn, creation, wrap, unwrap
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-amm-tx-history \
-H "Content-Type: application/json" \
-d '{"count": 50, "transactionType": "swap"}'
```
---
### get-all-address-amm-tx-history
Get all AMM transactions for a specific address.
**Endpoint:** `POST /get-all-address-amm-tx-history`
**Parameters:**
- `address` (string) - Bitcoin address
- `count` (number, optional) - Limit results
- `transactionType` (string, optional) - Filter by type
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-address-amm-tx-history \
-H "Content-Type: application/json" \
-d '{"address": "bc1p...", "count": 50}'
```
---
### JavaScript Example
```javascript
class HistoryClient {
constructor(apiKey, network = 'mainnet') {
this.baseUrl = `https://${network}.subfrost.io/v4/api`;
this.apiKey = apiKey;
}
async post(endpoint, body) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': this.apiKey
},
body: JSON.stringify(body)
});
return (await response.json()).data;
}
async getPoolSwaps(poolBlock, poolTx, limit = 50) {
return this.post('/get-pool-swap-history', {
poolId: { block: String(poolBlock), tx: String(poolTx) },
count: limit
});
}
async getAddressHistory(address, limit = 100) {
return this.post('/get-all-address-amm-tx-history', {
address,
count: limit
});
}
async getTotalUnwrapped() {
return this.post('/get-total-unwrap-amount', {});
}
}
// Usage
const history = new HistoryClient('your-api-key');
const { swaps } = await history.getPoolSwaps(4, 1, 20);
console.log(`Last 20 swaps:`, swaps);
const { transactions } = await history.getAddressHistory('bc1p...', 100);
console.log(`User transactions:`, transactions);
```
---
## OPI (BRC-20 Indexer) REST API
The OPI REST API provides indexed data for BRC-20 tokens. It follows the [Open Protocol Indexer (OPI)](https://github.com/bestinslot-xyz/OPI) standard by Best in Slot.
### Base URL
```
https://mainnet.subfrost.io/v4/opi
```
Also available on other networks:
- Signet: `https://signet.subfrost.io/v4/opi`
- Regtest: `https://regtest.subfrost.io/v4/opi`
With API key: `https://mainnet.subfrost.io/v4/{api_key}/opi`
---
### Block & Indexer Info
#### Get Block Height
Get the current indexed block height.
**GET** `/v1/brc20/block_height`
**Example:**
```bash
curl "https://mainnet.subfrost.io/v4/opi/v1/brc20/block_height"
```
**Response:**
```
838346
```
#### Get Extras Block Height
Get the extras block height (for additional indexed data).
**GET** `/v1/brc20/extras_block_height`
#### Get DB Version
Get the database version.
**GET** `/v1/brc20/db_version`
#### Get Event Hash Version
Get the event hash version for verification.
**GET** `/v1/brc20/event_hash_version`
---
### Balance Endpoints
#### Get Current Balance of Wallet
Get the current BRC-20 balance for a wallet address.
**GET** `/v1/brc20/get_current_balance_of_wallet`
**Query Parameters:**
- `address` (required): Bitcoin wallet address
- `ticker` (required): Token ticker (case-insensitive)
**Example:**
```bash
curl "https://mainnet.subfrost.io/v4/opi/v1/brc20/get_current_balance_of_wallet?address=bc1q...&ticker=ordi"
```
**Response:**
```json
{
"error": null,
"result": {
"overall_balance": "550000000000000000000",
"available_balance": "50000000000000000000",
"block_height": 840857
}
}
```
#### Get Balance on Block
Get the BRC-20 balance for a pkscript at the start of a specific block height.
**GET** `/v1/brc20/balance_on_block`
**Query Parameters:**
- `block_height` (required): Block height
- `pkscript` (required): Bitcoin pkscript (hex)
- `ticker` (required): Token ticker
**Example:**
```bash
curl "https://mainnet.subfrost.io/v4/opi/v1/brc20/balance_on_block?block_height=840000&pkscript=0014...&ticker=ordi"
```
---
### Token Holders & Transfer Notes
#### Get Holders
Get all holders of a specific BRC-20 token.
**GET** `/v1/brc20/holders`
**Query Parameters:**
- `ticker` (required): Token ticker
**Response:**
```json
{
"error": null,
"result": {
"unused_txes": [
{
"pkscript": "...",
"wallet": "bc1q...",
"overall_balance": "1000000000000000000",
"available_balance": "1000000000000000000"
}
],
"block_height": 840857
}
}
```
#### Get Valid Transfer Notes of Wallet
Get unused (unspent) transfer inscriptions for a wallet.
**GET** `/v1/brc20/get_valid_tx_notes_of_wallet`
**Query Parameters:**
- `address` (optional): Bitcoin address
- `pkscript` (optional): Bitcoin pkscript (use one or the other)
**Response:**
```json
{
"error": null,
"result": {
"unused_txes": [
{
"tick": "ordi",
"inscription_id": "abc123i0",
"amount": "1000000000000000000",
"genesis_height": 800000
}
],
"block_height": 840857
}
}
```
#### Get Valid Transfer Notes of Ticker
Get all unused transfer inscriptions for a specific token.
**GET** `/v1/brc20/get_valid_tx_notes_of_ticker`
**Query Parameters:**
- `ticker` (required): Token ticker
---
### Activity & Events
#### Get Activity on Block
Get all BRC-20 events for a specific block.
**GET** `/v1/brc20/activity_on_block`
**Query Parameters:**
- `block_height` (required): Block height
**Example:**
```bash
curl "https://mainnet.subfrost.io/v4/opi/v1/brc20/activity_on_block?block_height=840000"
```
**Response:**
```json
{
"error": null,
"result": [
{
"event_type": "transfer-inscribe",
"inscription_id": "abc123i0",
...
}
]
}
```
Event types include:
- `deploy-inscribe` - Token deployment
- `mint-inscribe` - Token minting
- `transfer-inscribe` - Transfer inscription created
- `transfer-transfer` - Transfer inscription sent
- `brc20prog-deploy-inscribe` - BRC2.0 contract deployment inscribed
- `brc20prog-deploy-transfer` - BRC2.0 contract deployment sent
- `brc20prog-call-inscribe` - BRC2.0 contract call inscribed
- `brc20prog-call-transfer` - BRC2.0 contract call sent
- `brc20prog-withdraw-inscribe` - BRC2.0 withdrawal inscribed
- `brc20prog-withdraw-transfer` - BRC2.0 withdrawal sent
#### Get Event by Inscription ID
Get all events associated with a specific inscription.
**GET** `/v1/brc20/event`
**Query Parameters:**
- `inscription_id` (required): Inscription ID
**Example:**
```bash
curl "https://mainnet.subfrost.io/v4/opi/v1/brc20/event?inscription_id=abc123i0"
```
#### Get Bitcoin RPC Results on Block
Get cached Bitcoin RPC results for a block (used for BRC2.0 precompiles).
**GET** `/v1/brc20/bitcoin_rpc_results_on_block`
**Query Parameters:**
- `block_height` (required): Block height
---
### Hash Verification
These endpoints are used for indexer verification and comparison.
#### Get Hash of All Activity
Get the cumulative and block event hashes for verification.
**GET** `/v1/brc20/get_hash_of_all_activity`
**Query Parameters:**
- `block_height` (required): Block height
**Response:**
```json
{
"error": null,
"result": {
"cumulative_event_hash": "abc123...",
"block_event_hash": "def456...",
"cumulative_trace_hash": "ghi789...",
"block_trace_hash": "jkl012...",
"indexer_version": "0.5.0",
"block_height": 840000
}
}
```
#### Get Hash of All Current Balances
Get a hash of all current balances for verification. Note: this may take several minutes to compute.
**GET** `/v1/brc20/get_hash_of_all_current_balances`
**Response:**
```json
{
"error": null,
"result": {
"current_balances_hash": "abc123...",
"indexer_version": "0.5.0",
"block_height": 840857
}
}
```
---
### Error Handling
All endpoints return errors in the following format:
```json
{
"error": "error message",
"result": null
}
```
Common errors:
- `block not indexed yet` - Requested block height exceeds indexed height
- `no balance found` - No balance exists for the given parameters
- `inscription_id is required` - Missing required parameter
- `internal error` - Server-side error
---
## REST API Overview
The Subfrost REST API provides high-level endpoints for alkanes, AMM pools, and blockchain data. These endpoints are part of the alkanes-data-api service.
### Base URLs
Choose the network for your application:
- **Mainnet:** `https://mainnet.subfrost.io/v4/api`
- **Signet:** `https://signet.subfrost.io/v4/api`
- **Regtest:** `https://regtest.subfrost.io/v4/api`
**OPI (BRC-20 Indexer) Base URL:**
- `https://mainnet.subfrost.io/v4/opi`
With API key authentication:
- `https://mainnet.subfrost.io/v4/{api_key}` (Standard API)
- `https://mainnet.subfrost.io/v4/{api_key}/opi` (OPI API)
### Authentication
REST endpoints can be accessed via:
1. **Public endpoint** - `/v4/api/{route}` (with CORS from registered domain)
2. **API key in path** - `/v4/{api_key}/{route}`
3. **API key header** - `/v4/api/{route}` with `x-subfrost-api-key` header
### Request Format
All endpoints accept POST requests with JSON bodies:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-price \
-H "Content-Type: application/json" \
-d '{}'
```
Or with API key:
```bash
curl -X POST https://mainnet.subfrost.io/v4/YOUR_API_KEY/get-bitcoin-price \
-H "Content-Type: application/json" \
-d '{}'
```
### Response Format
All responses follow this structure:
```json
{
"statusCode": 200,
"data": { /* response data */ }
}
```
Error responses:
```json
{
"statusCode": 400,
"error": "Invalid request parameters"
}
```
### Endpoint Categories
#### Alkanes
Query alkane tokens and contracts.
- **`POST /get-alkanes`** - List all alkanes
- **`POST /get-alkanes-by-address`** - Alkanes for an address
- **`POST /get-alkane-details`** - Specific alkane info
- **`POST /get-alkanes-utxo`** - Alkane UTXOs
- **`POST /global-alkanes-search`** - Search alkanes
#### Pools & AMM
Liquidity pools and AMM operations.
- **`POST /get-pools`** - List all pools
- **`POST /get-pool-details`** - Pool details
- **`POST /get-all-pools-details`** - All pool details
- **`POST /address-positions`** - Liquidity positions
- **`POST /get-all-token-pairs`** - All token pairs
- **`POST /get-token-pairs`** - Pairs for a token
#### Bitcoin/UTXOs
Bitcoin balance and UTXO queries.
- **`POST /get-address-balance`** - Address balance
- **`POST /get-taproot-balance`** - Taproot balance
- **`POST /get-address-utxos`** - Address UTXOs
- **`POST /get-account-utxos`** - Account UTXOs
#### History
Transaction history for AMM operations.
- **`POST /get-pool-swap-history`** - Pool swap history
- **`POST /get-token-swap-history`** - Token swap history
- **`POST /get-pool-mint-history`** - Liquidity adds
- **`POST /get-pool-burn-history`** - Liquidity removes
- **`POST /get-address-swap-history-for-pool`** - Address swaps
#### Bitcoin Price
Real-time BTC price from Uniswap V3 WBTC/USDC pool.
- **`POST /get-bitcoin-price`** - Current BTC price in USD
- **`POST /get-bitcoin-market-chart`** - Historical price data
- **`POST /get-bitcoin-market-weekly`** - 52-week high/low
- **`POST /get-bitcoin-markets`** - Market summary
#### OPI (BRC-20)
Standard OPI endpoints for BRC-20 indexing (via `/v4/opi` base).
- **`GET /v1/brc20/balance`** - Get balance
- **`GET /v1/brc20/history`** - Get history
- **`GET /v1/brc20/token_summary`** - Token info
### Health Check
```bash
GET https://mainnet.subfrost.io/v4/api/health
```
Response:
```json
{
"status": "ok",
"timestamp": 1699123456
}
```
### Example request
Get Bitcoin price:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-price \
-H "Content-Type: application/json" \
-d '{}'
```
Get 52-week high/low and current price:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-bitcoin-market-weekly \
-H "Content-Type: application/json" \
-d '{}'
```
### JavaScript Example
```javascript
const API_BASE = 'https://mainnet.subfrost.io/v4/api';
async function getAlkanesByAddress(address) {
const response = await fetch(`${API_BASE}/get-alkanes-by-address`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
});
const data = await response.json();
if (data.statusCode !== 200) {
throw new Error(data.error);
}
return data.data;
}
const alkanes = await getAlkanesByAddress('bc1p...');
console.log('Alkane holdings:', alkanes);
```
### Next Steps
- [Alkanes Endpoints](./alkanes): Token queries
- [Pools & AMM](./pools): Liquidity operations
- [Bitcoin Data](./bitcoin): Balance and UTXOs
- [History](./history): Transaction history
---
## Pools & AMM REST Endpoints
Query and interact with alkanes AMM liquidity pools.
### get-pools
List all liquidity pools for a factory.
**Endpoint:** `POST /get-pools`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pools \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}}'
```
---
### get-all-pools-details
Get detailed information for all pools.
**Endpoint:** `POST /get-all-pools-details`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-pools-details \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}}'
```
---
### get-pool-details
Get detailed information about a specific pool.
**Endpoint:** `POST /get-pool-details`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
- `poolId` (object) - Pool ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-pool-details \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}, "poolId": {"block": "4", "tx": "1"}}'
```
---
### get-all-token-pairs
Get all available token pairs.
**Endpoint:** `POST /get-all-token-pairs`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-all-token-pairs \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}}'
```
---
### get-token-pairs
Get pairs containing a specific token.
**Endpoint:** `POST /get-token-pairs`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
- `alkaneId` (object) - Token ID with `block` and `tx` fields
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-token-pairs \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}, "alkaneId": {"block": "4", "tx": "65522"}}'
```
---
### address-positions
Get liquidity positions for an address.
**Endpoint:** `POST /address-positions`
**Parameters:**
- `factoryId` (object) - Factory ID with `block` and `tx` fields
- `address` (string) - Bitcoin address
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/address-positions \
-H "Content-Type: application/json" \
-d '{"factoryId": {"block": "4", "tx": "65522"}, "address": "bc1p..."}'
```
---
### get-alkane-swap-pair-details
Get swap pair details for a specific alkane token.
**Endpoint:** `POST /get-alkane-swap-pair-details`
**Parameters:**
- `tokenId` (object) - Token ID with `block` and `tx` fields
- `factoryId` (object) - Factory ID with `block` and `tx` fields
- `page` (number, optional) - Page number
- `limit` (number, optional) - Maximum results
Example request:
```bash
curl -X POST https://mainnet.subfrost.io/v4/api/get-alkane-swap-pair-details \
-H "Content-Type: application/json" \
-d '{"tokenId": {"block": "4", "tx": "65522"}, "factoryId": {"block": "4", "tx": "65522"}, "limit": 20}'
```
---
### JavaScript Example
```javascript
class AMMClient {
constructor(apiKey, network = 'mainnet') {
this.baseUrl = `https://${network}.subfrost.io/v4/api`;
this.apiKey = apiKey;
this.factoryId = { block: "4", tx: "65522" };
}
async post(endpoint, body = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-subfrost-api-key': this.apiKey
},
body: JSON.stringify(body)
});
return (await response.json()).data;
}
async getAllPools() {
return this.post('/get-all-pools-details', {
factoryId: this.factoryId
});
}
async getPoolDetails(poolBlock, poolTx) {
return this.post('/get-pool-details', {
factoryId: this.factoryId,
poolId: { block: String(poolBlock), tx: String(poolTx) }
});
}
async getPositions(address) {
return this.post('/address-positions', {
factoryId: this.factoryId,
address
});
}
}
// Usage
const amm = new AMMClient('your-api-key');
const { pools } = await amm.getAllPools();
console.log(`Found ${pools.length} pools`);
const positions = await amm.getPositions('bc1p...');
console.log('LP positions:', positions);
```
---
## Source API
Every alkane the explorer has **verified** carries its exact source with it: the repo, the commit, and, when the verification embedded the source itself (for example for a private repo), a byte-for-byte copy of the source tree the sandbox reproduced the on-chain wasm from. The Source API exposes that tree programmatically, so any surface can render the same file browser you see at `https://explorer.subfrost.io/alkane/{id}/source`.
There are three read endpoints under `https://explorer.subfrost.io/api/v1`, all keyed by alkane ID:
| Endpoint | Returns |
|---|---|
| [`GET /api/v1/{key}/source/{block}/{tx}`](#source-metadata) | Metadata: repo, commit, subdir, package, verification status, entrypoint. |
| [`GET /api/v1/{key}/source/{block}/{tx}/tree`](#file-tree) | The full file tree (paths, types, sizes). |
| [`GET /api/v1/{key}/source/{block}/{tx}/blob?path=…`](#file-contents) | The contents of one file. |
### The data model
The endpoints return four JSON shapes:
- **`AlkaneSource`**: resolved provenance and verification status for an alkane: `{ alkane, block, tx, verified, verdict, match_pct, origin, repo, owner, name, commit, subdir, package, entrypoint, private, fileCount }`.
- **`SourceTree`**: `{ alkane, origin, repo, commit, subdir, entrypoint, nodes: SourceNode[] }`.
- **`SourceNode`**: one entry in the tree, `{ path, type: "blob" | "tree", size? }` (the shape of GitHub's `git/trees` API).
- **`SourceBlob`**: one file's contents, `{ alkane, path, bytes, truncated, binary, text, base64? }`. `text` is `null` for a binary file; binary files served from the explorer's own store carry a `base64` payload instead.
Fields with no value (for example `subdir` on a crate at the repo root) are omitted from the JSON.
#### Where the mapping comes from
There is **no static alkane → repo table**. The mapping is resolved from the alkane's verification record, the same lookup the [verify endpoint](./verify-and-attest#verify) and the explorer's source page use. When an alkane verifies, its record keeps `repo_url`, `commit`, `package`, and, when the source was embedded in the verification, an inline copy of the source tree. Each request resolves that record and picks a serving **origin**:
- **`db`**: the source is served from the explorer's own store of verified sources. This is the exact source the sandbox reproduced, and it works even when the origin repo is **private**, with no GitHub call. It takes priority whenever the record carries an inline copy.
- **`github`**: a public GitHub repo, read live at the verified commit through the GitHub `git/trees` API (tree) and `raw.githubusercontent.com` (file contents), cached per commit. Only GitHub is supported here: a verified alkane whose repo is hosted elsewhere, and which has no inline copy, resolves to `none`.
- **`none`**: the alkane has no browsable verified source. The endpoints return `404`.
An alkane becomes browsable here once it verifies. To make one browsable, submit its source through the [verify endpoint](./verify-and-attest#verify) or [`alkanes-cli upload`](../cli-sdk/build-info#5-upload-submit-a-buildinfo-to-the-explorer); a `reproducible` or `verified` verdict auto-promotes it. A run that embedded its own source can be served from `db` before it has verified, so always check the `verified` flag.
### Authentication
Same as the rest of the [`/api/v1` API](./verify-and-attest#authentication): the API key is a path segment, `/api/v1/{key}/source/…`, from the same key store as `https://mainnet.subfrost.io/v4/{key}`. Any active key may read. See [API Keys](../platform/api-keys).
A missing, unknown, or inactive key gets `401`.
### Source metadata
```
GET https://explorer.subfrost.io/api/v1/{key}/source/{block}/{tx}
```
Provenance and verification status for an alkane's source, plus the `entrypoint` file a client should open first. The entrypoint is the package's `src/lib.rs` when it can be found, otherwise the root `src/lib.rs`, any other `src/lib.rs`, the root `Cargo.toml`, the first `.rs` file, or the first file.
```bash
curl -sS https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/source/2/0
```
Example response (illustrative values):
```json
{
"ok": true,
"source": {
"alkane": "2:0",
"block": "2",
"tx": "0",
"verified": true,
"verdict": "reproducible",
"match_pct": 100.0,
"origin": "github",
"repo": "https://github.com/kungfuflex/alkanes-rs",
"owner": "kungfuflex",
"name": "alkanes-rs",
"commit": "fb11ee0e",
"package": "alkanes-std-genesis-alkane-upgraded-eoa",
"entrypoint": "crates/alkanes-std-genesis-alkane-upgraded-eoa/src/lib.rs",
"private": false,
"fileCount": 214
}
}
```
`fileCount` counts files (`blob` nodes) in the tree. If the tree cannot be listed, the metadata is still returned, with `fileCount: 0`.
| Status | Cause |
|---|---|
| `400` | `block` or `tx` is not numeric. |
| `401` | Missing, unknown, or inactive API key. |
| `404` | No browsable verified source for this alkane (including when the verifier could not be reached). |
| `502` | Unexpected upstream failure. |
### File tree
```
GET https://explorer.subfrost.io/api/v1/{key}/source/{block}/{tx}/tree
```
The full recursive file listing at the verified commit. `nodes` mirrors the GitHub `git/trees` shape: both `blob` (file) and `tree` (directory) entries, with `size` in bytes for files. For the `github` origin the listing covers the whole repository; for the `db` origin it covers the stored files, with directory entries derived from their paths.
```bash
curl -sS https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/source/32/0/tree
```
Example response (abbreviated, illustrative values):
```json
{
"ok": true,
"tree": {
"alkane": "32:0",
"origin": "db",
"repo": "https://github.com/subfrost/subfrost-alkanes",
"commit": "0748786d",
"subdir": "crates/fr-btc",
"entrypoint": "crates/fr-btc/src/lib.rs",
"nodes": [
{ "path": "Cargo.toml", "type": "blob", "size": 412 },
{ "path": "crates", "type": "tree" },
{ "path": "crates/fr-btc", "type": "tree" },
{ "path": "crates/fr-btc/src/lib.rs", "type": "blob", "size": 18422 }
]
}
}
```
| Status | Cause |
|---|---|
| `400` | `block` or `tx` is not numeric. |
| `401` | Missing, unknown, or inactive API key. |
| `404` | No browsable verified source for this alkane. |
| `502` | The listing failed (the repo is private on GitHub, deleted, or GitHub rate-limited the request). |
### File contents
```
GET https://explorer.subfrost.io/api/v1/{key}/source/{block}/{tx}/blob?path=…
```
The contents of one file. `path` should be a `blob` path from the tree. Returns a JSON `SourceBlob` by default; add `raw=1` for the raw bytes (`text/plain`, or `application/octet-stream` for a binary file).
```bash
curl -sS "https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/source/2/0/blob?path=crates/alkanes-std-genesis-alkane-upgraded-eoa/src/lib.rs"
```
Example response (text abbreviated):
```json
{
"ok": true,
"blob": {
"alkane": "2:0",
"path": "crates/alkanes-std-genesis-alkane-upgraded-eoa/src/lib.rs",
"bytes": 14071,
"truncated": false,
"binary": false,
"text": "use alkanes_runtime::message::MessageDispatch;\n..."
}
}
```
Raw bytes, for piping straight into a file or an editor pane:
```bash
curl -sS "https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/source/2/0/blob?path=crates/alkanes-std-genesis-alkane-upgraded-eoa/src/lib.rs&raw=1"
```
Limits and binary files depend on the origin:
- **`github` origin**: a file over 512 KiB comes back with `truncated: true` and `text` cut at 512 KiB; `bytes` is still the full size. A file containing a NUL byte is treated as binary (`binary: true`, `text: null`) and has no `base64` payload, so `raw=1` returns an empty body for it.
- **`db` origin**: files are never truncated. Binary files carry a `base64` payload and `text: null`, and `raw=1` streams their bytes as `application/octet-stream`.
| Status | Cause |
|---|---|
| `400` | `block` or `tx` is not numeric, or `path` is missing. |
| `401` | Missing, unknown, or inactive API key. |
| `404` | No browsable verified source, or the file is not in the tree or could not be read. |
| `502` | Unexpected upstream failure. |
### Embedding the browser
The model maps one-to-one onto a two-pane browser (file tree plus viewer):
1. `GET …/source/{block}/{tx}`: read `entrypoint`. If `verified` is `false` or you get `404`, show "not verified yet".
2. `GET …/tree`: render `nodes` as a collapsible tree (fold `blob` paths on `/` into directories; `tree` nodes also give you empty directories).
3. On a file click, `GET …/blob?path=…`: render `text` with line numbers. If `binary` is `true`, show a "binary file" placeholder, or decode `base64` when it is present.
### Next steps
- [Verify and Attest](./verify-and-attest): publish an alkane's source so it becomes browsable.
- [Verifying alkanes](../../build/verifying-alkanes): how verification works.
---
## Verify and Attest
The [SUBFROST explorer](https://explorer.subfrost.io/alkanes) exposes a contract verification API at `https://explorer.subfrost.io/api/v1`. It has two write routes:
- **[`verify`](#verify)**: any active API key. The explorer rebuilds the source in a sandbox and publishes it only if the rebuild matches the on-chain bytecode.
- **[`attest`](#attest)**: admin key only. Records a verified source directly, without a rebuild.
To read a verified alkane's source back, see the [Source API](./source-api). For what the verdicts mean, see [Verifying alkanes](../../build/verifying-alkanes).
:::tip
You rarely call these endpoints by hand. [`alkanes-cli upload`](../cli-sdk/build-info#5-upload-submit-a-buildinfo-to-the-explorer) maps a BuildInfo onto the right body and POSTs it for you. The `curl` examples below are the raw HTTP equivalents.
:::
### Authentication
The API key is part of the path: `/api/v1/{key}/…`. Keys come from the same key store as the mainnet gateway, so a key that works at `https://mainnet.subfrost.io/v4/{key}` works here too. The `x-subfrost-api-key` header is not read by this API; put the key in the path. See [API Keys](../platform/api-keys) to create one.
- **Any active key** may call `verify`. The verifier only publishes a source when its own diff confirms the rebuild, so a bad recipe just fails; nothing is trusted blindly.
- **`attest` requires an admin key** issued by the SUBFROST team. Any other valid key gets `403`.
These write routes **fail closed**: there is no keyless tier, and a missing, unknown, or inactive key gets `401`.
### Verify
```
POST https://explorer.subfrost.io/api/v1/{key}/verify
```
Rebuild an alkane from a source recipe in the sandbox and diff it against the on-chain bytecode. If the diff engine confirms the match (verdict `reproducible` or `verified`), the source is **auto-promoted** to the alkane's verified-source panel and becomes readable through the [Source API](./source-api).
:::note Minimal recipe
You almost never need to fill in the reproduction fields by hand. The verifier fetches the on-chain bytecode once and **reverses the recipe out of it**: the embedded panic paths carry the exact git-dependency revs, `HOME`, and host triple; the `producers` section carries the clang and rustc versions; and the deploy transaction's confirmation time gives the crates.io freeze date. So a body of just `{ alkane, repo_url, commit, package }` (plus `subdir` when the crate is not at the repo root) reproduces most alkanes. Any field you *do* send **overrides** its auto-reversed value; send one only to correct a mis-reversal.
:::
#### Request body
Only `alkane` and `repo_url` are required. Every other field is optional, and fields not in this table (or sent as `null` or `""`) are dropped before the request reaches the verifier.
| Field | Type | Notes |
|---|---|---|
| `alkane` | string | **Required.** Alkane ID as `"block:tx"`, both numeric (for example `"4:797"`). |
| `repo_url` | string | **Required.** Source git repository. |
| `commit` | string | Commit or ref to build. |
| `package` | string | Cargo package (`-p`). |
| `subdir` | string | Path of the crate inside the repo, if it is not at the root. |
| `rustc` | string | rustc version (for example `1.82.0`). Auto-reversed from the bytecode. |
| `clang_version` | string | clang version recorded in `producers` (for example `14.0.0`). Auto-reversed from the bytecode. |
| `home_dir` | string | Build `HOME`; reconstructs the registry, git and toolchain paths. Auto-reversed from the bytecode. |
| `freeze_commit` | string | `crates.io-index-archive` commit for the registry time-machine. If omitted, it is resolved from `git_date`. |
| `git_url` | string | A single git dependency to pin. Defaults to `https://github.com/kungfuflex/alkanes-rs`. |
| `alkanes_rev` | string | The rev to pin `git_url` to. |
| `git_source_form` | string | `bare` or `rev`: how that dependency's source-id was spelled in the origin lockfile. Defaults to `bare`. |
| `git_pins` | string | Several git dependency pins at once, one `url rev [bare\|rev]` per line. Pins **every** listed dependency (a contract commonly pins both `alkanes-rs` and `metashrew`), which the single `git_url`/`alkanes_rev` pair cannot express. Usually unnecessary: the verifier derives pins from the bytecode's checkout paths. |
| `git_date` | string | Deploy date (`YYYY-MM-DD` or RFC 3339). Freezes the crates.io index **and** any still-unpinned git dependency to what existed then, which is the fix for a contract that ships no committed lockfile. If neither `git_date` nor `freeze_commit` is sent, it defaults to the deploy transaction's block time (or earlier, when the dependency versions in the bytecode show the lockfile was resolved before deployment). |
| `build_env` | string | Extra build environment variables, one `KEY=VALUE` per line. |
| `typo_fix` | boolean | `true` rewrites a malformed `https:/github.com` URL in the repo's `Cargo.toml` to `https://github.com` before resolving. |
| `inline_src_b64` | string | The full source tree as a base64-encoded `tar.gz`. When present, the builder uses it instead of cloning `repo_url`, so a private or deleted repo can still verify; `repo_url` and `commit` are then recorded as provenance only. The verified tree is stored with the result, so the Source API serves it without GitHub access. |
When the repo commits its own `Cargo.lock`, the builder treats that lockfile as authoritative and skips the git pinning and date-freeze rewrites. [Reproducible builds](../../build/reproducible-builds) explains each axis.
#### Examples
A full recipe for DIESEL (`2:0`):
```bash
curl -sS -X POST https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/verify \
-H 'content-type: application/json' \
-d '{
"alkane": "2:0",
"repo_url": "https://github.com/kungfuflex/alkanes-rs",
"commit": "fb11ee0e",
"package": "alkanes-std-genesis-alkane-upgraded-eoa",
"rustc": "1.86.0",
"clang_version": "20.1.7",
"home_dir": "/Users/kevinyao",
"git_source_form": "bare"
}'
```
The same alkane usually verifies from a **minimal** body, because the verifier reverses `rustc`, `clang_version`, `home_dir`, the git pins, and the crates.io freeze from the bytecode itself:
```bash
curl -sS -X POST https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY/verify \
-H 'content-type: application/json' \
-d '{
"alkane": "2:0",
"repo_url": "https://github.com/kungfuflex/alkanes-rs",
"commit": "fb11ee0e",
"package": "alkanes-std-genesis-alkane-upgraded-eoa"
}'
```
#### Response
The verifier's JSON and HTTP status are passed through unchanged. The rebuild runs as a background job, so a successful response describes a newly **queued** verification run, not a verdict:
```json
{
"run_id": "6f0c…",
"alkane": { "block": "2", "tx": "0" },
"repo_url": "https://github.com/kungfuflex/alkanes-rs",
"commit": "fb11ee0e",
"package": "alkanes-std-genesis-alkane-upgraded-eoa",
"status": "building",
"stage": "queued"
}
```
When the run finishes, the verdict appears on the alkane's explorer page (`https://explorer.subfrost.io/alkane/{id}`). A `reproducible` or `verified` verdict means the source is now promoted, and [`GET /api/v1/{key}/source/{block}/{tx}`](./source-api#source-metadata) returns it with `verified: true`.
#### Errors
| Status | Cause |
|---|---|
| `400` | Body is not valid JSON, `alkane` is not a numeric `"block:tx"`, or `repo_url` is missing. |
| `401` | Missing, unknown, or inactive API key. |
| `502` | The verifier could not be reached, or did not answer within 25 seconds. |
| other | Any other status comes from the verifier and is passed through, for example `422` when a field has the wrong type (such as `build_env` sent as an array) or `503` when verification is temporarily unavailable. |
### Attest
```
POST https://explorer.subfrost.io/api/v1/{key}/attest
```
**Admin only.** Directly record a bytecode-to-verified-source mapping **without** a sandbox rebuild. This exists for results proven outside the verifier's own sandbox that it cannot reproduce itself, for example a build proven on a host the verifier does not run. It needs an admin key issued by the SUBFROST team because it bypasses the diff check. `alkanes-cli upload` (without `--verify`) is the ergonomic way to call it: it sends the full BuildInfo as the `manifest`.
The attestation is keyed by the bytecode's sha256, so it applies to every alkane that shares that bytecode (for example, every clone of a template).
#### Request body
`repo_url` is required. Identify the bytecode with **either** `block` and `tx` **or** `wasm_sha256`; if both are sent, `wasm_sha256` is used.
| Field | Type | Notes |
|---|---|---|
| `block`, `tx` | string | Alkane ID parts, as **strings** (for example `"4"` and `"76"`). JSON numbers are rejected. |
| `wasm_sha256` | string | Bytecode sha256, as an alternative to `block` and `tx`. |
| `repo_url` | string | **Required.** Source repository. |
| `commit`, `subdir`, `rustc`, `alkanes_rev` | string | Source provenance, recorded verbatim. |
| `verdict` | string | The attested verdict (for example `verified`). Defaults to `verified`. |
| `match_pct` | number | Normalized match percentage. |
| `note` | string | Free-text explanation of the residual. |
| `manifest` | object | Optional structured manifest (for example a full BuildInfo), stored with the attestation and shown as the reproduction recipe. |
#### Example
```bash
curl -sS -X POST https://explorer.subfrost.io/api/v1/$ADMIN_KEY/attest \
-H 'content-type: application/json' \
-d '{
"block": "4", "tx": "76",
"repo_url": "https://github.com/kungfuflex/alkanes-rs",
"verdict": "verified",
"match_pct": 96.7,
"note": "cross-host secp256k1 C-object memory-base residual (~6 bytes, shifts many offsets); logic and producers reproduced from source"
}'
```
#### Response
The stored record: `wasm_sha256`, `repo_url`, the provenance fields you sent, `verdict`, `match_pct`, `note`, `verified_via` (the `block:tx` you attested through, if any), and `manifest`.
#### Errors
| Status | Cause |
|---|---|
| `400` | Body is not valid JSON, `repo_url` is missing, or neither `wasm_sha256` nor both `block` and `tx` were sent. |
| `401` | Missing, unknown, or inactive API key. |
| `403` | The key is valid but is not an admin key. |
| `404` | No bytecode exists for the given `block` and `tx`. |
| `422` | A field has the wrong type, for example `block` or `tx` sent as a number. |
| `502` | The verifier could not be reached, or did not answer within 15 seconds. |
### Next steps
- [Source API](./source-api): read a verified alkane's file tree and files.
- [BuildInfo Workbench](../cli-sdk/build-info): produce a recipe locally and upload it.
- [Reproducible builds](../../build/reproducible-builds): what each recipe field reconstructs.
---
## Glossary
### Protocol and tokens
* **SUBFROST:** A decentralized custodian for Bitcoin. It moves BTC into a programmable on-chain form and back, without a single company holding the coins.
* **Alkanes:** A smart-contract metaprotocol built on the Bitcoin blockchain. SUBFROST is built on it.
* **Metaprotocol:** A protocol built on top of another protocol. Alkanes is a metaprotocol on Bitcoin.
* **Protorunes:** The standard Alkanes is compatible with. Alkanes is a protorunes-compatible protocol, not a fork of Runes.
* **Protostone:** The unit of Alkanes execution encoded in a Bitcoin transaction. A transaction can carry several protostones (for example transfer, then mint, then call).
* **Cellpack:** The message an Alkanes call carries, a list beginning with the target contract and an opcode, followed by inputs.
* **Alkane ID:** A contract's address, a `[block, tx]` pair identifying where it was deployed.
* **View function:** A read-only call that runs against indexer state and costs nothing, used to query balances, simulate calls, or read a contract's ABI.
* **Opcode:** A numbered method on a contract. A call names the target Alkane ID and an opcode.
* **Runestone:** A message embedded in an `OP_RETURN` output of a Bitcoin transaction.
* **`OP_RETURN`:** A Bitcoin script opcode that embeds a small amount of data in a transaction.
* **BRC2.0:** An EVM-compatible smart-contract layer on Bitcoin (also called brc20-prog) that runs Solidity contracts whose state is derived from Bitcoin transactions.
* **`frBTC`:** Bitcoin-backed value inside Alkanes, pegged 1:1 to BTC and usable across Bitcoin DeFi.
* **`FR-BTC`:** The BRC2.0 deployment of frBTC.
* **DIESEL:** The protocol's fair-launch native token. Every Bitcoin block emits the same amount of DIESEL as it emits new bitcoin, halving alongside Bitcoin.
* **FIRE:** FIRE is the Alkanes governance token. It is also the token behind the FIRE vault, tied to bond mechanics priced against the protocol treasury through the oracle.
* **`dxBTC`:** A yield-bearing synthetic Bitcoin asset (planned).
* **`FUEL`:** FUEL is the SUBFROST protocol governance token (planned). Not to be confused with lowercase fuel, below.
* **fuel:** The compute budget an Alkanes contract call is allowed to spend, comparable to gas on Ethereum in what it measures. Unlike gas, it is free: each block's fixed fuel budget is shared, by size, among the transactions that actually run contracts, and nothing is charged against it. Network fees are the ordinary Bitcoin miner fee, in native BTC.
* **AMM:** Automated Market Maker. A decentralized exchange that prices assets with a formula rather than an order book.
* **Wrap / Unwrap:** Wrapping locks BTC and mints frBTC 1:1; unwrapping burns frBTC and releases the underlying BTC through the signer group.
* **Oracle:** A decentralized group that brings outside data (mainly prices) on-chain by agreement, so no single party dictates the value.
* **Signer set:** The group of participants that collectively custodies the BTC backing frBTC through threshold signing.
* **PSBT:** Partially Signed Bitcoin Transaction. The format a wallet signs when authorizing a transaction.
### Alkane IDs
An Alkane ID is written `block:tx`. For example, frBTC is `32:0`, which appears as `[32, 0]` in calldata arrays and as `{block: 32n, tx: 0n}` in protostone JSON.
| Token | Mainnet Alkane ID |
| --- | --- |
| `frBTC` | `32:0` |
| DIESEL | `2:0` |
| frUSD | `4:1776` |
### Cryptography
* **Schnorr signature:** The signature scheme Bitcoin adopted in Taproot (BIP-340), over the secp256k1 curve. Its linearity lets partial signatures combine into one.
* **FROST:** Flexible Round-Optimized Schnorr Threshold. A threshold scheme letting a dynamic set of signers produce one Schnorr signature without any signer holding the full private key.
* **ROAST:** Robust Asynchronous Schnorr Threshold. A robust, asynchronous optimization of FROST that keeps signing moving when some participants are slow or offline.
* **DKG:** Distributed Key Generation. A ceremony where participants jointly produce one group public key while each holds only a secret share of the private key.
* **Keystore:** An encrypted file (or hardware-backed store) that holds a user's sensitive cryptographic material.
* **AES-256-GCM:** A modern authenticated encryption cipher providing both confidentiality and integrity.
* **PBKDF2:** A key-derivation function that stretches a passphrase so guessing it offline is expensive.
* **ASCII armoring:** Encoding binary data as human-readable text so it is easy to store and move.
### Indexing
* **metashrew:** The base indexer that runs the Alkanes WASM against Bitcoin block data and answers view functions. It is the source of truth for on-chain state.
* **subshrew:** The v10 indexer that runs the metashrew runtime against TiKV, decoupling the view layer from indexing.
* **rockshrew-mono:** A single-binary way to run the metashrew runtime with a local database.
* **TiKV:** A distributed key-value store used as the shared state backend for the v10 indexer.
* **ESPO:** A secondary data indexer for bulk history and aggregates (trade counts, holder lists) that the base indexer does not compute.
* **WASM:** WebAssembly. A binary instruction format for a stack-based virtual machine, used for both contracts and the indexer.
* **`wasip2`:** A standardized interface for running WebAssembly modules across systems.
---
## Alkanes Integration
The Alkanes CLI (`alkanes-cli`) is the primary command-line tool for interacting with the Alkanes metaprotocol on Bitcoin. It provides commands for wallet management, token operations, contract deployment, and querying the network.
### Installation
Install from the alkanes-rs repository:
```bash
git clone https://github.com/kungfuflex/alkanes-rs
cd alkanes-rs
cargo build --release
```
The binary will be available at `target/release/alkanes-cli`.
### Global Options
All commands support these global options:
| Option | Description |
|--------|-------------|
| `--wallet-file ` | Path to wallet file |
| `--passphrase ` | Wallet passphrase for signing |
| `--wallet-address ` | Watch-only mode (read-only) |
| `--jsonrpc-url ` | Custom JSON-RPC endpoint |
| `--provider ` | Network: `regtest`, `signet`, `mainnet` |
| `--frbtc-address ` | Override frBTC contract address |
### Wallet Commands
#### Create a Wallet
```bash
alkanes-cli wallet create
```
Creates a new wallet with a 12-word mnemonic phrase. Store this phrase securely.
#### List Addresses
```bash
alkanes-cli wallet addresses p2tr:0-5
```
Shows wallet addresses. Pass one or more positional range specs of the form `TYPE:START-END` (for example `p2tr:0-5` or `p2sh:0-500`) to choose which addresses to display. With no range, it shows the first 5 addresses of each type for the current network.
#### Check Balance
```bash
alkanes-cli wallet balance
```
Displays BTC and token balances for your wallet.
#### List UTXOs
```bash
alkanes-cli wallet utxos
```
Lists all unspent transaction outputs in your wallet.
### Alkanes Operations
#### Wrap BTC to frBTC
```bash
alkanes-cli alkanes wrap-btc \
--to \
--from \
--change \
--fee-rate \
-y
```
Wraps native BTC to frBTC tokens. `