Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
Ordinal theory, the inscription envelope field by field, commit and reveal, the runestone byte by byte, etching commitments, edicts, cenotaphs and the costly mistakes.
Bitcoin has no smart contracts. No global state, no loops, no persistent per-account storage. Even so, since 2023 it has been carrying images, texts, collections and fungible tokens.
That did not happen through a consensus change. It happened because two features created for another purpose — SegWit's weight discount and Taproot's script path — accidentally opened up cheap room for arbitrary data. Ordinals and Runes are what got built on top of that.
This guide is the documentation I wish I had found: the byte format, the exact rules, the commands and the mistakes that cost money.
Three concepts need to be solid, otherwise the rest makes no sense.
Bitcoin does not keep a balance per address. It keeps unspent outputs — each with a value in satoshis and a spending condition. A transaction consumes whole outputs and creates new ones.
bitcoin-cli listunspent
# [{ "txid": "abc...", "vout": 0, "amount": 0.00015000, "confirmations": 12 }]
bitcoin-cli gettxout "abc..." 0Since each output has a traceable origin, it is possible to follow specific fractions of coin through history. That is what ordinal theory takes advantage of.
SegWit (2017) separated the signature from the body of the transaction and created the weight unit (WU):
The virtual size (vsize), which is what sets the fee, is the weight divided by 4. In practice: data in the witness costs a quarter.
bitcoin-cli decoderawtransaction "<hex>" | jq '{size, vsize, weight}'
# size: total size in bytes
# weight: 4 * (bytes in the body) + 1 * (bytes in the witness)
# vsize: weight / 4 <- this is what multiplies the fee in sats/vBTaproot (2021) allows an output to be spent by revealing a script kept in a tree. That script goes into the witness — and has no practical size limit imposed by consensus.
Put the two together: a Taproot script can carry an entire file, paying a quarter of the fee. Neither upgrade was proposed with that goal. The consequence was emergent — and it is the root of all the discussion that followed.
Ordinal theory, created by Casey Rodarmor, is a numbering convention. It changes nothing about Bitcoin: it is a rule anyone can apply over the chain and arrive at the same result.
Rule 1 — assignment. Satoshis are numbered from 0 to 2,099,999,997,690,000, in the order in which they are mined. The satoshis in a block's subsidy get the next available numbers.
Rule 2 — transfer. Satoshis pass through transactions in first-in, first-out order (FIFO). Concatenate the inputs in the order they appear; distribute them to the outputs in the order they appear.
# Inputs: [ A: sats 100-199 ] [ B: sats 500-549 ]
# Outputs: [ X: 120 sats ] [ Y: 30 sats ]
# X receives: 100-199 (100 sats) + 500-519 (20 sats)
# Y receives: 520-549 (30 sats)Rule 3 — fees. The satoshis paid as a fee go to the miner and enter the coinbase output, right after the subsidy satoshis. That is why spending an inscribed sat as a fee hands your inscription to the miner.
It is worth insisting: none of this is validated by the network. A Bitcoin node has no idea what an ordinal is. The indexer running that rule is what knows. Two divergent implementations would produce divergent balances — the convention only holds because everyone uses the same one.
The same satoshi can be written in five ways. They all identify the same number.
| Notation | Example | What it expresses |
|---|---|---|
| Integer | 2099994106992659 | The plain serial number |
| Decimal | 3891094.16797 | block.offset — block of origin and position within it |
| Degree | 1°111094′214″16797‴ | Cycle, epoch, difficulty period and position |
| Percentile | 99.99971949060254% | Relative position in the total supply |
| Name | satoshi | Base-26 encoding (letters a–z) |
Degree notation is the one that reveals rarity, because it exposes Bitcoin's four cycles at once:
A°B′C″D‴
# A = cycle (every 6 halvings, when the halving and the adjustment coincide)
# B = index of the block in the halving epoch (0 to 209,999)
# C = index of the block in the difficulty period (0 to 2,015)
# D = index of the satoshi within the blockFrom that comes the rarity scale:
| Rarity | Condition | Approximate quantity |
|---|---|---|
| Common | Any sat that is not the first of the block | ~2.1 quadrillion |
| Uncommon | The first sat of each block | ~6.9 million |
| Rare | The first sat of each difficulty adjustment | ~3,400 |
| Epic | The first sat of each halving epoch | 32 |
| Legendary | The first sat of each cycle | 5 |
| Mythic | The first sat of the genesis block | 1 |
ord find 2099994106992659 # which UTXO holds this sat
ord list <outpoint> # which sats are in this UTXO
ord traits 2099994106992659 # rarity, cycle, epoch, notationsRarity is a property of the convention, not of the protocol. A "rare" sat is indistinguishable from any other as far as Bitcoin's consensus is concerned — it is worth whatever the market that accepts the convention decides it is worth.
An inscription attaches content to a satoshi. The content goes inside an envelope in the Taproot script revealed at spending time:
OP_FALSE
OP_IF
OP_PUSH "ord" # protocol marker
OP_PUSH 0x01 # tag 1 = content-type
OP_PUSH "image/png"
OP_PUSH 0x00 # tag 0 = body
OP_PUSH <bytes ...> # content, in chunks of up to 520 bytes
OP_PUSH <bytes ...>
OP_ENDIFThree details explain why this works:
OP_FALSE OP_IF ... OP_ENDIF creates a block that never executes. As far as consensus is concerned, it is inert data — it costs no validation and breaks nothing.The inscription is assigned to the first satoshi of the first output of the reveal transaction — unless a pointer says otherwise (tag 2, below).
An inscription's identifier is the reveal txid followed by the index:
6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
# <reveal txid> + "i" + <index of the inscription in that transaction>After the "ord" marker, the envelope carries pairs of tag and value. The tags are numbers:
| Tag | Field | What it is for |
|---|---|---|
| 0 | body | The content itself (always last) |
| 1 | content-type | MIME type: image/png, text/plain;charset=utf-8, text/html |
| 2 | pointer | Which sat in the transaction the inscription should land on |
| 3 | parent | Id of the parent inscription (provenance) |
| 5 | metadata | Metadata in CBOR |
| 7 | metaprotocol | The name of a protocol running on top (e.g. BRC-20) |
| 9 | content-encoding | Encoding of the body, such as gzip |
| 11 | delegate | Id of another inscription that supplies the content |
The parity convention matters: even tags are essential — an indexer that does not recognise one of them must treat the inscription as invalid; odd tags can safely be ignored by older versions. It is the mechanism that lets the protocol evolve without breaking anyone who has not updated.
# Compressed content, to fit more cheaply
OP_PUSH 0x09 OP_PUSH "gzip"
OP_PUSH 0x01 OP_PUSH "text/html;charset=utf-8"
OP_PUSH 0x00 OP_PUSH <compressed html>Inscribing requires two transactions. Understanding this avoids most operational mistakes.
# 1) COMMIT
# Creates a Taproot output whose address commits to the script tree,
# and one of the branches of that tree is the envelope with your content.
# At this stage NONE of the content appears on the chain.
# 2) REVEAL
# Spends that output via the script path, revealing the envelope branch.
# This is where the bytes enter the blockchain, in the witness.Practical consequences:
ord wallet inscribe --fee-rate 15 --file arte.png
# {
# "commit": "e2f1...",
# "reveal": "6fb9...",
# "inscriptions": [{ "id": "6fb9...i0", "location": "6fb9...:0:0" }],
# "total_fees": 24310
# }Relay limit. By default, a node only relays transactions of up to 400,000 weight units (-maxstandardtxweight). Beyond that, the transaction is valid but does not circulate through the network: it only gets into a block if it is delivered straight to a miner.
Block limit. 4,000,000 WU. A single inscription can, in theory, take up almost a whole block.
The fee arithmetic. Since the witness costs 1 WU per byte:
# Estimate for a 100 KB file in the witness
# 100,000 bytes * 1 WU = 100,000 WU
# vsize = 100,000 / 4 = 25,000 vB
# at 20 sats/vB -> 25,000 * 20 = 500,000 sats of fee (the reveal alone)
bitcoin-cli estimatesmartfee 6 | jq '.feerate' # BTC/kvB
bitcoin-cli getmempoolinfo | jq '{size, bytes, mempoolminfee}'Rule of thumb: the same file in the body of the transaction would cost four times as much. It is the witness discount that makes the practice economically viable — and it is exactly why part of the community considers the discount a design mistake.
Cutting the cost, in practice:
# 1. Compress first (tag 9 = content-encoding)
gzip -9 -c pagina.html > pagina.html.gz
# 2. Prefer lean formats: SVG and HTML usually beat PNG
# 3. Use recursion: reference already-inscribed libraries instead of re-inscribing them
# 4. Inscribe in a batch: one transaction for several inscriptions
ord wallet batch --fee-rate 8 --batch lote.yamlTag 3 points to a parent inscription. For the parentage to be valid, the parent inscription has to be spent in the reveal transaction — that is, only whoever controls the parent can create a child. This is how a collection proves authenticity with no central registry.
# lote.yaml — a collection with provenance
mode: separate-outputs
parent: 6fb9...i0
inscriptions:
- file: 001.png
- file: 002.png
- file: 003.pngTag 11 makes one inscription point at another one's content. A thousand items sharing the same image can be a thousand tiny inscriptions delegating to a single one — the cost collapses.
An inscription can fetch another through the /content/<id> endpoint. This allows a library to be inscribed once and reused:
<script src="/content/6fb9...i0"></script>
<img src="/content/a1b2...i0">Other recursion endpoints expose data from the chain itself, which allows generative art that reacts to Bitcoin's state:
/r/blockheight # current height
/r/blockhash # hash of the block
/r/blocktime # timestamp
/r/sat/<number> # inscriptions on that sat
/r/children/<id> # children of an inscriptionTag 5 carries metadata in CBOR — binary, more compact than JSON:
# {"nome": "Peça 001", "autor": "Jhonatan", "edicao": 1}
# becomes a few bytes of CBOR in the envelope
ord wallet inscribe --fee-rate 10 --file arte.png --json-metadata meta.jsonA satoshi can receive more than one inscription. Later ones are recognised, but clearly marked as reinscriptions — the first is the canonical one.
In the first months, some inscriptions were created in ways the reference implementation had not anticipated: duplicated envelopes in the same input, unknown tags, non-standard structures.
Instead of discarding them, ord started numbering them with negative numbers — the cursed inscriptions. It was a way of saying "this exists, but it is not canonical".
At block 824,544, the so-called jubilee ended that distinction: from then on, the previously cursed forms started receiving normal positive numbering. The old ones kept their negative numbers as a historical record.
ord list <outpoint> | jq '.inscriptions'
# a negative number = an inscription from before the jubilee, created non-canonicallyThe engineering lesson here is a good one: in a convention-based protocol, changing the rule is a social decision, not a technical one. There was no fork — there was an agreement among the people running the indexer.
# Prerequisite: a full node with a transaction index
bitcoind -daemon -txindex=1
bitcoin-cli getblockchaininfo | jq '{blocks, initialblockdownload}'
# Indexer (the first indexing takes hours and occupies tens of GB)
ord --index-sats server --http-port 8080
# Wallet
ord wallet create # generates and shows the seed — keep it offline
ord wallet receive # address to deposit to
ord wallet balance # separates cardinal (spendable) from ordinal
ord wallet outputs # all the UTXOs
ord wallet inscriptions # what you have inscribed# Inscribing
ord wallet inscribe --fee-rate 12 --file arte.png
ord wallet inscribe --fee-rate 12 --file arte.png --destination bc1p...
ord wallet inscribe --fee-rate 12 --file arte.png --postage 10000sat
# Sending
ord wallet send --fee-rate 10 bc1p... 6fb9...i0 # by inscription id
ord wallet send --fee-rate 10 bc1p... 2099994106992659 # by sat number
# Querying
ord wallet transactions
ord decode --txid <txid> # shows the decoded envelopeThe --postage parameter deserves attention: it is how many satoshis accompany the inscription in the output. The default is 10,000 sats. Very low values can fall below the dust limit and block future transfers.
Before Runes, creating a fungible token on Bitcoin meant using BRC-20: inscriptions with a JSON saying what you wanted to do.
{"p":"brc-20","op":"deploy","tick":"ordi","max":"21000000","lim":"1000"}
{"p":"brc-20","op":"mint","tick":"ordi","amt":"1000"}
{"p":"brc-20","op":"transfer","tick":"ordi","amt":"100"}It works, but with three serious flaws:
Runes, launched by Casey Rodarmor at block 840,000 (the April 2024 halving), was designed to solve this by being native to the UTXO model: the balance lives in the UTXO itself, and the instruction goes in a single OP_RETURN output.
A runestone is an OP_RETURN output like this:
OP_RETURN
OP_13 # the Runes protocol marker (OP_PUSHNUM_13)
<data push> # payload
<data push> # (if there is more than one, concatenate in order)The payload is a sequence of LEB128 integers (variable-length varints), read as tag → value pairs:
| Tag | Name | Function |
|---|---|---|
| 0 | Body | From here on come the edicts |
| 1 | Divisibility | Decimal places (0 to 38) |
| 2 | Flags | Bits enabling etching, terms and turbo |
| 3 | Spacers | Where the name's visual separators go |
| 4 | Rune | The name, encoded in base 26 |
| 5 | Symbol | A Unicode character |
| 6 | Premine | The amount reserved for the creator |
| 8 | Cap | Maximum number of mints |
| 10 | Amount | Amount delivered per mint |
| 12 / 14 | HeightStart / HeightEnd | Mint window by absolute height |
| 16 / 18 | OffsetStart / OffsetEnd | Mint window relative to the etching |
| 20 | Mint | Id of the rune being minted |
| 22 | Pointer | The output receiving the unallocated balance |
The same parity rule as inscriptions applies here: an unknown even tag invalidates the runestone (it becomes a cenotaph); an unknown odd tag is ignored. That is what allows features to be added in future without breaking old indexers.
# Decoding a runestone from the chain
ord decode --txid <txid>
bitcoin-cli getrawtransaction <txid> 1 | jq '.vout[] | select(.scriptPubKey.type=="nulldata")'Why OP_RETURN and not the witness? Because OP_RETURN is provably unspendable — nodes can discard those outputs from the UTXO set, without bloating the network's memory. It costs more per byte (it sits in the body, with no discount), and that is precisely why the format is binary and lean.
Etching is the act of creating the rune. It defines the name, symbol, divisibility, premine and the minting rules.
# etching.yaml
mode: separate-outputs
etching:
rune: MINHA•PRIMEIRA•RUNE
divisibility: 2
premine: 1000
symbol: ¤
supply: 21000
terms:
amount: 100
cap: 200
height: [840000, 900000] # absolute window (optional)
turbo: true
inscriptions:
- file: logo.png # optional: a "visual cenotaph" of the runeord wallet batch --fee-rate 20 --batch etching.yamlHere is the detail that most confuses first-time etchers: the rune's name has to be committed beforehand, and the commitment needs at least 6 confirmations before the etching transaction.
The reason is anti-front-running: without it, anyone would see your name in the mempool and publish the same rune with a higher fee. With the commitment, whoever tries to copy it would have to wait six blocks — long enough for the original to confirm.
# In practice, ord does this for you and waits:
# "Waiting for rune commitment ... to mature (6 confirmations)"
# It is about an hour between starting and the rune existing. Do not interrupt the process.premine + (amount × cap).# premine 1000 + (100 * 200) = 21,000 units
# The larger the premine's share, the more centralised the distribution.Premine is the first number anybody should look at before touching a rune. A 90% premine means the creator holds nearly everything.
The Flags field (tag 2) is a set of bits:
| Bit | Name | Meaning |
|---|---|---|
| 0 | Etching | This transaction creates a rune |
| 1 | Terms | The etching defines open mint rules |
| 2 | Turbo | The rune automatically adopts future protocol changes |
Rune names use only capital letters from A to Z, encoded as an integer in a modified base 26.
MINHA•PRIMEIRA•RUNE
# The real identity is MINHAPRIMEIRARUNE.
# The "•" are spacers (tag 3): visual decoration, not part of the name.
# There are no two runes with the same name, even with different spacers.If every name had been available at launch, the short ones would have been taken in the first blocks. The protocol prevents that by releasing the names over time:
# Minimum length by current height, in practice:
# 840,000 -> 13 letters
# 857,500 -> 12 letters
# 875,000 -> 11 letters
# ...
# 1,050,000 -> 1 letterRunes etched before block 840,000 do not exist: the first possible rune is the one from the launch block itself.
Each rune is identified by the block and the position of the transaction within it:
840000:1 # block 840,000, transaction 1
# This is the id the edicts use — not the name.If the etching defined terms, anyone can mint until the cap is reached, within the permitted window.
ord wallet mint --fee-rate 15 --rune MINHA•PRIMEIRA•RUNE
# A mint is a runestone with the Mint tag (20) pointing at the rune id:
# OP_RETURN OP_13 <20> <block> <tx>Rules worth knowing:
amount. There is no partial mint.height or offset) is not a transaction error: it simply produces nothing, and the fee was paid all the same.cap, mints stop producing. On popular runes this becomes a race per block, and it is what usually blows up the network fee during mint windows.height) uses absolute block numbers; the offset window (offset) counts from the etching block.A transfer is an edict inside the runestone: a triple of (id, amount, output).
ord wallet send --fee-rate 12 bc1p... 500:MINHA•PRIMEIRA•RUNETwo conventions make all the difference:
OP_RETURN outputs". It is the airdrop mechanism in a single transaction.# A transaction with 3 outputs (indexes 0, 1, 2) plus the OP_RETURN:
# edict (840000:1, 0, 3) -> splits the whole balance among outputs 0, 1 and 2Whatever is left over without an explicit edict goes to the first non-OP_RETURN output. The Pointer tag (22) changes that destination:
# Without a pointer: the rune change lands on output 0
# With pointer = 2: the change lands on output 2Forgetting this is one of the easiest ways to send a balance to the wrong address — including the recipient's, when you only meant to send the change.
A cenotaph is a malformed runestone. The name is no accident: a cenotaph is an empty tomb.
A runestone becomes a cenotaph when:
And the consequence is harsh, deliberately:
| Situation | What happens |
|---|---|
| Runes entering the transaction | They are burned |
| The runestone contained an etching | The rune is created with zero supply and cannot be minted |
| The runestone contained a mint | The mint counts against the cap, but the units are burned |
Why so severe? Because the alternative — ignoring the error and moving on — would open room for divergence between indexers. Burning is deterministic and the same for everyone. The cost of that choice is that a bug in your implementation destroys real balance, with no recourse.
# Before signing any hand-built rune transaction:
ord decode --txid <txid> # ord flags a cenotaph
# When in doubt, test on signet first. Always.Since neither Ordinals nor Runes are validated by consensus, every balance depends on an indexer. Running your own is the only way not to trust a third party.
# Approximate requirements for ord with the sat index
# - a full Bitcoin node with -txindex=1 (~700 GB and growing)
# - tens of GB more for ord's index
# - an SSD is practically mandatory; the initial indexing takes hours
ord --index-sats --index-runes server --http-port 8080
curl -s localhost:8080/status | jqUseful endpoints of the local API:
curl -s localhost:8080/inscription/<id> -H "Accept: application/json"
curl -s localhost:8080/output/<txid>:<vout> -H "Accept: application/json"
curl -s localhost:8080/rune/MINHA•PRIMEIRA•RUNE -H "Accept: application/json"
curl -s localhost:8080/sat/2099994106992659 -H "Accept: application/json"There are third-party APIs that spare you the work of indexing. They are convenient and legitimate, but note what you are accepting: their answer is your application's source of truth. For display, that is fine; for deciding on the custody of value, run your own index.
The risk here is rarely cryptographic — it is operational.
# 1. Separate the wallets. Inscribed sats and runes NEVER in the everyday wallet.
ord wallet balance # cardinal = spendable | ordinal = protected
# 2. Lock the UTXOs that carry value
bitcoin-cli lockunspent false '[{"txid":"<txid>","vout":0}]'
bitcoin-cli listlockunspent
# 3. Check the destination before sending
ord wallet send --dry-run --fee-rate 10 bc1p... 6fb9...i0Rules that prevent almost every loss:
bitcoind -signet -daemon
ord --signet --index-sats server
ord --signet wallet inscribe --fee-rate 1 --file teste.txtOrdinals and Runes are, technically, a remarkable case study: two complete protocols built without changing a single line of consensus, taking advantage of features that existed for another purpose.
That brings a characteristic that needs to be clear before anything else: Bitcoin knows about neither of them. There is no balance validation, there is no friendly error message, there is no reversal. There is a convention that holds as long as the indexers agree — and a set of unforgiving rules for when you get the encoding wrong.
For anyone who writes code, the value of studying this goes beyond the subject: it is a rare example of a well-documented protocol, with explicit design decisions (why OP_RETURN and not the witness, why burn instead of ignore, why tag parity) and verifiable consequences on the chain. You can read the code, reproduce every byte on signet and check.
And if you are going to touch mainnet: signet first, a separate wallet always, and nothing of value without a hardware wallet.