Skip to main content

Fractal Developer Guide

This page is for wallets and applications that want to move FB into frFB, or read the peg's state. If you only need the concepts, read the overview first.

The endpoint​

POST https://mainnet.subfrost.io/v4/{apikey}/fractal          JSON-RPC 2.0
methodwhat it returns
metashrew_heightprotofractal's indexed Fractal height
metashrew_viewan alkanes view against protofractal: simulate, getbytecode, protorunesbyoutpoint, …
fractal_height, getblockcountFractal chain tip
fractal_addressutxosan address's UTXOs
fractal_addressbalancean address's balance
fractal_feeestimates{slow, avg, fast} sat/vB
fractal_broadcast, sendrawtransactionbroadcast a raw transaction

Mempool fee rates are at /v4/{apikey}/fractal/mempool (mempool_feerates).

If you use alkanes-cli, point it at the endpoint and it works unchanged for views:

alkanes-cli -p mainnet --jsonrpc-url https://mainnet.subfrost.io/v4/{apikey}/fractal \
alkanes simulate 44:44:104

Reading a contract​

metashrew_view takes [view_name, hex_protobuf_request, "latest"]. To call a view opcode, the request is a MessageContextParcel holding only calldata (field 5). The calldata is the cellpack [block, tx, opcode, …inputs] as a list of LEB128 varints, not fixed-width words:

def varint(n):
out = bytearray()
while True:
b = n & 0x7f; n >>= 7
out.append(b | 0x80 if n else b)
if not n: return bytes(out)

def simulate_param(block, tx, inputs):
cd = b''.join(varint(v) for v in [block, tx] + inputs)
return '0x' + (b'\x2a' + varint(len(cd)) + cd).hex()

simulate_param(44, 44, [104]) # '0x2a032c2c68' -> fb-vault GetSignerScript

The response is a SimulateResponse: execution.data (field 1, then field 3) is the return value, and error (field 3) is a revert.

A revert is an HTTP 200

A reverted call does not produce a JSON-RPC error. The transport succeeds and the failure is a string in SimulateResponse.error. If you only check the JSON-RPC error, every revert reads as success with empty data.

fb-vault (44:44) views​

opcodenamereturns
103GetSignerthe group's x-only key, untweaked. Not a payable script.
104GetSignerScriptthe 34-byte P2TR script the vault credits. Read this, and pay this.
20GetHeldFB credited and not yet released, sats (u128 LE)
22GetNoncenext nonce for signed calls
23GetDepositsByHeight {height}deposit records at that Fractal height
21GetPaymentsByHeight {height}payment records queued at that height
24GetSigilHeld1 while FB-SIGIL is in the vault (now 0)
25GetIntent {txid0, txid1}the intent stored for a deposit txid
26GetBurnCursorhow far the group has worked through frFB's burn queue
27GetRedeemed {txid0, txid1, vout}the Fractal height a Bitcoin burn was redeemed at, or 0
79Redeemthe group's redemption call. Its arguments are in the witness, see Redemption.

A deposit or payment record is consensus(OutPoint) ‖ consensus(TxOut). Records are concatenated with no header, so decode them in a loop until the bytes run out. For a deposit, the outpoint is the vault output that received the FB. The TxOut's value is the amount credited, and its script is the claimant: who receives the frFB on Bitcoin.

GetIntent takes the deposit txid as two little-endian u128 words, in the txid's internal byte order (the order it is serialized in the outpoint). It returns source ‖ payload:

sourcemeaning
0x00no intent
0x01from Wrap cellpack words. An empty payload means no intent: protostone padding makes every plain Wrap look like this.
0x02from an fb-intent tapscript envelope
0xfe / 0xffoversized / malformed: stored without a payload, and the frFB is delivered plainly

frFB (4:4444, Bitcoin) views​

Use the Bitcoin endpoint, https://mainnet.subfrost.io/v4/{apikey}.

opcodename
99 / 100 / 101name / symbol / total supply
21GetBurnsByHeight {height}: the redemption queue, same record format; the TxOut is the FB owed and its Fractal destination
88Burn {dest0, dest1, dest2, dest_len}: burn the frFB you carry in, to be redeemed at that Fractal script

To redeem, call [4, 4444, 88, dest0, dest1, dest2, dest_len] carrying only frFB. The destination is your Fractal scriptPubKey as three little-endian u128 words plus its length. Only standard scripts are accepted; anything else reverts and you keep the frFB. The rest is in Redemption.

Three ways to deposit​

1. Use a gateway address (recommended). You build nothing. Derive the address from your intent, check it, and send FB to it from any wallet. The rest of this page covers this path.

2. Call Wrap yourself. One Fractal transaction that:

  • pays the vault's opcode-104 script;
  • has an output to your claimant script, the Bitcoin script that will receive frFB;
  • carries an OP_RETURN with one protostone: protocol tag 44, cellpack [44, 44, 77], and pointer = your claimant output. An intent can be appended as [44, 44, 77, byte_len, w0, w1, …].

The pointer must not point back at the vault, and the transaction must pay the vault something. Otherwise Wrap reverts and nothing is credited.

3. Don't pay the script from opcode 103. That is the untweaked key. Paying it, or tweaking a key yourself and getting it wrong, results in nothing credited, and no error anywhere. Always pay the script that opcode 104 returns.

The intent encoding (v2)​

An intent is canonical bytes. The address depends on every byte, so encode exactly this:

fieldsize
version 0x021
recipient script length n (1–75)1
recipient scriptPubKey: receives frFB on Bitcoin, and is the claimant output on Fractaln
refund x-only key, or 32 zero bytes for no refund32
refund CSV in blocks, u16 LE; 0 when there is no refund2
sweep fee K, u32 LE, 1,000–100,000 sats: the deposit pays its own sweep4
action length m, u16 LE2
action: what to do with the frFB after minting (opaque; empty = deliver)m

No refund is the default. The sweep is fully committed and anyone can broadcast it, so a refund path is not needed for safety. An intent with no refund (all-zero key, CSV 0) gives a gateway address with a single leaf, the sweep. If you do set a refund key and CSV, a second leaf lets that key take the deposit back after CSV blocks. The two forms give different addresses for the same recipient.

Version 0x01 intents have no K, and their sweeper pays the claimant output and the fee. They still decode and can still be swept, but nothing issues them any more.

Decoding is strict. Trailing bytes, an unknown version, or anything over 4096 bytes is rejected.

Computing the gateway address yourself​

The address is P2TR(internal = H, tree = {sweep leaf}), plus a refund leaf only if the intent sets one. H is BIP341's unspendable point 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0. The sweep leaf is built from two things only:

  • your intent bytes;
  • the vault script: fb-vault opcode 104, read live, never hardcoded.

The reference implementation is the fb-gateway crate:

fb-gateway address --recipient <bitcoin scriptPubKey hex> --vault <opcode-104 script hex> [--fee 2000]

Fractal mainnet uses Bitcoin's bc1p… address format, so the address is the same string a Bitcoin wallet would show.

Always compute the address on your side. Whoever builds the address chooses where the frFB goes. Never send FB to a gateway address you have not recomputed from your own recipient script.

Asking the gateway service to sweep it​

SUBFROST runs a gateway service on its peer-to-peer network, subtun0. It is not behind a public HTTP endpoint: you reach it through any subtun0 relay (wss://wss-2.asilos.ltd/ws, with wss-1 and wss-3 as fallbacks) at

fr1kq3hutys4xp0gvukkpr68nyzrjt2tnrwlafnqxuuv8agh2l720tqlxjywg.peer

The name is derived from the service's subtun0 key, so nobody else can answer as it without that key. Even so, never trust an address it returns without comparing: the client below does that for you.

requestdoes
POST /v1/gateway {recipient_spk | recipient_address, sweep_fee?, refund_xonly?, csv?, action?}derives the address from the live vault script, registers it, and returns the address, intent bytes, leaf scripts, control block and merkle root
GET /v1/gateway/:addressthe registration, deposits seen and their sweep status, and whether the address is stale
GET /v1/vaultthe current vault script and signer, the sweep fee bounds and the minimum deposit

The reference client does the whole compare-before-deposit flow:

fb-gatewayd client --service fr1kq3hutys4xp0gvukkpr68nyzrjt2tnrwlafnqxuuv8agh2l720tqlxjywg.peer \
gateway --recipient-address <your Bitcoin address> [--sweep-fee 2000]

It reads opcode 104 itself, derives the address locally, asks the service to derive and register the same intent, and prints the deposit address only if both agree. The service currently sweeps deposits of at least 10,000 sats (and always at least K + 330).

The flow:

  1. Compute the gateway address locally from your intent and the live opcode-104 script.
  2. Register the same intent with the service. It computes the address independently and stores it.
  3. Compare the two addresses. If they differ, stop: someone is wrong about the intent or the vault script.
  4. Deposit FB to the address from any wallet.
  5. The service sweeps the deposit after one confirmation. It broadcasts the covenant spend into fb-vault; the deposit itself pays the claimant output and the fee, out of K. Ask it for status, or watch fb-vault's deposit log (opcode 23) and your intent (opcode 25).
  6. The signer group mints frFB, less fees, to your recipient on Bitcoin.

The service is a convenience, not a dependency. The sweep is fully determined by your intent, so if the service is down you can build and broadcast it yourself with fb-gateway (the deposit pays its own fee, so you need no other funds).

Checklist​

  • Read opcode 104 every time. Don't cache it. A rotation changes it, and a stale address can only be swept to the old key.
  • Recompute every gateway address locally before showing it to a user.
  • Treat a SimulateResponse.error as failure, even though the HTTP status is 200.
  • Sweep or deposit promptly after deriving an address.
  • Use env -u SUBFROST_API_KEY with alkanes-cli if you have that variable set to anything other than a valid RPC key. The header outranks the key in the URL, and a wrong key makes reads come back empty rather than failing.