Walkthrough
Guide
Three things you will actually do: move a balance, call a contract, deploy a contract. Each one is
the same shape, one OP_RETURN output carrying a protostone, and each is worked through to real
bytes you can check.
The mental model
Four ideas cover almost everything.
Balances live on outputs
An alkane balance is recorded against a specific transaction output. To move it you must spend that output. If you spend it without a valid instruction, the balance does not follow.
The instruction is one output
A single zero-value OP_RETURN starting 6a 5d. Everything the protocol reads
comes from it.
Everything unassigned has a home
Whatever your edicts do not explicitly place ends up at the protostone's pointer, or the default output when there is no pointer. There is no discard.
Contracts are ordinary WASM
A call is a cellpack: a target id, an opcode, and arguments. The contract sees the alkanes you sent it and returns the alkanes it wants to send on.
Transfer a balance
Goal: send 100000000 base units of alkane 2:1 to someone, keeping the change.
Step 1: choose inputs
Spend the outpoint or outpoints that hold the balance. All alkane balances on all inputs are pooled, so you can combine several. Add a plain funding input if you need more sats for the fee.
Step 2: lay out outputs
vout 0 recipient address 546 sats
vout 1 your change address 546 sats
vout 2 OP_RETURN 0 sats <- the protostone
vout 3 fee change (optional)
The alkane outputs need enough value to be spendable later, so use your wallet's dust threshold rather than the minimum.
Step 3: build the protostone
const { encodedRunestone } = encodeRunestoneProtostone({
protostones: [
ProtoStone.edicts({
protocolTag: 1n,
edicts: [{
id: new ProtoruneRuneId(2n, 1n),
amount: 100000000n,
output: 0,
}],
}),
],
});
6a5d0cff7f818a80909080a0e1d75f
That is the output: 0 variant, 15 bytes. The output: 1 variant
worked through on the encoding page is
6a5d0dff7f818a80909080a0e1d7df04. Changing one small integer at the end of the protostone
changes the tail of the payload and its length, which is a good reminder to verify rather than eyeball.
Step 4: know where the change goes
The edict places exactly 100000000 at output 0. Everything left over goes to the protostone's
pointer. This protostone has no pointer, so the default output is used: the first non-OP_RETURN
output, which here is output 0 as well. If you want change at output 1, say so:
ProtoStone.edicts({
protocolTag: 1n,
edicts: [
{ id: new ProtoruneRuneId(2n, 1n), amount: 100000000n, output: 0 },
{ id: new ProtoruneRuneId(2n, 1n), amount: 0n, output: 1 },
],
})
The second edict has amount 0, which means the entire remaining balance, so it sweeps the change
to output 1. Both edicts address the same id, so the delta encoding makes the second pair 0, 0,
and the protostone integers become
[1, 9, 0, 2, 1, 100000000, 0, 0, 0, 0, 1].
6a5d11ff7f819280909080a0e1d7df8080808040
20 bytes.
Step 5: verify before signing
Paste the script hex into the decoder. Confirm the protocol tag is 1, the
id is the alkane you meant, the amount is in base units, and the output index is the recipient. Then sign.
Call a contract
Goal: call alkane 2:1 with opcode 77, sending the result to output 0 and refunding
to output 1 if it fails.
const cellpack = new Cellpack(2n, 1n, [77n]);
const { encodedRunestone } = encodeRunestoneProtostone({
protostones: [
ProtoStone.message({
protocolTag: 1n,
calldata: cellpack.serialize(),
pointer: 0,
refundPointer: 1,
}),
],
});
6a5d0eff7f818cec82d0abc0a88285d215
- Pointer and refund pointer are both required. Omitting either is not a soft failure: it aborts protostone processing for the whole transaction.
- Sending alkanes into the call. Add edicts to the same protostone targeting its own virtual
output. For a transaction with 3 real outputs the first protostone is virtual output
4. - Pass every argument. Calldata is zero-padded on the wire, so an omitted argument becomes a zero rather than an error. See the message field.
- Order matters. The message runs first; the protostone's edicts then operate on whatever landed at the pointer. If the message reverts, its edicts are skipped.
A real example
This mainnet payload calls the fr-BTC system contract at 32:0 with opcode 77:
6a5d101600ff7f818cec8ad0abc0a8a081d215
It decodes to a Runes-layer pointer of 0, then protostone tag 1 with pointer 1, refund pointer 1, and cellpack
[32, 0, 77]. Run it through the decoder to see every field.
Deploy a contract
Deployment is a call to a reserved target. The binary itself does not go in the
OP_RETURN; it goes in a witness envelope, the same way an inscription does.
| Target | Form | Result id | Binary source |
|---|---|---|---|
1:0 | CREATE | 2:<next sequence> | Witness envelope on the first input |
3:N | CREATERESERVED | 4:N | Witness envelope on the first input |
5:N | Factory from sequence template | 2:<next sequence> | Pointer to the template's code |
6:N | Factory from reserved template | 2:<next sequence> | Pointer to the template's code |
A CREATE with the initializer at opcode 0:
const cellpack = new Cellpack(1n, 0n, [0n]); // target 1:0, opcode 0
const { encodedRunestone } = encodeRunestoneProtostone({
protostones: [
ProtoStone.message({
protocolTag: 1n,
calldata: cellpack.serialize(),
pointer: 0,
refundPointer: 0,
}),
],
});
6a5d0bff7f818cec82d08bc0a801
14 bytes. The whole deployment instruction fits in fourteen bytes because the contract itself travels in the witness, which is also why the fuel accounting strips that witness before sizing the transaction.
A factory clone of the reserved template 65517:
const cellpack = new Cellpack(6n, 65517n, [0n]);
6a5d0fff7f818cec82d0abc0a886b5ffff01
Deployment is not something to attempt against mainnet first. alkanes-rs ships a test harness that runs your contract against the same code path the mainnet indexer uses, with no funds and no live node. Use it.
Read balances and traces
State is derived, so reading it means asking an indexer, not the chain. alkanes-rs exposes view functions through metashrew. The ones you will use most:
| View | Answers |
|---|---|
protorunes_by_address | Every outpoint an address controls and the alkane balances on each. |
protorunes_by_outpoint | The balances on one specific outpoint. |
trace | The full execution trace for a transaction's protostone, including revert data. |
traceblock | Every trace in a block. |
simulate | Runs a cellpack without broadcasting, so you can check a call before you pay for it. |
getbytecode | The decompressed WASM behind an alkane id. |
getstorageat | One storage key of one contract. |
meta | The contract's ABI, if it exports __meta. |
When a transaction did not do what you expected, trace is the first place to look. A revert
carries a readable message after the four-byte 08 c3 79 a0 prefix.
Support matrix
Only capability that is declared in this organisation's own code is listed as supported. Where something is absent, that is stated rather than implied.
Bitcoin Universe products
| Surface | Actions | State |
|---|---|---|
| Core, main | view, discover, view collection, view activity, view transaction | Supported |
| Wallet | view, send, receive | Supported |
| Inscribe | mint | Supported |
| Marketplace, reads | view, view collection, view activity | Read-only |
| Marketplace, mutations | list, update listing, unlist, buy, make offer, accept offer, cancel offer, sell, settle, reconcile | Not supported |
“Alkanes mutations remain read-only until exact Alkane state, transferability, builder, signed-transaction validation, broadcast, settlement, and reorg recovery are deployed and proven.”
The same snapshot records that no executable order authority is deployed, that ownership is read authority only with no complete transferability proof, that no executable settlement authority is deployed, that freshness is not enforced (no node-tip comparison, no maximum observation age), that no supported mutation reaches confirmation or settlement, and that reorg reconciliation is not automatic because there is no executable order state to roll back.
Developer tooling
| Tool | What it covers | Where |
|---|---|---|
@alkanes/ts-sdk | Protostone and runestone encoding, cellpacks, provider and wallet helpers, CLI commands for alkanes, protorunes, runestone, and metashrew. | ts-sdk/ in alkanes-rs |
| alkanes-rs test harness | Runs contracts against the same code path as the mainnet indexer, without funds or a live node. | alkanes-rs repository |
alkanes-runtime | Rust SDK for writing alkane contracts. | crates/alkanes-runtime |
| The decoder on this site | Checking a payload before you sign it. | tool.html |
Third-party wallets and marketplaces are outside this organisation's code and are not listed. Their support is not something this documentation can verify, so it makes no claim either way.
Common mistakes
- Writing JSON into an
OP_RETURN. Some Bitcoin metaprotocols work that way. Alkanes does not. Without6a 5dnothing is read, and the balance is stranded on a spent outpoint. - Using amount 0 as a placeholder. It means the entire remaining balance.
- Assuming decimals. Divisibility is asset-defined. The protocol moves raw base units and the registry records alkanes decimals as asset-defined, so never hardcode a scale.
- Pointing an edict at the
OP_RETURN. Check your output indexes against the final output order, not the order you built them in. - Omitting a cellpack argument. Zero padding fills it with
0, silently. - Forgetting the refund pointer. A message without it aborts protostone processing for the transaction.
- Adding a second
OP_RETURN. Only the first runestone counts, and a stray non-push opcode in it makes a cenotaph, which burns balances. - Testing on mainnet. The regtest harness exists precisely so you do not have to.