Development of Bitcoin Ordinals Infrastructure
Ordinals are not smart contracts. When developers come with EVM experience and expect a standard stack (ABI, events, RPC), the first two weeks are spent rethinking the fundamentals. Bitcoin doesn't emit events, it has no state, no address abstraction in the familiar sense. Ordinals work on top of witness data in SegWit transactions — this is fundamentally different infrastructure work.
How Ordinals and Inscriptions work technically
Ordinal theory assigns each satoshi a sequence number based on mining order. The satoshi number is deterministic — it is calculated from the block number and position in the coinbase transaction. Transferring ordinals — transferring a specific satoshi in a transaction with the correct input/output order.
Inscriptions — arbitrary data written to the witness field of a transaction via the envelope pattern:
OP_FALSE
OP_IF
OP_PUSH "ord" // marker
OP_PUSH 1 // tag: content-type
OP_PUSH "image/png" // MIME type
OP_PUSH 0 // tag: content
OP_PUSH <data_chunk1> // data (up to 520 bytes per chunk)
OP_PUSH <data_chunk2> // continuation
...
OP_ENDIF
OP_FALSE OP_IF creates a branch that never executes, but data is written to witness. After Taproot (BIP 341), witness data is ~4x cheaper than regular transaction data (discount factor). This made Ordinals economically viable.
Commit-reveal scheme: Inscription is created in two transactions:
- Commit tx — contains P2TR output with commitment to the script with inscription
- Reveal tx — spends this output, revealing the script with inscription data
This protects against front-running: before the reveal transaction, inscription content is unknown.
Node infrastructure setup
Bitcoin Core + ord indexer
Minimal production stack:
Bitcoin Core (full node, pruned NOT suitable) → ord indexer → PostgreSQL/RocksDB → API
Bitcoin Core requires archival mode (unpruned) — Ordinals need access to witness data of all historical transactions. Size as of early 2025: ~650GB and growing. SSD is mandatory, HDD is unacceptable for production.
# bitcoin.conf
txindex=1 # index all transactions by hash
server=1 # RPC
rpcuser=rpc
rpcpassword=strong_password
rpcallowip=127.0.0.1
zmqpubrawblock=tcp://127.0.0.1:28332
zmqpubrawtx=tcp://127.0.0.1:28333
ord — reference indexer implementation by Casey Rodarmor:
# Initial synchronization (takes 12-48 hours)
ord --bitcoin-data-dir /data/bitcoin \
--data-dir /data/ord \
index update
# Run server
ord --bitcoin-data-dir /data/bitcoin \
--data-dir /data/ord \
server --http-port 8080
ord provides REST API: /inscription/{id}, /sat/{sat_number}, /output/{outpoint}. For production — behind nginx with caching.
Server requirements
| Component | CPU | RAM | Disk |
|---|---|---|---|
| Bitcoin Core (mainnet) | 4+ cores | 8GB | 700GB+ NVMe SSD |
| ord indexer | 8+ cores | 16GB | 100GB+ NVMe SSD |
| Total | 12 cores | 24GB | 800GB+ |
Initial Bitcoin Core synchronization — 1–3 days. ord index on top — another 12–48 hours. Block updates — real-time (seconds after confirmation).
Custom indexer development
ord server covers basic queries, but for complex products (marketplace, collection analytics, parent-child inscriptions) a custom indexer is needed.
Transaction parsing via Bitcoin RPC
from bitcoinrpc.authproxy import AuthServiceProxy
import json
rpc = AuthServiceProxy("http://rpc:[email protected]:8332")
def parse_inscription_from_tx(txid: str) -> dict | None:
"""Extracts inscription from reveal transaction"""
raw = rpc.getrawtransaction(txid, True)
for vin in raw.get("vin", []):
witness = vin.get("txinwitness", [])
for item in witness:
script_bytes = bytes.fromhex(item)
inscription = try_parse_inscription_script(script_bytes)
if inscription:
return inscription
return None
def try_parse_inscription_script(script: bytes) -> dict | None:
"""Parses ord envelope from witness script"""
# Look for marker: OP_FALSE(0x00) OP_IF(0x63) ... "ord" ...
try:
idx = script.index(b"\x00\x63") # OP_FALSE OP_IF
except ValueError:
return None
# Parse content-type and content
# ... (full parser envelope ~100 lines)
pass
Parent-child Inscriptions (recursive)
From ord 0.6+ parent inscriptions are supported — NFT collections with provenance. Child inscription references parent via pointer in envelope. For collection marketplace need to index these relationships:
CREATE TABLE inscriptions (
id TEXT PRIMARY KEY, -- inscription id (txid + i)
sat BIGINT NOT NULL, -- ordinal number
content_type TEXT,
content_length INTEGER,
block_height INTEGER NOT NULL,
parent_id TEXT REFERENCES inscriptions(id),
created_at TIMESTAMP NOT NULL
);
CREATE INDEX ON inscriptions(parent_id); -- for fast collection lookup
CREATE INDEX ON inscriptions(sat);
BRC-20 and Runes: tokens on top of Ordinals
BRC-20
BRC-20 uses JSON content in text/plain inscriptions as operations (deploy, mint, transfer). No smart contract — the indexer must interpret the sequence of operations itself:
// Deploy
{"p":"brc-20","op":"deploy","tick":"ordi","max":"21000000","lim":"1000"}
// Mint
{"p":"brc-20","op":"mint","tick":"ordi","amt":"1000"}
// Transfer (two-step: inscribe transfer → send)
{"p":"brc-20","op":"transfer","tick":"ordi","amt":"500"}
Critical point: balances are completely determined by the indexer. No on-chain state — balance is the result of applying all operations to the initial state. Different indexers can give different results in edge cases (double-spend attempts, invalid sequences). For production — strict adherence to l1brc20 indexer specification.
Runes (Casey Rodarmor, April 2024)
Runes — official token standard from the author of Ordinals. Unlike BRC-20, Runes store state in UTXO: each UTXO carries the balance of a specific Rune. This is closer to Bitcoin UTXO model, less burden on the indexer.
OP_RETURN OP_13 <encoded_runestone>
Runestone — CBOR-like structure in OP_RETURN output. Contains: Etching (deploy), Mint (minting), Edicts (transfers). ord indexer supports Runes natively from version 0.17.
Custodial operations: working with UTXO
For a marketplace or platform that manages user inscriptions, a PSBT (Partially Signed Bitcoin Transactions) scheme is needed:
# PSBT logic for inscription sale
# Buyer and seller sign their parts independently
# Seller signs: input (inscription UTXO) + output (price in BTC)
seller_psbt = create_psbt_seller(
inscription_outpoint=outpoint,
price_sats=price_sats,
seller_payment_address=seller_addr
)
# Buyer adds: input (BTC) + output (inscription to them)
final_psbt = buyer_sign_and_complete(
seller_psbt,
buyer_address=buyer_addr,
funding_utxos=buyer_utxos
)
# Broadcast
rpc.sendrawtransaction(final_psbt.extract_transaction().serialize().hex())
PSBT (BIP 174) — standard for multi-party transactions. For marketplace: seller lists inscription with PSBT-signature for sale, buyer adds their inputs and finalizes.
Sat-control during transfers: need to precisely control which satoshi (and its number) goes to which output. Input and output order determines this. The ord library contains sat-tracking logic, which can be reused.
Monitoring and alerts
ZMQ from Bitcoin Core — the right way to receive real-time notifications of new blocks and transactions:
import zmq, asyncio
async def watch_new_blocks():
ctx = zmq.asyncio.Context()
sock = ctx.socket(zmq.SUB)
sock.connect("tcp://127.0.0.1:28332")
sock.setsockopt_string(zmq.SUBSCRIBE, "rawblock")
while True:
topic, body, seq = await sock.recv_multipart()
block_hash = body[:32].hex()
await process_new_block(block_hash)
This is faster than RPC polling and doesn't overload the node with extra requests.
Timelines and scope
| Phase | Content | Duration |
|---|---|---|
| Infrastructure | Bitcoin Core + ord setup, server, monitoring | 3–5 days |
| Custom indexer | Inscription parsing, BRC-20/Runes, PostgreSQL schema | 1–2 weeks |
| API layer | REST API for frontend/partners, caching | 1 week |
| Marketplace mechanics | PSBT listing/purchase, custodial operations | 2–3 weeks |
| Testing | Testnet (signet), edge cases, load testing | 1 week |
Complete infrastructure for Ordinals marketplace: 5–8 weeks. Just indexer + API for existing product: 2–3 weeks.







