# 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.
The FIRE flywheel A closed loop of four steps. Stakers lock DIESEL/frBTC LP, and locked LP earns the 85 percent share of each block's emission. The remaining 15 percent funds the bonding pool. Bonders pay LP for FIRE at a discount to the market price. The treasury keeps that LP permanently, which raises the floor price, and a higher floor protects holders and makes committing liquidity safer. Stakers lock DIESEL / frBTC LP FIRE is emitted each block Bonders pay LP for FIRE The treasury keeps that LP the longer the lock, the more FIRE 85% to staking, 15% to bonds at a discount to the market price so the floor price rises locked LP earns 85% the 15% funds bonds the LP never leaves the floor protects FIRE fixed emission, a floor that only rises
| 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`. ![dxBTC diagram](../Diagrams/dxBTC_example.png) 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.
Swapping native BTC into DIESEL with one signature A user signs once. Their BTC is atomically wrapped into frBTC, swapped through an AMM, and DIESEL is returned to the user, all on Bitcoin L1. {/* user, left */} {/* BTC */} BTC {/* SUBFROST logomark, the atomic wrap */} {/* frBTC */} frBTC {/* AMM swap */} {/* DIESEL */} DIESEL {/* user, right */} {/* connectors */} {/* captions */} 1 User Signature Atomic Wrap AMM Swap frBTC to DIESEL, or any Alkanes token
`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. ETHEREUM BITCOIN L1 You USDC or USDT SUBFROST bridge vault Curve 3pool the reserve FROST signer group frUSD 4:1776 deposit payout sees it mints burns held as LP released on a burn 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. BITCOIN L1 FROST signer group frUSD 4:1776 mints burns ETHEREUM You USDC or USDT SUBFROST bridge vault Curve 3pool the reserve deposit payout held as LP released on a burn sees the deposit signs the release 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. BNB CHAIN Depositor signs one intent Permit2SweepForwarder pull and bridge, one transaction Stargate pool takes the USDT in 3,000.000000 Permit2 3,000.000000 sendToken LayerZero, 38 s Stargate fee 6.000001 2,993.999999 ETHEREUM Stargate pool releases the USDT gateway no code until settle 2,993.999999 00:32:11 GatewayFactory settle, 00:44:35 deployed and swept in one transaction frUSD vault 2,993.999999 depositAndBridge BITCOIN L1 payment #52 2,989.017990 after fees FROST signer group frUSD bc1pcpfr…qvhvqwd mints 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. `` is an integer amount in satoshis (the example below wraps 1000000 sats = 0.01 BTC), and `--to` is required. The `-y` flag auto-confirms the transaction. **Example:** ```bash alkanes-cli alkanes wrap-btc 1000000 \ --to bc1p... \ --from bc1q... \ --change bc1q... \ --fee-rate 10 \ -y ``` #### Check Token Balance ```bash alkanes-cli alkanes getbalance
``` #### View Pending Unwraps ```bash alkanes-cli alkanes unwrap --block-tag latest ``` Shows pending frBTC unwrap requests waiting to be processed. #### Execute Custom Transactions ```bash alkanes-cli alkanes execute \ --inputs \ --to \ -y ``` For advanced users building custom Alkanes transactions. Protostone specs are positional arguments, not a flag. **Input format:** - `B:amount` — Bitcoin amount in sats - `BLOCK:TX:amount` — Specific token amount #### Inspect Contracts ```bash alkanes-cli alkanes inspect --disasm --meta ``` Disassemble and inspect contract bytecode. #### Simulate Transactions ```bash alkanes-cli alkanes simulate 32:0:103 ``` Simulate a contract call without broadcasting anything. The argument is `BLOCK:TX:OPCODE[:ARG...]`; this example calls opcode 103 (Get Signer, see the table below) on the frBTC contract `{32, 0}`. ### Bitcoin RPC Commands Direct Bitcoin Core RPC access: ```bash # Get block count alkanes-cli bitcoind getblockcount # Get raw transaction alkanes-cli bitcoind getrawtransaction # Decode transaction alkanes-cli bitcoind decoderawtransaction # Send transaction alkanes-cli bitcoind sendrawtransaction ``` ### Esplora Commands Query Esplora API: ```bash # Get address info alkanes-cli esplora address
# Get address UTXOs alkanes-cli esplora address-utxo
# Get transaction alkanes-cli esplora tx # Fee estimates alkanes-cli esplora fee-estimates ``` ### Transaction Options #### Fee Rate Set custom fee rate in sat/vB: ```bash --fee-rate 15 ``` #### Broadcasting Methods | Option | Description | |--------|-------------| | Default | Standard mempool broadcast | | `--use-slipstream` | MARA Slipstream (bypasses mempool) | | `--use-rebar` | Rebar Shield (private relay with MEV protection) | | `--mine` | Mine immediately (regtest only) | #### Auto-confirm Skip confirmation prompts: ```bash -y, --auto-confirm ``` ### Key Constants | Constant | Value | Description | |----------|-------|-------------| | frBTC Contract | `{32, 0}` | Block 32, Transaction 0 | | Wrap Opcode | `77` | frBTC exchange function | | Unwrap Opcode | `78` | frBTC unwrap function | | Get Signer Opcode | `103` | Query signer public key | | Genesis Block | `880000` | Mainnet genesis | ### Getting Help ```bash alkanes-cli --help alkanes-cli --help ``` For issues and contributions: [kungfuflex/alkanes-rs](https://github.com/kungfuflex/alkanes-rs) ### See Also - [frBTC on Alkanes](./wrapping-frBTC) — Wrapping and unwrapping frBTC - [Alkanes Metaprotocol](../key-components/alkanes) — Protocol overview --- ## BRC2.0 (brc20-prog) ## BRC2.0 (brc20-prog) Integration While Alkanes uses a binary protocol for token operations, BRC2.0 uses standard ord inscriptions with JSON payloads to enable general-purpose computation. ### Setting Up Your Environment #### Prerequisites - Rust and Cargo installed - Bitcoin Core node (or access to one) - Foundry (for Solidity compilation) - The alkanes-rs toolkit #### Installing Foundry ```bash # Install Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # Verify installation forge --version ``` #### Building alkanes-rs ```bash git clone https://github.com/kungfuflex/alkanes-rs cd alkanes-rs cargo build --release ``` ### Writing Your First Contract #### 1. Create a Solidity Contract ```solidity // src/MyToken.sol // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; contract MyToken { string public name = "My BRC2.0 Token"; string public symbol = "MBT"; uint8 public decimals = 18; uint256 public totalSupply; mapping(address => uint256) public balanceOf; event Transfer(address indexed from, address indexed to, uint256 value); function mint(address to, uint256 amount) public { balanceOf[to] += amount; totalSupply += amount; emit Transfer(address(0), to, amount); } function transfer(address to, uint256 amount) public returns (bool) { require(balanceOf[msg.sender] >= amount, "Insufficient balance"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; emit Transfer(msg.sender, to, amount); return true; } } ``` #### 2. Compile with Foundry ```bash # Initialize Foundry project (if not already done) forge init --no-commit # Compile forge build # Output will be in out/MyToken.sol/MyToken.json ``` #### 3. Deploy to Bitcoin Using `alkanes-cli`: ```bash alkanes-cli brc20-prog deploy-contract out/MyToken.sol/MyToken.json \ --from bc1p... \ --change bc1p... \ --fee-rate 30 ``` The CLI will output: - **Commit txid**: The funding transaction - **Reveal txid**: The inscription transaction - **Activation txid**: The activation transaction (sent by default; pass `--no-activation` to skip it) - **Contract address**: The deployed contract's address ### Calling Contract Functions #### ABI Encoding BRC2.0 uses standard Ethereum ABI encoding for function calls: ```text // Function signature: transfer(address,uint256) // Selector: keccak256("transfer(address,uint256)")[:4] = 0xa9059cbb // Arguments: // - to: 0x1234... // - amount: 1000000000000000000 (1 token with 18 decimals) // Encoded calldata: // 0xa9059cbb // 0000000000000000000000001234567890abcdef1234567890abcdef12345678 // 0000000000000000000000000000000000000000000000000de0b6b3a7640000 ``` #### Making a Call ```bash alkanes-cli brc20-prog transact \ --address 0x1234567890abcdef1234567890abcdef12345678 \ --signature "transfer(address,uint256)" \ --calldata "0xrecipient...,1000000000000000000" \ --from bc1p... \ --fee-rate 30 ``` ### FR-BTC: The Reference Implementation FR-BTC is the flagship BRC2.0 application—a trustless BTC wrapper that mints FR-BTC tokens: #### Key Functions | Function | Description | |----------|-------------| | `wrap()` | Mint FR-BTC for BTC sent to the contract | | `unwrap(amount, vout)` | Burn FR-BTC and queue a BTC payment | | `wrapAndExecute2(target, data)` | Wrap BTC and call another contract atomically | #### Example: Wrapping BTC ```bash alkanes-cli brc20-prog wrap-btc 1000000 \ --from bc1q... \ --fee-rate 30 ``` The amount is an integer in satoshis (1000000 sats = 0.01 BTC). ### Cross-Protocol Interoperability #### BRC2.0 ↔ Alkanes BRC2.0 contracts can interact with Alkanes tokens: ``` User sends BTC │ ▼ ┌─────────────────┐ │ FR-BTC.wrap() │ ← BRC2.0 contract │ mints FR-BTC │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Alkanes Vault │ ← Alkanes contract │ holds FR-BTC │ │ issues auth │ └─────────────────┘ │ ▼ User receives vault tokens ``` #### The wrapAndExecute2 Pattern This powerful pattern atomically wraps BTC and calls another contract: ```solidity function wrapAndExecute2(address target, bytes calldata data) public { // 1. Wrap incoming BTC to FR-BTC uint256 wrapped = _wrap(msg.sender); // 2. Approve target contract _approve(msg.sender, target, wrapped); // 3. Call target with provided data (bool success, ) = target.call(data); require(success, "Target call failed"); } ``` ### Good to Know #### Inscription Protection If your UTXOs contain other inscriptions, BRC2.0 automatically protects them via a split transaction: ``` UTXO with inscription │ ▼ ┌─────────────────┐ │ Split TX │ ├─────────────────┤ │ Out 1: Clean │ → Used for commit │ Out 2: Original │ → Keeps inscription safe └─────────────────┘ ``` #### Fee Calculation BRC2.0 uses precise fee calculation via the "dummy transaction" technique: 1. Build a complete reveal transaction with real script 2. Calculate exact vsize (not estimated) 3. Set commit output = inscription_output + reveal_fee 4. No change output (avoids dust) #### Mempool State Tracking The executor tracks inscription state through unconfirmed transactions, allowing: - Resume from partial deployments - Trace inscriptions through pending UTXOs - Handle reorgs gracefully ### Troubleshooting #### Common Issues **"Insufficient funds"** - Ensure your wallet has enough BTC for fees + inscription output (546 sats) - Check that UTXOs aren't locked or already spent **"Frontrun detected"** - This shouldn't happen with atomic broadcasting - If it does, the transaction can be resumed **"Contract address mismatch"** - Verify the nonce used in address derivation - Check that the sender address is correct #### Getting Help - GitHub Issues: [kungfuflex/alkanes-rs](https://github.com/kungfuflex/alkanes-rs) - Message @gabe_subfrost on X ### Key Implementation Files The reference implementation lives in `alkanes-rs`: | File | Purpose | |------|---------| | `execute.rs` | Core execution engine (commit-reveal-activation) | | `envelope.rs` | Ord inscription envelope construction | | `calldata.rs` | Ethereum ABI encoding | | `contract_address.rs` | Address derivation | | `types.rs` | Parameter and result types | ### See Also - [FR-BTC - BRC2.0](./frBTC-brc20) — Building scripts with FR-BTC - [BRC2.0 Protocol](../key-components/brc20-prog) — Protocol technical details --- ## FR-BTC on BRC2.0 FR-BTC provides two powerful functions for building composable DeFi applications on BRC2.0: `wrapAndExecute` and `wrapAndExecute2`. These functions atomically wrap BTC into FR-BTC and execute custom logic in a single transaction. ### Overview When a user sends BTC to the FR-BTC contract, they can choose to: 1. Simply wrap it into FR-BTC tokens (`wrap()`) 2. Wrap and execute a deployed script (`wrapAndExecute`) 3. Wrap and execute a script with custom calldata (`wrapAndExecute2`) The key benefit is **atomic execution**: if your script fails for any reason (including running out of gas), the user still receives their FR-BTC tokens safely. ### Contract Addresses | Network | Chain ID | FR-BTC Address | |---------|----------|---------------| | BRC2.0 Mainnet | `0x4252433230` | `0xdBB5b6A1D422fca2813cF486e5F986ADB09D8337` | | BRC2.0 Signet | `0x425243323073` | `0x1EB63D0d0e5A86146B4E1Cebc79b1d6e35093288` | ### How It Works #### wrapAndExecute Wraps BTC and executes a script deployed via CREATE2: ```solidity function wrapAndExecute(bytes memory script) public; ``` - `script`: The bytecode of the contract to deploy and execute - The script is deployed using CREATE2 (reuses existing deployment if already deployed) - Script must implement `IScript` interface #### wrapAndExecute2 Wraps BTC and calls an already-deployed script with custom calldata: ```solidity function wrapAndExecute2(address target, bytes memory data) public; ``` - `target`: Address of the deployed script contract - `data`: Custom calldata passed to the script - Script must implement `IScript2` interface **`wrapAndExecute2` is the recommended approach** for most use cases since scripts can be deployed once and reused. ### Script Interfaces #### IScript ```solidity interface IScript { function execute(address sender, uint256 amount) external; } ``` - `sender`: The user who initiated the wrap (use this to send tokens to the user) - `amount`: The amount of FR-BTC that was minted #### IScript2 ```solidity interface IScript2 { function execute(address sender, uint256 amount, bytes calldata data) external; } ``` - `sender`: The user who initiated the wrap - `amount`: The amount of FR-BTC that was minted - `data`: Custom calldata for flexible script logic ### Gas Safety FR-BTC implements robust gas safety to ensure users always receive their tokens, even if a script fails or runs out of gas. #### How It Works ``` User calls wrapAndExecute2(target, data) │ ├─ 1. Mint FR-BTC to FrBTC contract ├─ 2. Check gasleft() >= 50,000 (MIN_GAS_FOR_SAFE_RETURN) │ └─ NO → transfer FR-BTC to user, return ├─ 3. Approve script to spend FR-BTC ├─ 4. Calculate gasForScript = gasleft() - 50,000 ├─ 5. Low-level call with limited gas: │ target.call{gas: gasForScript}(execute(...)) │ │ │ ├─ SUCCESS → continue │ └─ FAILURE (revert, OOG, etc.) → transfer FR-BTC to user, return │ └─ 6. Transfer remaining FR-BTC to user ``` #### Key Points 1. **OOG Protection**: Scripts run in a subcall with limited gas. Even if a script infinite-loops, FR-BTC retains 50,000 gas to safely return tokens. 2. **Automatic Token Return**: On any failure, FR-BTC tokens are automatically transferred to the user. 3. **No Trust Required**: Scripts don't need special gas handling - FR-BTC handles it at the caller level. ### Building Scripts #### Basic Script (IScript2) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {IScript2} from "frbtc/interfaces/IScript2.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract MyScript is IScript2 { using SafeERC20 for IERC20; address public immutable frbtc; constructor(address _frbtc) { frbtc = _frbtc; } function execute( address sender, uint256 amount, bytes calldata data ) external override { // Only FR-BTC contract should call this require(msg.sender == frbtc, "only frbtc"); // Your logic here... // The FR-BTC contract has approved `amount` tokens to this contract // Example: transfer FR-BTC to the sender IERC20(frbtc).safeTransferFrom(msg.sender, sender, amount); } } ``` #### Swap Script Example ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {IScript2} from "frbtc/interfaces/IScript2.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface ISwapRouter { function swap( address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, address recipient ) external returns (uint256 amountOut); } contract FrBTCSwapScript is IScript2 { using SafeERC20 for IERC20; address public immutable frbtc; ISwapRouter public immutable router; constructor(address _frbtc, address _router) { frbtc = _frbtc; router = ISwapRouter(_router); } function execute( address sender, uint256 amount, bytes calldata data ) external override { require(msg.sender == frbtc, "only frbtc"); // Decode swap parameters from calldata (address tokenOut, uint256 minAmountOut) = abi.decode(data, (address, uint256)); // Transfer FR-BTC from FR-BTC contract to this script IERC20(frbtc).safeTransferFrom(msg.sender, address(this), amount); // Approve router IERC20(frbtc).safeApprove(address(router), amount); // Execute swap, sending output directly to user router.swap(frbtc, tokenOut, amount, minAmountOut, sender); } } ``` #### Calling from Frontend ```javascript // Encode the calldata for the script const tokenOut = "0x..."; // Token to swap to const minAmountOut = ethers.parseUnits("100", 18); const data = ethers.AbiCoder.defaultAbiCoder().encode( ["address", "uint256"], [tokenOut, minAmountOut] ); // Call wrapAndExecute2 const tx = await frbtc.wrapAndExecute2(swapScriptAddress, data); ``` ### Helper Library: FrBTCLib The `FrBTCLib` library provides chain detection utilities: ```solidity import {FrBTCLib} from "frbtc/libraries/FrBTCLib.sol"; // Get FR-BTC address for current chain address frbtc = FrBTCLib.FRBTC_ADDRESS(); // Check chain bool isMainnet = FrBTCLib.isMainnet(); bool isSignet = FrBTCLib.isSignet(); bool isSupported = FrBTCLib.isSupportedChain(); // Chain IDs uint256 mainnetId = FrBTCLib.MAINNET_CHAIN_ID; // 0x4252433230 uint256 signetId = FrBTCLib.SIGNET_CHAIN_ID; // 0x425243323073 ``` ### CLI Commands Use `alkanes-cli` with the `brc20-prog` subcommand to interact with FR-BTC. Every `` below is an integer amount in satoshis. #### Wrap BTC ```bash alkanes-cli brc20-prog wrap-btc \ --from \ -y ``` #### Unwrap FR-BTC ```bash alkanes-cli brc20-prog unwrap-btc \ --vout \ --to \ -y ``` #### Wrap and Execute Atomically wrap BTC and call a contract: ```bash alkanes-cli brc20-prog wrap-and-execute2 \ --target 0x \ --signature "deposit()" \ -y ``` #### Check Balance `
` is a positional, 0x-prefixed BRC2.0 (EVM) address. ```bash alkanes-cli brc20-prog get-balance
``` #### Get Signer Address ```bash alkanes-cli brc20-prog signer-address ``` #### View Pending Unwraps ```bash alkanes-cli brc20-prog unwrap ``` ### Security Considerations #### Caller Validation Always verify that only the FR-BTC contract can call your script's `execute` function: ```solidity require(msg.sender == frbtc, "only frbtc"); ``` Or using FrBTCLib for multi-chain deployments: ```solidity require(msg.sender == FrBTCLib.FRBTC_ADDRESS(), "only frbtc"); ``` #### Token Handling The FR-BTC contract approves your script to spend the minted tokens before calling `execute`. You must: 1. Transfer tokens from FR-BTC contract using `transferFrom` 2. Use the tokens in your logic 3. Send any remaining tokens to the `sender` If your script reverts or runs out of gas, FR-BTC automatically returns the tokens to the user. #### Reentrancy FR-BTC uses `nonReentrant` modifier on `wrapAndExecute` and `wrapAndExecute2`. Your scripts cannot re-enter these functions. ### Testing Use Foundry to test your scripts: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {MyScript} from "../src/MyScript.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MyScriptTest is Test { MyScript public script; address public frbtc; address public user = address(0x1234); function setUp() public { // Deploy or mock FR-BTC frbtc = address(new MockFrBTC()); script = new MyScript(frbtc); } function testExecute() public { uint256 amount = 1e8; // 1 BTC // Mint tokens to FR-BTC contract (simulating wrap) MockFrBTC(frbtc).mint(frbtc, amount); // Approve script vm.prank(frbtc); IERC20(frbtc).approve(address(script), amount); // Execute as FR-BTC contract bytes memory data = abi.encode(/* your params */); vm.prank(frbtc); script.execute(user, amount, data); // Verify results // ... } function testOnlyFrBTCCanCall() public { vm.expectRevert("only frbtc"); script.execute(user, 1e8, ""); } } ``` ### Best Practices 1. **Keep scripts simple**: Complex logic increases gas costs and failure risk 2. **Validate inputs**: Always check calldata parameters are valid 3. **Use SafeERC20**: For safe token transfers 4. **Test thoroughly**: Test success cases, failure cases, and edge cases 5. **Consider gas costs**: Users pay for script execution gas 6. **Handle dust amounts**: Account for rounding in swaps/calculations ### See Also - [BRC2.0 Integration](./brc20-prog) — Deploy and call BRC2.0 contracts - [frBTC - Alkanes](./wrapping-frBTC) — frBTC on the Alkanes protocol --- ## Reading Alkanes data from Bitcoin Alkanes is a smart contract system on Bitcoin. It has no chain of its own: every Alkanes action is an ordinary Bitcoin transaction carrying a message in its `OP_RETURN`, and the state (balances, contract storage, token supplies) is computed by an indexer that replays those messages block by block, starting at block 880,000. Anyone with Bitcoin block data can reproduce that state without trusting an API. This page answers three questions: [what you need to set up](#what-you-need-to-set-up), [how to extract Alkanes data](#how-to-extract-alkanes-data), and [what the data means](#what-the-data-means). ### Before you start - **Bitcoin Core node (`bitcoind`)**: a full Bitcoin node that serves raw blocks over RPC. The indexer reads blocks from it. [Bitcoin Core](https://bitcoincore.org) - **`OP_RETURN` and Runestone**: an `OP_RETURN` output carries data instead of coins. A Runestone is the Runes protocol's `OP_RETURN` format (it starts with `OP_RETURN OP_13`), and Alkanes messages travel inside it. [Runes specification](https://docs.ordinals.com/runes/specification.html) - **Protostone**: a message packed into a Runestone's protocol field. A protostone with protocol tag `1` is an Alkanes message. [Protostones and cellpacks](../protocol/alkanes#protostones-and-cellpacks) - **Cellpack**: the contract call inside a protostone, a list of integers `[block, tx, opcode, ...inputs]`: which contract, which method, which arguments. [Protostones and cellpacks](../protocol/alkanes#protostones-and-cellpacks) - **Alkanes indexer (`alkanes.wasm`)**: the Alkanes protocol itself, compiled to WebAssembly from `kungfuflex/alkanes-rs`. It executes every message and keeps the resulting state. [alkanes-rs](https://github.com/kungfuflex/alkanes-rs) - **metashrew and `rockshrew-mono`**: metashrew is the framework that runs an indexer program over every Bitcoin block and stores what it writes. `rockshrew-mono` is its single binary: indexer, database and JSON-RPC server. [Indexing with metashrew](../build/indexing-with-metashrew) - **AlkaneId**: the address of a contract or token, written `block:tx`. `2:0` is DIESEL and `32:0` is [frBTC](../tokens/frBTC-overview). - **Trace**: the record of one protostone's execution: the calls it made, the tokens in and out, the storage it wrote, and whether it succeeded. [Reading a trace](#reading-a-trace) - **View function**: a read-only query answered by an indexer program through the `metashrew_view` JSON-RPC method. [`metashrew_*` reference](../api-reference/json-rpc/metashrew) - **The `/traces` feed**: a SUBFROST endpoint that serves Bitcoin blocks with each block's Alkanes traces attached, so a program can index execution results without running any contract. [Path 2](#path-2-index-from-the-traces-feed) - **alspo programs**: SUBFROST's own metashrew programs that index Alkanes state from the `/traces` feed. The SUBFROST explorer reads that feed too. - **SUBFROST API key**: the key for SUBFROST's hosted endpoints, `/traces` included. [API keys](../api-reference/platform/api-keys) ### What you need to set up There are three ways in. They differ in what you get back and in what you have to run. | | Path 1: run the Alkanes indexer | Path 2: index the `/traces` feed | Path 3: decode the message | |---|---|---|---| | **You get** | The full Alkanes state: balances, storage, traces, every view function | Execution results (traces) for every block, indexed your way | What each transaction asked for, not what happened | | **Bitcoin node** | Your own `bitcoind` | Not needed | Any source of raw transactions | | **Also needed** | Rust, `protoc`, a wasm32 `clang`, a large disk | A SUBFROST API key, Rust with the wasm32 target, a wasm32 `clang`, `rockshrew-mono` | Node.js | | **Starts at** | Block 880,000, then every block in order | Any height | Any transaction | **Path 1, the full state:** - **A Bitcoin Core node** synced past block 880,000, with RPC enabled and every block from 880,000 on (a node pruned below that height cannot serve them). `rockshrew-mono` makes only three RPC calls: `getblockcount`, `getblockhash` and `getblock`. - **Matching versions.** The [alkanes-rs README](https://github.com/kungfuflex/alkanes-rs#running-against-mainnet) pins alkanes-rs `v2.2.1-rc.4` with metashrew `v9.0.5-rc.14` and warns that mismatched versions can produce divergent state. - **Rust through rustup.** alkanes-rs pins Rust 1.86.0 in its `rust-toolchain.toml`, and rustup installs that compiler and the `wasm32-unknown-unknown` target on the first build. metashrew builds with stable Rust. - **`protoc`** (the Protocol Buffers compiler) on the `PATH`, and a **`clang` that can target wasm32**. On macOS, Apple's clang cannot: install Homebrew `llvm` and point `CC_wasm32_unknown_unknown` and `AR_wasm32_unknown_unknown` at its `clang` and `llvm-ar`. - **Build tools for metashrew**, which compiles RocksDB. On macOS the Xcode command line tools are enough. On Debian, the repository's own `docker/Dockerfile.indexer` installs `build-essential` and `libclang-dev`. - **Disk and time.** The index grows with the chain: SUBFROST's published mainnet snapshot of it, last updated on 2 June 2026, is 377 GB compressed, on top of the Bitcoin chain itself. [Indexing with metashrew](../build/indexing-with-metashrew) puts a sync from 880,000 on the order of days. **Path 2, the `/traces` feed:** no Bitcoin node. You need a SUBFROST API key (to try it, `https://mainnet.subfrost.io/v4/jsonrpc/traces` answers without one), Rust with the `wasm32-unknown-unknown` target plus the same `protoc` and `clang` as above to build your program, and `rockshrew-mono` to run it (build it as in [Path 1](#path-1-run-the-alkanes-indexer)). Reading a single block needs only Python 3, `protoc`, and `alkanes.proto` from an alkanes-rs checkout (the first command block of Path 1). **Path 3, the message only:** Node.js, and raw transactions from your node, mempool.space or another Esplora API. ### How to extract Alkanes data #### Path 1: run the Alkanes indexer Build the indexer program: ```bash cd ~ git clone https://github.com/kungfuflex/alkanes-rs cd alkanes-rs git checkout v2.2.1-rc.4 cargo build --release --target wasm32-unknown-unknown --features mainnet -p alkanes ``` This writes `target/wasm32-unknown-unknown/release/alkanes.wasm` (about 7.8 MB). The README's version of this command ends in `--locked`, which fails at this tag: the tag's `Cargo.lock` still lists the workspace's own crates at `2.2.1-rc.3`. Without the flag, cargo only rewrites those version numbers. Build metashrew: ```bash cd ~ git clone https://github.com/kungfuflex/metashrew cd metashrew git checkout v9.0.5-rc.14 cargo build --release -p rockshrew-mono ./target/release/rockshrew-mono --version ``` ```text rockshrew-mono 9.0.5-rc.14 ``` Run it against your node, from Alkanes activation: ```bash ~/metashrew/target/release/rockshrew-mono \ --daemon-rpc-url http://localhost:8332 \ --auth : \ --indexer ~/alkanes-rs/target/wasm32-unknown-unknown/release/alkanes.wasm \ --db-path ~/.metashrew \ --start-block 880000 \ --host 127.0.0.1 \ --port 8080 ``` - **Keep `--start-block 880000`.** Without it, indexing starts at block 0. - **Stopping and restarting.** Ctrl-C ends with a tokio panic message (`Cannot drop a runtime in a context where blocking is not allowed`), but committed blocks are kept and a restart resumes after the last one. - **Trust your RPC source.** At startup it warns that SPV validation is off: it checks each block body against the hash the node reports, but it does not verify proof of work. Point `--daemon-rpc-url` only at a node you trust. - **Memory floor for views.** While the machine has less available memory than `--view-memory-floor-mb` (8,192 MB by default), every view answers `View runtime unavailable: system memory pressure`. Indexing continues. On a smaller machine, lower the floor (`--view-memory-floor-mb 0` turns the check off). - **A quick test without a node.** The SUBFROST gateway answers the same three RPC calls: `--daemon-rpc-url https://mainnet.subfrost.io/v4/`, with no `--auth`. It is shared infrastructure with [rate limits](../api-reference/platform/rate-limits), so a full sync belongs on your own node. - **Starting from a snapshot.** Instead of indexing from 880,000, you can start from SUBFROST's snapshot of the database: see [Snapshot download](../api-reference/platform/snapshots). That page says the snapshot targets metashrew `v9.0.5-rc.13` with alkanes-rs `v2.2.0-rc.5`, not the pair above, and asks you to contact support before bootstrapping other versions. On 30 September 2026 the file was 377 GB, last modified on 2 June 2026. Once blocks are in, query the node. `metashrew_height` returns the last indexed block: ```bash curl -s -X POST http://127.0.0.1:8080 -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"metashrew_height","params":[]}' ``` ```text {"id":1,"jsonrpc":"2.0","result":"880115"} ``` Reads go through `metashrew_view` with a view name, a hex-encoded protobuf input and a block tag. The `trace` view returns the execution of one protostone. Its input is an `Outpoint` message: the bytes `0a 20`, the 32-byte txid in internal byte order (the displayed txid reversed), the byte `10`, and the vout. For a protostone the vout is virtual: in a transaction with `n` outputs, the first protostone is `n + 1`, the next `n + 2`, and so on. The first DIESEL mint in Alkanes history, transaction `0f8b4333…a411744d` in block 880,004, has three outputs, so its protostone is vout 4: ```bash TXID=0f8b433391e245856668e8c204e0e56081677a380cadddc8ad58b6e8a411744d INPUT=0a20$(python3 -c "print(bytes.fromhex('$TXID')[::-1].hex())")1004 curl -s -X POST http://127.0.0.1:8080 -H 'content-type: application/json' \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"metashrew_view\",\"params\":[\"trace\",\"0x$INPUT\",\"latest\"]}" \ | python3 -c "import json,sys; print(json.load(sys.stdin)['result'][2:])" | xxd -r -p > trace.bin protoc -I ~/alkanes-rs/crates/alkanes-support/proto --decode=alkanes.AlkanesTrace alkanes.proto < trace.bin ``` The result is an `AlkanesTrace` protobuf, defined in [`alkanes.proto`](https://github.com/kungfuflex/alkanes-rs/blob/v2.2.1-rc.4/crates/alkanes-support/proto/alkanes.proto). The end of the decoded output (the call's exit) reads: ```text response { alkanes { id { block { lo: 2 } tx { } } value { lo: 312500000 } } storage { key: "/seen/\204m\r\000\000\000\000\000" value: "\001\000\000\000" } storage { key: "/totalsupply" value: " \037\377\236\004(\000\000\000\000\000\000\000\000\000\000" } } fuel_used: 139101 } } ``` The same call on the hosted gateway returns the same trace except `fuel_used`: the `v2.2.1-rc.4` indexer records it, and the hosted trace does not carry it. [Reading a trace](#reading-a-trace) explains every field. #### Path 2: index from the `/traces` feed The Alkanes indexer has to execute every contract call since block 880,000 before it can answer anything. If what you need is execution results (who called what, which tokens moved, what reverted), the `/traces` feed hands them to you with each block: ```text https://mainnet.subfrost.io/v4//traces ``` It answers the three Bitcoin Core calls `rockshrew-mono` uses: `getblockcount`, `getblockhash` and `getblock`. The block it returns is the real block with one extra output appended to the coinbase transaction: an `OP_RETURN` that pushes the marker `ASP0` followed by a protobuf `AlkanesBlockTraceEvent`, which holds the trace of every protostone in the block. The header is untouched, so the block hash still matches. Blocks with no Alkanes activity carry the marker with an empty payload. A program reading this feed never re-executes a contract, so it can start at any height. SUBFROST's alspo programs and its explorer work this way. **Read one block without Rust.** This script fetches a block from the feed and writes out the payload, and `protoc` decodes it. The gateway answers HTTP 403 to Python's default `User-Agent`, so the script sends its own.
traces_payload.py (Python 3, standard library only) ```python #!/usr/bin/env python3 """Print the Alkanes trace payload that the /traces feed embeds in a block. usage: traces_payload.py > block.bin Writes protobuf(AlkanesBlockTraceEvent) to stdout; decode it with protoc. """ import json, sys, urllib.request def rpc(url, method, params): body = json.dumps({"jsonrpc": "1.0", "id": 1, "method": method, "params": params}).encode() # the gateway refuses Python's default User-Agent with a 403 req = urllib.request.Request(url, body, {"content-type": "application/json", "user-agent": "alkanes-traces-example"}) return json.load(urllib.request.urlopen(req, timeout=120))["result"] def varint(b, i): n = b[i] if n < 0xfd: return n, i + 1 size = {0xfd: 2, 0xfe: 4, 0xff: 8}[n] return int.from_bytes(b[i + 1:i + 1 + size], "little"), i + 1 + size def coinbase_outputs(block): i = 80 # skip the block header _, i = varint(block, i) # transaction count i += 4 # coinbase version if block[i] == 0 and block[i + 1] == 1: # segwit marker and flag i += 2 n_in, i = varint(block, i) for _ in range(n_in): i += 36 # previous outpoint size, i = varint(block, i) i += size + 4 # scriptSig and sequence n_out, i = varint(block, i) for _ in range(n_out): i += 8 # value size, i = varint(block, i) yield block[i:i + size] i += size def pushed_data(script): """Concatenate the pushes after OP_RETURN (the feed uses one push).""" i, data = 1, b"" while i < len(script): op = script[i] if op <= 75: size, i = op, i + 1 elif op == 76: size, i = script[i + 1], i + 2 elif op == 77: size, i = int.from_bytes(script[i + 1:i + 3], "little"), i + 3 elif op == 78: size, i = int.from_bytes(script[i + 1:i + 5], "little"), i + 5 else: return b"" data += script[i:i + size] i += size return data url, height = sys.argv[1], int(sys.argv[2]) block = bytes.fromhex(rpc(url, "getblock", [rpc(url, "getblockhash", [height]), 0])) payload = None for script in coinbase_outputs(block): if script[:1] == b"\x6a": data = pushed_data(script) if data.startswith(b"ASP0"): payload = data[4:] # the last match wins if payload is None: sys.exit(f"block {height}: no ASP0 output in the coinbase") sys.stdout.buffer.write(payload) ```
```bash python3 traces_payload.py https://mainnet.subfrost.io/v4//traces 880004 > block.bin protoc -I ~/alkanes-rs/crates/alkanes-support/proto \ --decode=alkanes.AlkanesBlockTraceEvent alkanes.proto < block.bin ``` Block 880,004 holds a single event: the trace shown in Path 1 (without `fuel_used`), followed by where it happened. ```text outpoint { txid: "Mt\021\244\350\266X\255\310\335\255\0148zg\201`\345\340\004\302\350hf\205E\342\2213C\213\017" vout: 4 } txindex: 2105 } ``` **Index it with your own program.** A metashrew program is a Rust crate compiled to wasm32. It exports `_start`, which runs once per block with the height and the raw block, and any number of view functions. This one stores, for every block, how many protostones ran and how many reverted:
Cargo.toml and src/lib.rs ```toml [package] name = "trace-count" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] metashrew-core = { git = "https://github.com/kungfuflex/metashrew", tag = "v9.0.5-rc.14" } metashrew-support = { git = "https://github.com/kungfuflex/metashrew", tag = "v9.0.5-rc.14" } bitcoin = "0.32" prost = "0.12" ``` ```rust //! A metashrew program that reads the /traces feed and stores, per block, //! how many protostones ran and how many of them reverted. use bitcoin::opcodes::all::OP_RETURN; use bitcoin::script::Instruction; use bitcoin::Block; use metashrew_core::{flush, get, input, set}; use metashrew_support::compat::export_bytes; use metashrew_support::utils::{consensus_decode, consume_sized_int, consume_to_end}; use prost::Message; use std::io::Cursor; use std::sync::Arc; // The part of alkanes.proto this program reads (same field numbers). #[derive(Clone, PartialEq, Message)] struct AlkanesBlockTraceEvent { #[prost(message, repeated, tag = "1")] events: Vec, } #[derive(Clone, PartialEq, Message)] struct AlkanesBlockEvent { #[prost(message, optional, tag = "1")] traces: Option, } #[derive(Clone, PartialEq, Message)] struct AlkanesTrace { #[prost(message, repeated, tag = "1")] events: Vec, } #[derive(Clone, PartialEq, Message)] struct AlkanesTraceEvent { #[prost(message, optional, tag = "2")] exit_context: Option, } #[derive(Clone, PartialEq, Message)] struct AlkanesExitContext { #[prost(int32, tag = "1")] status: i32, // 0 = SUCCESS, 1 = FAILURE } /// The feed appends one coinbase output: OP_RETURN, then "ASP0" + protobuf. fn trace_payload(block: &Block) -> Option> { let coinbase = block.txdata.first()?; let mut found = None; for out in &coinbase.output { let mut ops = out.script_pubkey.instructions(); if !matches!(ops.next(), Some(Ok(Instruction::Op(op))) if op == OP_RETURN) { continue; } let mut data = Vec::new(); for op in ops { if let Ok(Instruction::PushBytes(bytes)) = op { data.extend_from_slice(bytes.as_bytes()); } } if let Some(payload) = data.strip_prefix(b"ASP0") { found = Some(payload.to_vec()); // the last match wins } } found } #[no_mangle] pub fn _start() { let mut data = Cursor::new(input()); let height = consume_sized_int::(&mut data).unwrap(); let block: Block = consensus_decode(&mut Cursor::new(consume_to_end(&mut data).unwrap())).unwrap(); let event = trace_payload(&block) .map(|payload| AlkanesBlockTraceEvent::decode(payload.as_slice()).unwrap()) .unwrap_or_default(); // A trace ends with the exit of the outermost call: its status is the outcome. let reverted = |e: &AlkanesBlockEvent| { let last = e.traces.as_ref().and_then(|t| t.events.last()); last.and_then(|ev| ev.exit_context.as_ref()).map_or(false, |x| x.status == 1) }; let traces = event.events.len() as u32; let reverts = event.events.iter().filter(|e| reverted(e)).count() as u32; let mut value = traces.to_le_bytes().to_vec(); value.extend_from_slice(&reverts.to_le_bytes()); set(Arc::new(format!("/count/{height}").into_bytes()), Arc::new(value)); flush(); } /// View: input is a block height (u32, little-endian). Returns 8 bytes: /// traces then reverts, both u32 little-endian. #[no_mangle] pub fn count() -> i32 { let mut data = Cursor::new(input()); let _tip = consume_sized_int::(&mut data).unwrap(); // prepended by the runtime let height = consume_sized_int::(&mut data).unwrap(); export_bytes(get(Arc::new(format!("/count/{height}").into_bytes())).as_ref().clone()) } ```
Save the two files as `trace-count/Cargo.toml` and `trace-count/src/lib.rs`, then build the program and run it against the feed, from any height. Stable Rust needs the wasm32 target added once, and on macOS the same `CC_wasm32_unknown_unknown` and `AR_wasm32_unknown_unknown` as above. On a laptop, add `--view-memory-floor-mb 0` (see the memory note in Path 1): ```bash rustup target add wasm32-unknown-unknown cd trace-count cargo build --release --target wasm32-unknown-unknown ~/metashrew/target/release/rockshrew-mono \ --daemon-rpc-url https://mainnet.subfrost.io/v4//traces \ --indexer target/wasm32-unknown-unknown/release/trace_count.wasm \ --db-path ./db \ --start-block 969330 \ --port 8081 ``` Then ask for block 969,334. The view input is the height as a little-endian `u32` (`0x76ca0e00`), and the runtime prepends the current height to every view input, which is why `count` reads one `u32` before the one it was asked for: ```bash curl -s -X POST http://127.0.0.1:8081 -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"metashrew_view","params":["count","0x76ca0e00","latest"]}' ``` ```text {"id":1,"jsonrpc":"2.0","result":"0x440a000000000000"} ``` That is two little-endian `u32` values: `0x0a44` = 2,628 protostones ran, and 0 reverted. They came from 2,588 transactions: 40 of them carried two protostones. #### Path 3: decode the message in the transaction Without executing anything, a raw transaction still tells you what it asked for. An Alkanes message is packed inside a [Runes Runestone](https://docs.ordinals.com/runes/specification.html): 1. **Find the Runestone.** It is the first output whose script starts with `OP_RETURN OP_13` (`6a5d`). The payload is a list of LEB128 integers in tag/value pairs, as the Runes spec defines. 2. **Read the Protocol field, tag `16383`** (`ff7f` in LEB128). This is where protostones live; their packing is specified in the [protorune Protocol Messages](https://github.com/kungfuflex/protorune/wiki/Protocol-Messages) page. A protostone with `protocol_tag = 1` is Alkanes. 3. **Unpack the cellpack**, a list of LEB128 integers `[block, tx, opcode, ...inputs]`: the target contract, the method, then its arguments. See [Alkanes Metaprotocol](../protocol/alkanes). The open-source pipeline behind [subfrost.io/metrics](https://subfrost.io/metrics) does this at scale: | Repository | What it does | |---|---| | [`alkanes-opreturn-decoder`](https://github.com/Vdto88/alkanes-opreturn-decoder) | Decodes one transaction from its raw hex, offline: Runestone, protostones, and the Alkanes cellpack. Ported from `alkanes-rs` and tested against it. | | [`alkanes-opreturn-scanner`](https://github.com/Vdto88/alkanes-opreturn-scanner) | Runs the decoder over whole blocks fetched from an Esplora API and writes one row per day. | | [`alkanes-opreturn-indexer`](https://github.com/Vdto88/alkanes-opreturn-indexer) | A metashrew program that classifies every block natively and also measures block weight. It reads plain blocks and runs no contracts. | | [`alkanes-opreturn-stats`](https://github.com/Vdto88/alkanes-opreturn-stats) | The published daily dataset, [`history.csv`](https://vdto88.github.io/alkanes-opreturn-stats/history.csv). | Here is the first DIESEL mint again, this time as a message. It is a call to contract `2:0` with opcode `77`: ```bash git clone https://github.com/Vdto88/alkanes-opreturn-decoder cd alkanes-opreturn-decoder && npm install npx tsx src/cli.ts 0f8b433391e245856668e8c204e0e56081677a380cadddc8ad58b6e8a411744d ``` ```text OP_RETURN (vout 1): 6a5d0eff7f818cec82d08bc0a88281d215 ── Protostone #0 (Alkanes) ── protocol_tag: 1 pointer: 0 refund: 0 edicts: (none) cellpack: target 2:0, opcode 77, inputs [] summary: Alkanes call to 2:0, opcode 77. ``` Given a txid, the decoder fetches the raw transaction from mempool.space. Pass `--rawtx ` instead to decode with no network access at all. The scanner applies these rules to every transaction: - **OP_RETURN**: any output whose script starts with `6a`. The coinbase counts; its witness commitment is an `OP_RETURN` that is never Alkanes. - **Alkanes**: a Runestone output holds at least one protostone with `protocol_tag = 1`. A call that later reverts still counts: it was mined and it carried an Alkanes message. - **DIESEL mint**: an Alkanes cellpack targeting `2:0` with opcode `77`. - **Bytes**: the full script size of the `OP_RETURN` outputs. - **Day**: each block is dated by its Esplora `mediantime`, in UTC. On block 969,334 the scanner counts 2,589 Alkanes transactions, one more than the 2,588 that have traces. A block that still fails to fetch after retries is skipped with a warning on stderr and the exit code stays 0, so check the `Coverage` line of the report before trusting a total. The repository READMEs cover the flags and the daily jobs. ### What the data means #### The message and the outcome A raw transaction tells you what it **asked for**: which contract, which method, which arguments, which tokens to move. It does **not** tell you what **happened**: whether the call reverted, how much was minted, the resulting balances, or the id of a newly created token. Those exist only after the contracts run, in block order, from block 880,000. | You want | Where it comes from | |---|---| | Alkanes transaction counts, contracts and methods called, `OP_RETURN` bytes, block weight | The message. [Path 3](#path-3-decode-the-message-in-the-transaction). | | Fees | The values of the spent outputs. Esplora reports each transaction's `fee`; a block's total is its coinbase minus the subsidy. | | Success or revert, tokens moved, new token ids | Execution. The traces from [Path 1](#path-1-run-the-alkanes-indexer) or [Path 2](#path-2-index-from-the-traces-feed). | | Balances, contract storage, anything a view function answers | Execution state. [Path 1](#path-1-run-the-alkanes-indexer). | #### Reading a trace The hosted gateway serves the same trace as readable JSON through `alkanes_trace`. Note that it takes the txid in internal byte order (reversed): with the displayed txid it returns an empty list. ```bash curl -s -X POST https://mainnet.subfrost.io/v4/ -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"alkanes_trace","params":[{"txid":"4d7411a4e8b658adc8ddad0c387a678160e5e004c2e868668545e29133438b0f","vout":4}]}' ``` The result for the first DIESEL mint, reformatted: ```json [ { "event": "invoke", "data": { "type": "call", "context": { "myself": { "block": "2", "tx": "0" }, "caller": { "block": "0", "tx": "0" }, "inputs": ["77", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], "incomingAlkanes": [], "vout": 4 }, "fuel": 100000000 } }, { "event": "return", "data": { "status": "success", "response": { "alkanes": [{ "id": { "block": "2", "tx": "0" }, "value": "312500000" }], "storage": [ { "key": "/seen/846d0d0000000000", "value": "0x01000000" }, { "key": "/totalsupply", "value": "0x201fff9e042800000000000000000000" } ], "data": "0x" } } } ] ``` | Field | Here | Meaning | |---|---|---| | `invoke` | first event | A call starts. When a contract calls another, a nested `invoke` / `return` pair appears between them. | | `type` | `call` | How it was called: `call`, `delegatecall` or `staticcall`. | | `myself` | `2:0` | The contract that runs: DIESEL. | | `caller` | `0:0` | Who called it. The top-level call of a protostone has caller `0:0`; a call made by another contract names that contract. | | `inputs` | `77`, then zeros | The opcode and its arguments. 77 is DIESEL's mint; the cellpack has no arguments (the decoder shows `inputs []`). | | `incomingAlkanes` | none | Alkanes sent into the call. | | `vout` | `4` | The protostone's virtual vout. | | `fuel` | `100000000` | The compute budget given to the call. | | `return` | last event | The call ends. The last `return` is the outcome of the whole protostone. | | `status` | `success` | `success` or `revert`. | | `response.alkanes` | 312,500,000 of `2:0` | Alkanes the call hands back: here, the newly minted DIESEL, in base units. | | `response.storage` | two keys | Storage the contract wrote. `/seen/` plus the height as a little-endian `u64` (`846d0d…` = 880,004), and `/totalsupply` as a little-endian `u128`: 44,000,312,500,000, the supply after this mint. | | `response.data` | `0x` | Bytes returned to the caller. Empty here. | When a call fails, `status` is `revert` and `data` carries the error: `0x08c379a0` followed by a UTF-8 message. A reverted call in block 969,340, for example, returns `ALKANES: revert: Error: slippage: tokens below minimum`. A deployment adds a create event (`create_alkane` in the protobuf) carrying the new alkane's id. In the protobuf form from Path 1 and Path 2 the same fields appear under their `alkanes.proto` names (`enter_context`, `exit_context`, `incoming_alkanes`). Values of zero are left out, so `tx {}` means `tx = 0`, and an exit with no `status` line succeeded. Each 128-bit number is a `uint128 { lo, hi }` pair. #### AlkaneIds Every contract and token is addressed by an AlkaneId, `block:tx`: `2:0` is DIESEL, `32:0` is frBTC. A new contract gets its id when its deployment executes, so the id of a newly created token comes from execution (the create event in its trace, or a view function), not from the message that deployed it. #### The daily dataset Every Alkanes figure on `/metrics` comes from block data, not from execution; the only off-chain input is the BTC price. [`history.csv`](https://vdto88.github.io/alkanes-opreturn-stats/history.csv) has one row per UTC day since 2025-01-20 (block 880,000) and is updated once a day. Parse it by column name: new columns are only ever appended. **Where the rows come from.** Days from 2025-01-20 to 2026-06-26 were backfilled from the indexer, through its exporter. From 2026-06-27 on, the daily scanner writes the count, byte and fee columns, and the indexer adds its six. | Columns | Source | Meaning | |---|---|---| | `date`, `fromHeight`, `toHeight`, `blocksScanned` | scanner (indexer before 2026-06-27) | The UTC day and the blocks it covers. | | `totalTx`, `txWithOpReturn`, `txAlkanes`, `dieselMints` | scanner (indexer before 2026-06-27) | Raw transaction counts for the day. | | `opReturnBytes`, `runestoneBytes`, `alkanesBytes` | scanner (indexer before 2026-06-27) | Script bytes of `OP_RETURN` outputs, of Runestones, and of Alkanes Runestones. | | `feeTotalSats`, `feeAlkanesSats`, `feeOpReturnSats` | scanner (indexer before 2026-06-27) | Fees paid by all, Alkanes, and `OP_RETURN` transactions. | | `btcUsd` | CoinGecko | BTC price for the day. | | `weightTotal`, `weightAlkanes` | indexer | Block weight (WU), total and Alkanes only. | | `ugMints`, `dieselUg` | indexer | UNCOMMON•GOODS mints, and the DIESEL mints that also carry one. | | `txAlkRunestone`, `txPureRunes` | indexer | Alkanes and non-Alkanes Runestone transactions, **scaled to a 144-block day** (`count / blocks × 144`, rounded). | Things to know before aggregating: - **The latest row can be a partial day.** When `date` is today (UTC), it covers only the blocks mined so far. - **Backfilled rows follow the indexer's rules.** They leave the coinbase out of `txWithOpReturn` and `opReturnBytes`, date blocks by header time, read Runestones with `alkanes-rs`'s `Runestone::decipher`, and count all the `OP_RETURN` bytes of an Alkanes transaction as `alkanesBytes` (the scanner counts only its `6a5d` outputs). Their `feeAlkanesSats` and `feeOpReturnSats` are estimates: a sampled fee share rescaled to the day's fee total, and 0 on a few early days that had no sample. - **The two column groups can cover slightly different blocks.** The scanner dates blocks by median time and the indexer by header time, so around midnight a block can fall on different days. - **The scaled columns are estimates.** Use `txAlkanes` for raw counts. Indexer cells are left empty (not `0`) when there is no value, so parse them as optional. - **Coverage is not perfect.** Row boundaries skip about 330 blocks out of roughly 89,000 (under 0.4%), so full-history totals are a slight undercount. A day whose full rescan failed keeps its sampled row, with `blocksScanned` below `toHeight - fromHeight + 1`. Where the two tools run independently (2026-06-27 to 2026-09-29, 95 days), the scanner's Alkanes count per block is within 0.5% of the indexer's, with a median daily difference of 0.02%. Total Alkanes transactions from the dataset: ```python import csv, io, urllib.request url = "https://vdto88.github.io/alkanes-opreturn-stats/history.csv" rows = list(csv.DictReader(io.StringIO(urllib.request.urlopen(url).read().decode()))) rows = [r for r in rows if r["date"] <= "2026-09-29"] # drop today's partial row print("Alkanes transactions:", sum(int(r["txAlkanes"]) for r in rows)) print("of which DIESEL mints:", sum(int(r["dieselMints"]) for r in rows)) ``` This gives **97,352,249** Alkanes transactions from activation through 2026-09-29, of which 92,898,799 were DIESEL mints. Move the cutoff date to count through any other day. ### Going further - [Indexing with metashrew](../build/indexing-with-metashrew): how the indexer and its view functions work. - [`metashrew_*` reference](../api-reference/json-rpc/metashrew) and [`alkanes_*` reference](../api-reference/json-rpc/alkanes): every view and its inputs. - [`kungfuflex/alkanes-rs`](https://github.com/kungfuflex/alkanes-rs): the canonical Alkanes implementation, including `alkanes.proto`. - [`kungfuflex/metashrew`](https://github.com/kungfuflex/metashrew): the indexer framework, with a minimal example program in `crates/metashrew-minimal`. --- ## frBTC on Alkanes frBTC is the native Bitcoin synthetic token on the Alkanes metaprotocol, pegged 1:1 to BTC. This guide covers wrapping, unwrapping, and interacting with the frBTC contract using Alkanes. ### Contract Location | Property | Value | |----------|-------| | **Alkane ID** | `{32, 0}` | | **Block** | 32 | | **Transaction** | 0 | The frBTC contract is deployed at genesis and is immutable. ### Contract Opcodes The frBTC contract exposes these methods via opcodes: #### Core Operations | Opcode | Method | Parameters | Description | |--------|--------|------------|-------------| | `77` | `exchange()` | None | Wrap BTC to frBTC | | `78` | `unwrap()` | `vout: u128, amount: u128` | Burn frBTC and queue BTC payment | #### Query Methods | Opcode | Method | Returns | Description | |--------|--------|---------|-------------| | `99` | `get_name()` | `"Fractional BTC"` | Token name | | `100` | `get_symbol()` | `"frBTC"` | Token symbol | | `101` | `get_pending_unwraps()` | `Payment[]` | List pending payments | | `102` | `get_decimals()` | `8` | Decimal places | | `103` | `get_signer()` | `[u8; 32]` | Current signer pubkey | | `105` | `get_total_supply()` | `u128` | Total frBTC supply | ### How Wrapping Works Wrapping converts native BTC to frBTC in a single atomic Bitcoin transaction: ``` ┌─────────────────────────────────────────────────────────┐ │ WRAP TRANSACTION │ ├─────────────────────────────────────────────────────────┤ │ Input: Your BTC UTXO │ ├─────────────────────────────────────────────────────────┤ │ Output 0: BTC → SUBFROST signer address │ │ Output 1: frBTC → Your address (via protostone) │ │ Output 2+: Change → Your change address │ ├─────────────────────────────────────────────────────────┤ │ OP_RETURN: Protostone calling exchange() opcode 77 │ └─────────────────────────────────────────────────────────┘ ``` The frBTC contract at `{32, 0}` validates that BTC was sent to the correct signer address and mints an equivalent amount of frBTC to your address. #### Wrap Using CLI ```bash alkanes-cli alkanes wrap-btc \ --to \ --from \ --change \ --fee-rate \ -y ``` `` is an integer amount in satoshis, not BTC. **Example: Wrap 0.01 BTC (1000000 sats)** ```bash alkanes-cli alkanes wrap-btc 1000000 \ --to bc1p... \ --from bc1q... \ --change bc1q... \ --fee-rate 10 \ -y ``` #### Advanced: Execute with Protostone For custom transactions: ```bash alkanes-cli alkanes execute "[32,0,77]:v1:v1" \ --inputs "B:1000000" \ --to bc1p... \ -y ``` ### How Unwrapping Works Unwrapping converts frBTC back to native BTC in a two-step process: #### Step 1: Burn frBTC Create a transaction that burns your frBTC and queues a payment request: ``` ┌─────────────────────────────────────────────────────────┐ │ UNWRAP TRANSACTION │ ├─────────────────────────────────────────────────────────┤ │ Input: Your frBTC UTXO │ ├─────────────────────────────────────────────────────────┤ │ OP_RETURN: Protostone calling unwrap() opcode 78 │ │ with vout and amount parameters │ └─────────────────────────────────────────────────────────┘ ``` #### Step 2: Receive BTC The SUBFROST signers monitor for unwrap requests and cooperatively sign a Bitcoin transaction to release the locked BTC to your specified address. #### Minimum Unwrap Amount Due to dust limits and transaction fees, there's a minimum unwrap amount: ```bash alkanes-cli subfrost minimum-unwrap \ --fee-rate \ --premium 0.001 ``` The formula considers: - Dust threshold (546 sats) - Fee per output based on transaction size - Protocol premium (default 0.1%) ### Transaction Structure #### Protostone Format Wrap transactions include a protostone in the OP_RETURN: ``` Protostone { cellpack: { target: {block: 32, tx: 0}, // frBTC contract inputs: [77] // exchange() opcode }, bitcoin_transfer: { amount: , target: Output(0) // Send BTC to output 0 }, pointer: Output(1), // frBTC goes to output 1 refund: Output(1) // Unused tokens back to output 1 } ``` #### Payment Storage Unwrap payments are stored by block height: ``` /alkanes/{32:0}/storage/ /payments/byheight/{height} // Payments created at block /fulfilled // Fulfilled payment tracking /last_block // Last processed block ``` ### Checking Balances #### frBTC Balance ```bash alkanes-cli alkanes getbalance ``` #### Pending Unwraps View pending unwrap requests: ```bash alkanes-cli alkanes unwrap --block-tag latest ``` ### Security Model #### Atomic Wrapping - Wrapping is trustless and permissionless - BTC lock and frBTC mint happen in the same transaction - If the BTC transfer fails, no frBTC is minted #### Trust-Minimized Unwrapping - Unwrapping requires SUBFROST signer cooperation - Signers monitor for unwrap events - Multi-party threshold signature required for BTC release - Users can verify pending payments on-chain ### Error Handling **"Insufficient funds"** - Ensure you have enough BTC for the wrap amount plus transaction fees - Check that your UTXOs aren't locked or already spent **"Below minimum unwrap"** - Your unwrap amount is below the dust threshold plus fees - Use `minimum-unwrap` command to check the current minimum **"Signer address mismatch"** - The BTC was sent to the wrong address - Query the current signer address before wrapping ### Complete Example Here's a complete wrap flow: ```bash # 1. Create wallet if needed alkanes-cli wallet create # 2. Check your addresses alkanes-cli wallet addresses p2tr:0-3 # 3. Fund your wallet with BTC (external step) # 4. Check balance alkanes-cli wallet balance # 5. Wrap 0.01 BTC (amount in satoshis: 1000000) alkanes-cli alkanes wrap-btc 1000000 \ --to bc1p... \ --from bc1q... \ --change bc1q... \ --fee-rate 10 \ -y # 6. Verify frBTC balance alkanes-cli alkanes getbalance bc1p... ``` ### Key Parameters | Parameter | Value | Description | |-----------|-------|-------------| | frBTC Contract | `{32, 0}` | Block 32, Transaction 0 | | Wrap Opcode | `77` | exchange() function | | Unwrap Opcode | `78` | unwrap() function | | Default Premium | `0.1%` | Protocol fee on unwrap | | Dust Threshold | `546 sats` | Minimum output value | ### See Also - [Alkanes Integration](./alkanes-integration) — CLI reference for Alkanes - [Alkanes Metaprotocol](../key-components/alkanes) — Protocol overview --- ## Alkanes Metaprotocol(Key-components) The Alkanes Metaprotocol is a powerful system for creating stateful, smart contract-like applications on top of the Bitcoin blockchain. It is the engine that powers synthetic assets like `frBTC` and enables complex logic to be executed in a way that is verifiable by the underlying SUBFROST consensus. ### Runestones Alkanes builds upon the concept of Runes, embedding protocol messages inside the `OP_RETURN` output of a Bitcoin transaction. This embedded data is called a **Runestone**. A Runestone is a structured piece of data that instructs the Alkanes metaprotocol on how to alter the state of its "on-chain" programs. Because this data is part of a valid Bitcoin transaction, **it inherits the security and immutability of the Bitcoin blockchain.** The SUBFROST protocol uses a `metashrew` indexer to read these `OP_RETURN` outputs, interpret the Runestones, and apply the resulting state changes to the Alkanes state database. This mechanism allows for a flexible and expressive way to manage digital assets and execute complex logic, all while **anchored to the security of the Bitcoin blockchain.** ### Structure of a Runestone The Runestone token operation has several key fields: - `protocolTag`: An identifier for the specific metaprotocol (e.g., `1n` for the base protorune protocol). - `edicts`: An array of instructions that modify rune balances. - `id`: The ID of the rune being affected (e.g., `{ block: 32n, tx: 0n }` for `frBTC`). - `amount`: The quantity of the rune to be minted or transferred. - `output`: The index of the transaction output that will receive the runes. - `pointer`: A default output index to assign any unassigned runes. - `calldata`: An arbitrary byte array that can be used to pass data to the Alkane contract, effectively calling a function. #### Example: Minting `frBTC` (Wrapping) When a user wraps BTC, they create a transaction with a Runestone that looks something like this: ```json { "protostones": [{ "protocolTag": 1n, "edicts": [{ "id": { "block": 32n, "tx": 0n }, // frBTC Alkane ID "amount": 100000000n, // 1.0 frBTC (in satoshis) "output": 2 // Assign to the 3rd output }], "pointer": 1, "calldata": [32n, 0n, 77n] // Call the 'exchange' function }] } ``` This Runestone instructs the protocol to: 1. Identify the `frBTC` Alkane contract. 2. Mint `1.0` new `frBTC`. 3. Assign ownership of this new `frBTC` to the recipient of the transaction's third output. 4. Pass `[32, 0, 77]` as calldata to the contract's logic (target block 32, tx 0, opcode 77). #### Example: Burning `frBTC` (Unwrapping) To unwrap, a user spends the UTXO containing `frBTC`. The Runestone in this transaction will have an empty `edicts` array, which signals a burn. The `calldata` calls `unwrap` (opcode 78) with two parameters: `vout`, the index of the transaction output that identifies where the released BTC goes, and `amount`, the quantity of `frBTC` to unwrap in satoshis. ```json { "protostones": [{ "protocolTag": 1n, "edicts": [], // An empty array signifies a burn of input runes "pointer": 0, "calldata": [32n, 0n, 78n, 0n, 50000000n] // Call 'unwrap': vout of the destination output, amount in sats }] } ``` ### See Also - [Alkanes Integration](../developer-guide/alkanes-integration) — CLI commands for Alkanes - [frBTC - Alkanes](../developer-guide/wrapping-frBTC) — Wrapping and unwrapping frBTC --- ## BRC2.0 Protocol ## BRC2.0 Protocol (brc20-prog) BRC2.0 (also known as brc20-prog) is a protocol for deploying and executing EVM-compatible smart contracts directly on Bitcoin via inscriptions. It enables Solidity developers to bring their skills to Bitcoin without learning a new language. ### How BRC2.0 Works BRC2.0 inscribes contract deployments and function calls as JSON data within Bitcoin transactions. An indexer reads these inscriptions and executes the embedded EVM bytecode, maintaining state across transactions. #### The Inscription Format BRC2.0 uses the standard ord inscription envelope: ``` CHECKSIG OP_FALSE OP_IF "ord" "text/plain;charset=utf-8" OP_ENDIF ``` The critical security feature is the ` CHECKSIG` at the start—this ensures only the holder of the ephemeral private key can create a valid reveal transaction, preventing frontrunning attacks. #### Payload Types **Deploy Operation:** ```json { "p": "brc20-prog", "op": "deploy", "d": "0x608060405234801561001057600080fd5b50..." } ``` The `d` field contains the full EVM deployment bytecode (compiled Solidity). **Call Operation:** ```json { "p": "brc20-prog", "op": "call", "c": "0x1234567890abcdef1234567890abcdef12345678", "d": "0xa9059cbb000000000000000000000000..." } ``` - `c`: The target contract address - `d`: ABI-encoded function call data ### The Commit-Reveal-Activation Pattern BRC2.0 uses a multi-transaction pattern to prevent frontrunning and ensure atomic execution: #### Two-Transaction Pattern (for calls) ``` Transaction 1: COMMIT ├─ Input: User's UTXO └─ Output: Exact sats to taproot address Transaction 2: REVEAL ├─ Input: Commit output (requires valid signature) ├─ Script: Pubkey+CHECKSIG + inscription envelope └─ Output: OP_RETURN (completes the call) ``` #### Three-Transaction Pattern (for deployments) ``` Transaction 1: COMMIT ├─ Input: User's UTXO └─ Output: Exact sats to taproot address Transaction 2: REVEAL ├─ Input: Commit output ├─ Script: Inscription envelope with bytecode └─ Output: 546-sat inscription UTXO Transaction 3: ACTIVATION ├─ Input: 546-sat inscription UTXO └─ Output: OP_RETURN (activates the contract) ``` The three-transaction pattern creates a persistent inscription UTXO that can be tracked, then "activates" it to deploy the contract. ### Anti-Frontrunning Mechanisms #### The Frontrunning Problem Without protection, an attacker could: 1. See a commit transaction in the mempool 2. Construct their own reveal before the original creator 3. Get their reveal mined first, hijacking the inscription #### The Solution: Presign + Atomic Broadcast BRC2.0 implements a hybrid defense: 1. **Pre-build all transactions** before any are broadcast 2. **Pre-sign all transactions** with RBF-enabled sequences 3. **Broadcast atomically** in a single RPC batch call 4. The reveal is already signed when commit hits mempool—attackers have no window The `Pubkey+CHECKSIG` in the reveal script means only the commit creator's signature is valid, making competing reveals impossible. ### Contract Address Derivation BRC2.0 uses Ethereum's contract address formula: ``` address = keccak256(rlp([sender, nonce]))[12:] ``` Where: - `sender`: The deployer's Ethereum-style address (derived from Bitcoin pubkey) - `nonce`: The deployer's transaction count (0 for first deployment) This allows predicting contract addresses before deployment—useful for setting up cross-contract references. ### ABI Encoding Function calls use standard Ethereum ABI encoding: ``` ┌─────────────────────────────────────────────────────────┐ │ Function Selector (4 bytes) │ │ keccak256("functionName(type1,type2)")[:4] │ ├─────────────────────────────────────────────────────────┤ │ Argument 1 (32 bytes, left-padded) │ ├─────────────────────────────────────────────────────────┤ │ Argument 2 (32 bytes, left-padded) │ ├─────────────────────────────────────────────────────────┤ │ ... │ └─────────────────────────────────────────────────────────┘ ``` **Example:** `transfer(address,uint256)` with args `(0x1234..., 1000)` ``` Selector: 0xa9059cbb (keccak256("transfer(address,uint256)")[:4]) Arg 1: 0x0000000000000000000000001234567890abcdef... Arg 2: 0x00000000000000000000000000000000000000000000000000000000000003e8 ``` ### Precise Fee Calculation BRC2.0 uses the "dummy transaction technique" for exact fee calculation: 1. Build a complete reveal transaction with real script and control block 2. Calculate exact virtual size (vsize) from the dummy 3. Commit output = inscription_output (546 sats) + (reveal_vsize × fee_rate) This precision is critical—too little causes reveal failure, too much wastes sats. ### Inscription Protection When funding a BRC2.0 operation, existing inscriptions on UTXOs must be protected. The protocol automatically inserts a **split transaction**: ``` UTXO containing inscription │ ▼ ┌─────────────────────┐ │ SPLIT TX │ ├─────────────────────┤ │ Output 1: Clean │ ──→ Used for commit funding │ Output 2: Protected │ ──→ Keeps inscription safe └─────────────────────┘ ``` The mempool indexer traces inscription state through unconfirmed transactions, allowing safe splitting. ### See Also - [BRC2.0 Integration](../developer-guide/brc20-prog) — How to deploy and call contracts - [Alkanes Metaprotocol](./alkanes) — Token-focused operations --- ## FROST & ROAST Cryptography for Signing **Flexible Round-Optimized Schnorr Threshold (FROST)** and **Robust Asynchronous Schnorr Threshold (ROAST)** signatures are the cryptographic cornerstones of the SUBFROST protocol. They are threshold signature schemes that allow a large, dynamic set of signers to collectively create a single Schnorr signature without any single signer ever possessing the full private key. SUBFROST uses the `frost-core` and `frost-secp256k1-tr` crates for its implementation of FROST and ROAST. ### How it Secures the Peg The 1:1 peg between Bitcoin and `frBTC` is maintained by a large pool of signers. The BTC that collateralizes `frBTC` is held in a single Bitcoin address whose private key is managed by the entire signer set using the FROST protocol. Please read [**here**](/tokens/frBTC-overview#frbtc-roadmap) to understand our two-phased approach to the signing group. #### Distributed Key Generation (DKG) Before the signing group can start creating signatures, they must first perform a **Distributed Key Generation (DKG)** ceremony. This is an interactive protocol where the participants collaboratively generate a single group public key, while each participant only holds a secret share of the corresponding private key. The DKG process ensures that the full private key is never constructed or held by any single participant, or even by a colluding subset of participants (as long as the threshold is not reached). This is a fundamental security property of the SUBFROST protocol. #### Signing with FROST & ROAST To spend the BTC held in the multisig address (i.e., to process an `frBTC` unwrap request), a threshold `t` of the total `n` signers must cooperate to create a signature. The signing process is optimized to be highly efficient and to minimize the number of communication rounds required. This is where ROAST comes in. ROAST is an optimization of FROST that allows for a non-interactive signing session, assuming a single round of pre-computation has already been performed. The signing process can be broken down into two phases: 1. **Pre-computation (Nonce Generation):** In this phase, each participant generates a set of public/private nonce pairs. The public nonces are then shared with the other participants. This phase can be done ahead of time, before a signature is actually needed. 2. **Signing (Online Phase):** When a signature is needed for a specific message (e.g., a Bitcoin transaction), each participant uses their secret share, the message, and one of their pre-computed nonces to create a **partial signature**. These partial signatures are then sent to a coordinator (which can be any of the participants) who aggregates them into a single, valid Schnorr signature. This two-phase process makes the online signing phase extremely fast and efficient, as it only requires a single round of communication. ### Key Advantages - **No Single Point of Failure:** The full private key never exists in one place, making it impossible for a single compromised signer (or a minority group) to steal the funds. - **Scalability:** The protocol supports a large and dynamic set of signers. New signers can join and old ones can leave through a process called a "reshare" ceremony. - **Efficiency:** ROAST's asynchronous design makes the signing process fast and efficient, which is crucial for a high-performance system like SUBFROST. - **Flexibility:** FROST is a flexible protocol that can be adapted to a variety of use cases, such as multisig wallets and their usage in complex decentralized applications like SUBFROST. This trust-minimized setup is a significant improvement over traditional federated or centralized peg mechanisms, providing a far higher degree of security and decentralization for the assets, such as BTC, locked on the SUBFROST protocol. ### The SUBFROST Implementation SUBFROST's implementation of FROST and ROAST is built on top of the `frost-core` and `frost-secp256k1-tr` crates. The `subfrost-core` crate provides the core logic for the SUBFROST protocol, and it uses these crates to implement the FROST and ROAST signing protocols. The `SUBFROSTEngine` is the main orchestrator of the signing process. It is responsible for: * **Managing the signer set:** The `SUBFROSTEngine` keeps track of the current set of signers and their public keys. * **Coordinating the DKG ceremony:** The `SUBFROSTEngine` initiates and coordinates the DKG ceremony when a new signer set is formed. * **Coordinating the signing process:** The `SUBFROSTEngine` coordinates the FROST and ROAST signing process when a new signature is required. The signing process is illustrated in the following diagram: ```mermaid sequenceDiagram participant Client participant SUBFROSTNode participant Signer1 participant Signer2 participant Signer3 Client->>SUBFROSTNode: Request to sign a message SUBFROSTNode->>Signer1: Request for partial signature SUBFROSTNode->>Signer2: Request for partial signature SUBFROSTNode->>Signer3: Request for partial signature Signer1-->>SUBFROSTNode: Partial signature Signer2-->>SUBFROSTNode: Partial signature Signer3-->>SUBFROSTNode: Partial signature SUBFROSTNode->>SUBFROSTNode: Aggregate partial signatures SUBFROSTNode-->>Client: Full signature ``` #### The `SUBFROSTEngine` The `SUBFROSTEngine` is the heart of the SUBFROST node. It is a generic struct that is parameterized by a `MetashrewProvider` and a `SUBFROSTWasmRunner`. This design allows the `SUBFROSTEngine` to be highly modular and testable. The `MetashrewProvider` trait provides an abstraction for the connection to a metashrew index. This allows the `SUBFROSTEngine` to be used with a live JSON-RPC endpoint in production and a direct in-memory adapter during tests. The `SUBFROSTWasmRunner` trait provides an abstraction for the execution of the consensus WASM. This allows for a real wasmtime-based runtime and a mock runtime. The `SUBFROSTEngine` implements the `SUBFROSTApiProvider` trait, which defines the `subfrost_getbundle` function. This is the primary entry point for clients who want to get a bundle of assets. Here is an example of how to instantiate and use the `SUBFROSTEngine`: ```rust use subfrost_core::engine::SUBFROSTEngine; use subfrost_core::traits::{MetashrewProvider, SUBFROSTWasmRunner}; use std::sync::Arc; // Create a mock MetashrewProvider and SUBFROSTWasmRunner let metashrew_provider = Arc::new(MockMetashrewProvider::new()); let wasm_runner = Arc::new(MockSUBFROSTWasmRunner::new()); // Create a new SUBFROSTEngine let engine = SUBFROSTEngine::new(metashrew_provider, wasm_runner); // Call the subfrost_getbundle function let result = engine.subfrost_getbundle("".to_string()).await; ``` --- ## Keystore Management In SUBFROST, a **keystore** is a file that securely stores all the sensitive cryptographic material for a user. This includes the user's master secret (their mnemonic phrase) and their shares of any FROST group keys they are a part of. ### The SUBFROST Keystore The SUBFROST keystore is designed to be both secure and portable. It is a JSON file that contains all the information needed to participate in FROST signing ceremonies and to manage your `frBTC` and other assets. Here are the key components of a SUBFROST keystore: * **Encrypted Mnemonic:** The user's mnemonic phrase (the master secret) is never stored in plaintext. Instead, it is encrypted with a user-provided passphrase and stored in the keystore. * **Public Key and Metadata:** The keystore contains the user's public key, the group public key for any FROST groups they are a part of, and other metadata such as the key derivation path. * **Encrypted FROST Shares:** For each FROST group the user is a part of, the keystore contains their encrypted secret share of the group's private key. ### Encryption The security of the SUBFROST keystore relies on a strong encryption scheme that protects the user's secrets from unauthorized access. When a user creates a new keystore, they are prompted to enter a passphrase. This passphrase is then used to encrypt the user's mnemonic phrase and their FROST shares using the following process: 1. **Key Derivation (PBKDF2):** The user's passphrase is not used directly as an encryption key. Instead, it is fed into the **PBKDF2 (Password-Based Key Derivation Function 2)** algorithm, along with a random salt. PBKDF2 is a computationally intensive algorithm that makes it very difficult for an attacker to guess the passphrase, even if they have access to the encrypted keystore. 2. **Authenticated Encryption (AES-GCM):** The key derived from PBKDF2 is then used to encrypt the user's secrets using **AES-256-GCM (Advanced Encryption Standard in Galois/Counter Mode)**. AES-GCM is a modern, authenticated encryption cipher that provides both confidentiality (the data is encrypted) and integrity (the data cannot be tampered with). This combination of PBKDF2 and AES-GCM provides a very high level of security for the user's secrets. ### ASCII Armoring To make the encrypted data easy to store and transport, it is **ASCII armored**. This means that the binary encrypted data is encoded into a human-readable text format. This allows the keystore to be easily copied, pasted, and stored in any text-based format, such as a JSON file. By combining strong encryption with a user-friendly file format, the SUBFROST keystore provides a secure and convenient way for users to manage their cryptographic keys and to participate in the SUBFROST ecosystem. ### Keystore Management in SUBFROST The `subfrost-cli` tool that shipped the keystore commands (`keystore new`, `import`, `export` and `list`) is retired and not available. There is no public command for managing signer keystores today. #### Example Keystore File Here is an example of a SUBFROST keystore file: ```json { "crypto": { "cipher": "aes-128-ctr", "cipherparams": { "iv": "83dbcc02d8ccb40e46619102d57e33be" }, "ciphertext": "d1728d7c6c34c235c893f87b8a0a82a3a2fb23c855884b6b9548547065a0463c", "kdf": "pbkdf2", "kdfparams": { "c": 262144, "dklen": 32, "prf": "hmac-sha256", "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c678585344e472434e37146434f" }, "mac": "517ead924a9d0dc3124507e3393d195ce33377b4737974ac46cc3b529526a7ea" }, "id": "3a1b7547-d4b4-4612-863f-34354f232a2a", "version": 3 } ``` --- ## Lending Protocol(Key-components) :::note Audience This is the **technical / developer deep-dive**: the on-chain contract, the PSBT structure, the SIGHASH scheme, and the settlement mechanics. If you just want to lend or borrow in the app, see [**Lending & Borrowing**](../using-subfrost/lending) for the user guide. ::: SUBFROST lending is a **peer-to-peer, fixed-term, over-collateralized loan market** for Alkanes tokens, settled in a **single Bitcoin transaction** via 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. It is built on the `lending-contract-psbt` Alkanes contract. Every loan is its own on-chain contract instance, so a loan's entire lifecycle — repayment, collateral release, default, liquidation — is enforced by code, not by a counterparty. --- ### Core ideas - **Off-chain order book, on-chain settlement.** Makers publish offers to a database. Posting **broadcasts a small "prep" transaction** (which carves the exact loan/collateral amount out of the maker's funding, so the leftover balance stays spendable), but the **settlement PSBT stays un-broadcast**. Nothing settles until a taker accepts — at which point a single settlement transaction is assembled and broadcast. - **One transaction = one loan.** The settlement tx 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 `ACTIVE` the moment it confirms. - **No bearer token, no admin.** A party's identity is simply the `scriptPubKey` they receive at in the settlement. Every later action (repay, claim) is authorized by spending to / 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 & terms A loan has two parties and a fixed set of terms: | Term | Meaning | |---|---| | **loan token / amount** | what the borrower receives (e.g. 1 DIESEL) | | **collateral token / amount** | what the borrower locks to back the loan (e.g. 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; the **taker** accepts it. Either side can be the maker: - **Lend offer** — maker is the **creditor** (brings the loan token). A taker borrows against it. - **Loan request** — maker is the **debitor** (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 offer book DB • 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 + per-loan clone A single `lending-contract-psbt` **template** is deployed at a block-4 slot (`LENDING_PSBT_TEMPLATE_ID`, e.g. `4:47876` on devnet). 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 + arm the loan from the agreed terms | | 1 | RepayLoan | debitor (permissionless tokens-in) | send the loan token back → `REPAID` | | 2 | ClaimRepayment | creditor | pull principal + interest | | 3 | ClaimCollateral | debitor | reclaim the collateral after repayment | | 4 | TriggerDefault | anyone (after deadline) | flip an expired `ACTIVE` loan → `DEFAULTED` | | 5 | ClaimDefaultedCollateral | creditor | seize the collateral after default | **Views:** `90 GetLoanDetails`, `91 GetRepaymentAmount`, `92 GetState`, `93 GetTimeRemaining`, `99 GetName`. #### Authorization model (claims) A claim (op 2/3/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/debitor scripts are recorded at `Initialize` from the settlement's receive outputs (each party's taproot address). 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 tx does everything. Its **outputs** are fixed: ``` vout 0 OP_RETURN ← the two protostones (Initialize + refund splitter) vout 1 3000 sats ← protocol fee (LENDING_PROTOCOL_FEE_SATS) vout 2 dust ← maker's receive output vout 3 dust ← taker's receive output ``` Which party is creditor vs debitor maps onto outputs 2/3 by the maker's role: - maker = **creditor** → `creditorOutput = 2` (maker), `debitorOutput = 3` (taker) - maker = **debitor** → `creditorOutput = 3` (taker), `debitorOutput = 2` (maker) #### The two protostones (the OP_RETURN at vout 0) - **p0 — 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 — refund splitter (edicts only).** On the revert path it returns the **loan token → creditor** and the **collateral → debitor** (amount `0` = "all"), making both parties whole if the clone+init fails. `Initialize` calldata argument order (all the terms, in this 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 tx without invalidating the maker's signatures: - **Maker inputs (0,1,2) → `SIGHASH_SINGLE | ANYONECANPAY` (0x83).** `SINGLE` makes each maker input commit to **only its own corresponding output** (so the maker pre-commits outputs 0,1,2 — the OP_RETURN, the protocol fee, and their own receive). `ANYONECANPAY` makes each input commit to **only itself**, not the other inputs — so the taker can freely append input 3 later. - **Taker input (3) → `SIGHASH_ALL` (0x01).** Signed last, it commits the **entire assembled tx** (all inputs + all outputs, including the taker's own output 3), locking it in place. The result: the maker publishes a partial transaction committing exactly their side; any taker can later add their input + output and finalize, and the maker's commitment still verifies. #### The protocol-fee output (why it exists) The maker's partial tx has **outputs worth more than its inputs** (its three dust inputs ≈ 1,638 sats, but it commits the 3,000-sat protocol fee + dust outputs). That negative balance makes the partial tx **impossible to broadcast on its own** — it stays inert until the taker adds an input that funds the deficit + the miner fee. This is deliberate: it prevents a published offer's PSBT from being broadcast early or out of context, and routes a small protocol fee to the treasury on every settlement. --- ### PSBT lifecycle in detail #### 1. Exact-amount UTXOs (the "prep") The contract requires the **incoming alkanes to equal the agreed amounts exactly** — 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 + 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 come under a single wallet approval, and the prep only goes out once that gate has passed, so a refused signature leaves nothing on chain. 2. Builds the **partial settlement PSBT** (3 maker inputs `0x83`, the 3 committed outputs, the OP_RETURN) and signs the maker's inputs. 3. POSTs the signed prep hex + the partial PSBT + the terms to the offer-book database. 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 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 offer book, which is why the taker path keeps the re-broadcast fallback below. ::: #### 3. Taker side — accepting (one broadcast) When a taker accepts: 1. Builds + signs their own **prep** (their token + a BTC fee budget) and broadcasts it. 2. **Appends** their input (input 3, `SIGHASH_ALL`) and receive output (vout 3) to the maker's partial PSBT, then signs input 3. The maker's `0x83` signatures survive untouched. 3. Broadcasts 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, not a package member. Before broadcasting, the taker 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). `computePackageSettlementFee` computes `max(ceil(feeRate × packageVsize) − prepsAlreadyPaid, ceil(feeRate × settlementVsize))`. `packageVsize` covers the taker prep plus the settlement; 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 + state. #### Repay and reclaim (borrower) — one transaction The borrower's "Repay and reclaim" combines **two** operations in a single tx 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 tx**, so `ClaimCollateral` (op 3) sees the just-`REPAID` state and releases the collateral. One click takes the loan `ACTIVE → REPAID → 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` (op 2) to pull the principal + 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 simply sits in the intermediate state and the UI offers the remaining step: - repay+reclaim where the reclaim reverted → loan at `REPAID` → borrower still sees **Claim Collateral**. - default+claim where the claim reverted → loan at `DEFAULTED` → 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 (not time-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 ``` Example — 1 DIESEL at 12.5% APR for 1008 blocks: `100_000_000 × 1250 × 1008 / (10_000 × 52_560) = 239,726` → repayment `1.00239726 DIESEL`. Because it's 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 `ACTIVE`). --- ### Off-chain offer book Offers live in **PostgreSQL**, with **complete physical isolation per network** — a separate database per network (`subfrost_devnet`, `subfrost_mainnet`, …), selected at request time. There is no shared offers table, so a devnet test loan can never appear on mainnet. (Identity/auth data stays on the shared default database; those are global, not per-network.) Each open offer row holds only the **off-chain matching data**: the terms, the maker's role + address, the signed **partial PSBT**, the signed **prep tx hex** kept as a re-broadcast fallback, and a status (`open → taken | cancelled`). Once a loan settles, **nothing about it is read from the DB** — the live loan lives entirely on-chain. #### Ownership of edits / 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). Cancelling/deleting is gated server-side: the request must carry the connected wallet's address and it must equal the offer's `makerAddress`. #### 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 — which is why the wallet treats them as locked for as long as the offer is open, while the prep's change outputs return 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; where the chain is only reachable in the browser (devnet) the client **hides** them. If the UTXO check is unavailable it does **not** prune — it surfaces a loading state rather than risk deleting a live offer. --- ### Loan portfolio (the registry) Active and historical loans are read **from the chain via the indexer (espo / quspo)**, not the DB: 1. `get_factory_children(LENDING_PSBT_TEMPLATE_ID)` enumerates every loan clone. 2. For each clone, `get_keys(/creditor_script, /debitor_script)` is matched against the connected wallet's receive `scriptPubKey` → that's the wallet's role. 3. Live terms + 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** (red) 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 & 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 signed with the BIP84 key. - **Browser wallets (Xverse, OYL, UniSat, …):** the same PSBT is signed via 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 don't break the taker's finalize. The alkanes (loan/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 and the devnet faucet keep clean BTC). --- ### Frontend The `/lend` page mirrors the swap UI: - **`/lend`** — a markets table of lendable tokens with average APY (same style as the markets tab). - **`/lend?lend=&collateral=`** — a pair view: an **order book** on the left (loan *requests* on top, loan *offers* on the bottom, sorted by APY like a sell/buy book) and a Lend/Borrow **form** on the right. - A bottom **My Loans** section with tabs: **Active Loans**, **Closed Loans**, **My Offers**. Taking an offer or running a lifecycle action opens a confirmation modal (loan summary, what you put in / get out, fee rate, and — for borrowers — a collateral-forfeiture warning). --- ### Network configuration Lending is enabled on a network only when its `LENDING_PSBT_TEMPLATE_ID` is configured (the deployed template's `[4:tx]` slot). Bringing lending to a new network requires: deploying the template on that chain and setting its slot in config, a per-network offer-book database, and an indexer that serves `get_factory_children`. --- ### Security properties — summary - **Atomic settlement.** The loan, the loan delivery, and the collateral lock all happen in one tx; if the clone+init reverts, the refund splitter returns both legs to their owners. - **No early broadcast.** The maker's partial PSBT can't be broadcast until a taker funds it (the protocol-fee output makes its outputs exceed its inputs). - **Tamper-evident escrow.** The maker's `SIGHASH_SINGLE|ANYONECANPAY` signatures commit exactly their side; the taker's `SIGHASH_ALL` locks the rest. Neither can alter the other's committed portion. - **No custodian / no admin keys.** Authorization is "spend to / 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. --- ## Schnorr Signatures Schnorr signatures are a type of digital signature scheme that is known for its simplicity, provable security, and linearity. They are a key building block of the SUBFROST protocol, and what makes FROST and ROAST signatures possible. ### What is a Digital Signature? A digital signature is a cryptographic mechanism used to verify the authenticity and integrity of a digital message or document. It is the digital equivalent of a handwritten signature or a stamped seal. A digital signature scheme typically consists of three algorithms: 1. **Key Generation:** An algorithm that generates a private key and a corresponding public key. 2. **Signing:** An algorithm that produces a signature for a message, using the private key. 3. **Verification:** An algorithm that verifies the signature for a message, using the public key. ### Advantages of Schnorr Signatures For many years, the dominant digital signature scheme used in Bitcoin and other cryptocurrencies was the Elliptic Curve Digital Signature Algorithm (ECDSA). However, Schnorr signatures have several advantages over ECDSA, which is why they were included in the Bitcoin Taproot upgrade and why they are used in SUBFROST. #### Linearity The most important advantage of Schnorr signatures is their **linearity**. This property allows multiple signatures from different signers to be aggregated into a single, valid signature. This is not possible with ECDSA. This linearity is what makes threshold signature schemes like FROST possible. In a FROST signing ceremony, multiple participants can each create a partial signature, and these partial signatures can then be combined to create a single, valid Schnorr signature for the group. #### Smaller Size Schnorr signatures are also smaller than ECDSA signatures. A Schnorr signature is typically 64 bytes, while an ECDSA signature can be up to 72 bytes. This may not seem like a big difference, but it can have a significant impact on the transaction fees and the overall scalability of a blockchain. #### Provable Security Schnorr signatures are provably secure under standard cryptographic assumptions. This means that there is a mathematical proof that the signature scheme is secure, as long as the underlying mathematical problems (like the discrete logarithm problem) are hard to solve. ### Conclusion Schnorr signatures are a powerful cryptographic tool that provides a number of advantages over older signature schemes. Their linearity, smaller size, and provable security make them an ideal choice for use in cryptocurrencies, other decentralized systems, and SUBFROST. ### Schnorr Signatures in SUBFROST SUBFROST uses Schnorr signatures extensively throughout the protocol. The `schnorrkel` crate is used to provide the core Schnorr signature functionality. #### `frBTC` (and the planned `dxBTC`) The `frBTC` token is secured by Schnorr signatures, and `dxBTC`, once launched, will be secured the same way. When a user wants to mint `frBTC` tokens, they must first lock up their BTC in a Bitcoin address that is controlled by the SUBFROST signer set. The private key for this address is a Schnorr key that is held by the signers. When a user wants to redeem their `frBTC` tokens for BTC, they must submit a request to the SUBFROST network. The SUBFROST signers will then use their Schnorr key to sign a Bitcoin transaction that sends the BTC to the user's address. #### The `schnorrkel` Crate The `schnorrkel` crate is a pure-Rust implementation of Schnorr signatures. It provides a simple and easy-to-use API for creating and verifying Schnorr signatures. The `subfrost-core` crate uses the `schnorrkel` crate to implement the FROST and ROAST signing protocols. The `schnorrkel` crate is also used to create the Schnorr signatures that are used to secure the `frBTC` token (and, once launched, the planned `dxBTC` token).