Alkanes protocol docs

Wire format

Protostone payload encoding

This page gives the exact bytes. Every claim here is checked against alkanes-rs: the Rust decoder in crates/protorune-support/src/protostone.rs, the tag table in crates/ordinals/src/runestone/tag.rs, and the encoder in ts-sdk/src/protostone/. The two sides agree, and the examples below were produced by running the encoder and feeding the result back through the decoder.

Why this page matters

A wrong payload does not fail loudly

An alkanes transfer is an encoded protostone. The output script must begin with 6a 5d, OP_RETURN followed by OP_PUSHNUM_13. A plain 6a OP_RETURN carrying JSON, a text string, or any other blob is not a runestone. It is skipped entirely.

The transaction still confirms. The fee is still spent. The input that held your alkanes is still gone from the UTXO set. But the index never learned that the balance should move, so it stays recorded against an outpoint that can never be spent again. There is no error, no refund, and no recovery.

The correct payload for a simple transfer is what the ts-sdk produces from encodeRunestoneProtostone with ProtoStone.edicts({ protocolTag: 1n, edicts: [{ id, amount, output }] }). Everything on this page explains what that call actually emits, so you can produce it in any language and verify it yourself.

The five layers

A protostone is nested twice inside LEB128. That is the part people get wrong. Reading outward from the data you care about:

The five encoding layers, drawn as a reaction sequence Five stages joined by arrows. Stage one, protostone integers: protocol tag, field count, then fields. Stage two, LEB128 encode into a byte stream. Stage three, split into 15-byte little-endian chunks, each becoming one u128. Stage four, emit each chunk as a runestone field with tag 16383 and LEB128 encode the whole runestone. Stage five, prefix OP_RETURN and OP_PUSHNUM_13 and append the payload as data pushes. 1 protostone integers 2 LEB128 varints 3 15-byte little-endian chunks 4 runestone fields, tag 16383, LEB128 5 6a 5d + data pushes [1, 5, 0, 2, 1, 100000000, 1] 010500020180c2d72f01 [5604910583939447391489] ff7f 818a80909080a0e1d7df04 6a5d0dff7f818a80909080a0e1d7df04
The worked transfer from further down this page, at every stage. Layers 2 and 4 are both LEB128. Layer 3 exists to keep every value inside the runestone varint limits.

1. The script envelope

The runestone lives in one OP_RETURN output whose value is zero. The script is:

OP_RETURN            0x6a
OP_PUSHNUM_13        0x5d
<data push>          the LEB128 runestone payload, in chunks of at most 520 bytes

alkanes-rs looks for exactly these two leading bytes. crates/ordinals/src/runestone.rs defines the magic as a single little-endian u16 constant 0x5d6a, which is the byte pair 6a 5d read as a 16-bit little-endian value. If the second opcode is anything else, the output is not a runestone and the transaction carries no protostone.

2. Runestone fields

The payload is a flat list of LEB128 varints read as tag and value pairs. Tag 0 is special: it ends the field section and everything after it is the Runes-layer edict body. The protostone data rides in repeated fields with tag 16383.

Runestone tags relevant to alkanes, from crates/ordinals/src/runestone/tag.rs.
TagNameRole for alkanes
0BodyRunes-layer edicts. Alkanes transfers do not use this; they use the protostone body instead.
20MintRunes minting. Not used by alkanes.
22PointerDefault output for unallocated Runes-layer balances. Frequently present alongside a protostone.
16383ProtocolOne 15-byte chunk of the protostone byte stream. Repeated as many times as needed.

16383 is 214 - 1, and its LEB128 encoding is always the two bytes ff 7f. Spotting ff7f repeatedly in an OP_RETURN is a reliable sign that you are looking at a protostone.

3. The 15-byte rule

Layer 3 is the step most re-implementations miss. The protostone byte stream is cut into groups of 15 bytes. Each group is read as a little-endian integer and becomes one u128 value.

Why 15 and not 16

A u128 LEB128-encodes to at most 19 bytes. The Runes decoder treats a varint longer than 18 bytes as a flaw and turns the runestone into a cenotaph. Capping each value at 15 bytes of payload keeps the top bits clear so the re-encoded varint always fits. The comment in crates/protorune-support/src/protostone.rs puts it directly: never write to the 16th byte of the u128.

The two directions are exact inverses:

Because the decoder always re-expands to a full 15 bytes, a short final group comes back with trailing zero bytes. That is expected and harmless at the protostone level: the record loop stops as soon as it reads a protocol tag of 0. It is not harmless inside a message field, which is covered below.

4. Protostone records

The reassembled byte stream is LEB128-decoded again into a flat integer list, which is then read as a sequence of records:

[ protocol_tag, length, value_0, value_1, ... value_(length-1) ]
[ protocol_tag, length, ... ]
...

Within a record the values are again tag and value pairs, with tag 0 ending the fields and starting the body:

Protostone field tags actually read by Protostone::from_fields_and_tag.
TagNameTypeMeaning
0BodyquadruplesEdicts. Every remaining value in the record belongs here.
81Messagerepeated u12815-byte chunks of the cellpack calldata.
83Burnu128Marks the record as a protoburn.
91ProtoPointeru32Output that receives the result when the message succeeds.
93Refundu32Output that receives the balances when the message fails.
95Fromu32Edict index selector used by protoburns.

Any other tag is parsed as a pair and then ignored. The ts-sdk enum additionally names SPLIT = 85, but the Rust Tag enum has no such variant and the decoder does nothing with it, so do not emit it.

5a. The edict body

Edicts are the transfer instructions. They are encoded as a flat run of values in groups of four:

block_delta, tx_delta, amount, output

Rules, from protostone_edicts_from_integers and next_protostone_edict_id:

A body that is not a multiple of four fails silently

protostone_edicts_from_integers returns edict values did not appear in sets of four, but from_fields_and_tag discards that error and substitutes an empty edict list. The protostone is still valid, the message still runs if there is one, and the transfer you intended simply never happens.

5b. The message field

A message carries a cellpack: the call to make. Its integer form is

[ target.block, target.tx, input_0, input_1, ... ]

target is the AlkaneId to invoke, and input_0 is by convention the opcode the contract dispatches on. The list is LEB128-encoded, then cut into 15-byte chunks exactly as in layer 3, and each chunk is emitted as a tag 81 field.

Calldata is zero-padded, and the padding becomes inputs

Because join_to_bytes always writes 15 bytes per chunk, the calldata the indexer reconstructs is padded with zero bytes, and decode_varint_list turns each of those into a zero-valued u128. The cellpack [2, 1, 77] is 3 bytes on the wire and arrives as target 2:1 with inputs [77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0].

Pass every argument explicitly rather than relying on a default, and design opcodes so that trailing zeros are harmless.

A message record must also carry ProtoPointer (91) and Refund (93). Both are required by Protostone::process_message; a message without them is an error that rolls the transaction's protostone processing back rather than refunding anything.

A cellpack that decodes to fewer than two varints is rejected before anything runs. That guard is explicit in crates/alkanes-support/src/cellpack.rs and exists because the slice expression that reads target.block and target.tx would otherwise panic, and a panic stops the indexer for everyone.

Worked example: a transfer, byte by byte

Move 100000000 base units of alkane 2:1 to output 1.

Step 1: protostone integers

[1, 5, 0, 2, 1, 100000000, 1]
 |  |  |  |  |  |          |
 |  |  |  |  |  |          output index 1
 |  |  |  |  |  amount, raw u128
 |  |  |  |  tx delta   (0 -> 1)
 |  |  |  block delta (0 -> 2)
 |  |  Tag 0, Body: edicts follow
 |  five values in this record
 protocol tag 1, alkanes

Step 2: LEB128

BytesValueMeaning
011protocol tag, alkanes
055record length
000Tag 0, Body
022block delta
011tx delta
80 c2 d7 2f100000000amount
011output index
010500020180c2d72f01          10 bytes

Step 3: 15-byte chunk

Ten bytes is a single group. Reversed and read as a little-endian integer:

bytes  01 05 00 02 01 80 c2 d7 2f 01
LE     0x012fd7c2800102000501
u128   5604910583939447391489

Step 4: runestone fields

[16383, 5604910583939447391489]

ff 7f                              tag 16383
81 8a 80 90 90 80 a0 e1 d7 df 04   the chunk        13 bytes total

Step 5: script

6a 5d 0d ff7f 818a80909080a0e1d7df04
Final script hex
6a5d0dff7f818a80909080a0e1d7df04

16 bytes. 6a OP_RETURN, 5d OP_PUSHNUM_13, 0d push 13 bytes, then the payload. Put it in one output with value 0.

The equivalent ts-sdk call:

const { encodedRunestone } = encodeRunestoneProtostone({
  protostones: [
    ProtoStone.edicts({
      protocolTag: 1n,
      edicts: [{
        id: new ProtoruneRuneId(2n, 1n),
        amount: 100000000n,
        output: 1,
      }],
    }),
  ],
});

Worked example: a contract call

Call alkane 2:1 with opcode 77, sending the result to output 0 and refunding to output 1.

cellpack           [2, 1, 77]
cellpack LEB128    02 01 4d                      3 bytes
one 15-byte chunk  0x4d0102 = 5046530

protostone         [1, 6, 91, 0, 93, 1, 81, 5046530]
                    |  |  |   |  |   |  |   calldata chunk
                    |  |  |   |  |   |  Tag 81, Message
                    |  |  |   |  |   refund pointer, output 1
                    |  |  |   |  Tag 93, Refund
                    |  |  |   proto pointer, output 0
                    |  |  Tag 91, ProtoPointer
                    |  six values
                    protocol tag 1

LEB128             01 06 5b 00 5d 01 51 82 82 b4 02      11 bytes
chunk              3270285073163573723334145
runestone          [16383, 3270285073163573723334145]
payload            ff7f 818cec82d0abc0a88285d215        14 bytes
Final script hex
6a5d0eff7f818cec82d0abc0a88285d215

17 bytes. Note that 91 is 0x5b and 93 is 0x5d: the byte 5d inside the payload is the Refund tag, not a second OP_PUSHNUM_13.

Worked example: a real mainnet payload

This script appears in the alkanes-rs test corpus as a captured mainnet OP_RETURN:

6a5d101600ff7f818cec8ad0abc0a8a081d215

Decoded, layer by layer:

LayerResult
Script6a 5d, then a 16-byte push
Runestone integers[22, 0, 16383, 3270250341403247458846209]
Runes-layer fieldsTag 22 Pointer = 0. Unallocated Runes-layer balances go to output 0.
Protostone integers[1, 6, 91, 1, 93, 1, 81, 5046304]
Protostonetag 1, pointer output 1, refund output 1, one message chunk
Cellpacktarget 32:0, opcode 77

Alkane 32:0 is the fr-BTC system contract, so this is a wrap: opcode 77 against fr-BTC, with both the success pointer and the refund pointer aimed at output 1. You can paste it into the decoder and watch it come apart.

Three ways to lose an asset

All three confirm on chain. None of them produce an error a wallet can show you.

No magic byte

Script is 6a followed by JSON, text, or any non-runestone payload. Runestone::decipher finds no output with the magic and returns nothing, so the protorune indexing path is never entered. Alkane balances are not moved and not cleared: they stay recorded against the outpoint you just spent. That outpoint can never be spent again, so the balance is stranded permanently.

6a 4c 0d 7b2270223a226a736f6e227d      not a runestone

Right envelope, wrong protocol tag

Script is a valid runestone with a valid protostone, but the protocol tag is not 1. This is worse. The runestone is valid, so the indexer does run, loads the alkane balances from the inputs, finds no protostone claiming tag 1 to assign them to, and then clears the input balances as part of normal processing. The balances are destroyed rather than stranded.

Edict body not a multiple of four

The protostone parses, the record is accepted, and the edict list silently becomes empty. Any balance you did not otherwise direct follows the default assignment instead of your intended recipient.

Encoder checklist

  1. Script starts with the two bytes 6a 5d.
  2. The OP_RETURN output has value 0 and is the only runestone in the transaction.
  3. Everything after 5d is data pushes only, each at most 520 bytes.
  4. The protocol tag inside the protostone record is 1.
  5. The record length equals the number of values that follow it, exactly.
  6. Edict ids are sorted and delta encoded from 0:0, and the body length is a multiple of four.
  7. Amounts are raw base units. 0 means the whole remaining balance, so never use it as a placeholder.
  8. Output indexes are real outputs, or deliberate virtual protostone indexes.
  9. Message records carry both ProtoPointer (91) and Refund (93).
  10. Cellpacks contain at least the two target values, and every argument is passed explicitly.
  11. Round-trip your own output through a decoder before you sign. The decoder on this site does that without sending anything anywhere.