A notarized BCH/USD price, written as a price message that says how many rows stand behind it.
Thirteen notaries each fetch one exchange over TLS every minute and sign the price they saw. A 518-byte covenant checks seven or more of those signatures, takes the lower median, and writes it on chain as a standard oracle price message followed by the number of rows that signed, plus two Ticker NFTs any transaction can spend to read it. Nobody in the system chooses a number, and nobody chooses your trust level: the chain records how many rows backed each minute, and your contract sets the bar. The notaries sign public facts, so anyone can run an oracle over them.
§01Overview
The oracle is a single CashTokens category on Bitcoin Cash. Its head is the category's minting NFT, held by the Oracle covenant. The head's 37-byte commitment is the whole state: a 16-byte standard oracle price message (the timestamp of the observed minute, the oracle's update counter twice, the price in US cents), then k (how many table rows signed), then the hash160 of the notary table. Every update spends the head and recreates it with a newer minute and a counter advanced by one, minting two immutable Tickers that carry the first 17 bytes. Consumers spend a Ticker in their own transaction; the Ticker covenant makes them recreate it at the same index with a small toll, which chains their transaction to the update that minted it.
The only parties with keys are the notaries. Each is a key and an exchange: once a minute it opens its own TLS session to the pinned host, validates the certificate chain against the public roots, fetches the pinned path, parses the pinned JSON field, converts the price to cents, and signs a statement for that minute. It signs at most once per minute, only while that minute is current on its own clock, and holds no funds. A statement names no deployment: it is a public fact, and any oracle head whose table lists that key may use it.
Builders (anyone) read the statements from the notaries' APIs and post updates; if two builders post the same minute, the network's first-seen rule picks one. The head carries a BCH reserve and pays its own fees, so building needs no coins. The same transaction recycles Tickers whose hour is up, so their dust and tolls return to the reserve. The table is state, not code: a majority of the current rows can replace it, move the head to a successor covenant, or retire it, without a new deployment.
§02Trust model
What consensus enforces
- At least 7 distinct table rows signed the same
(exchange, minute, price); each signature is checked withOP_CHECKDATASIGagainst the pubkey in the table whose hash160 the head carries. - The value written is the lower median of those prices in cents, and the commitment's k is exactly how many rows signed.
- The timestamp strictly increases and may lead the chain's median-time-past by at most six hours; the update counter advances by exactly one. No block height, no relative lock anywhere.
- The head, its category and minting capability survive; exactly two Tickers of 792 sats are minted with the same snapshot; every input's value returns to the reserve except the two Tickers and a fee of at most 1,000 sats plus two per byte of unlocking data.
- The table changes only through
rotate(), approved by a strict majority of the current rows.
Who must collude to forge a minute
The builder chooses which statements to include, and the chain cannot tell a row that was down from one that was left out. So a minute is only as strong as the rows that signed it, and the commitment says how many did:
| rows that signed (k) | keys that forge that minute |
|---|---|
| 7 or 8 | 4 |
| 9 or 10 | 5 |
| 11 or 12 | 6 |
| 13 | 7 |
A consumer that requires k ≥ 11 cannot be fooled by fewer than six colluding keys and tolerates two rows being down; one that requires k ≥ 13 needs seven keys to fool and tolerates none. A forged minute signed by four colluding rows is still postable, but it is labeled k = 7, it carries their signatures on chain as evidence, and the other rows can rotate them out. On a normal day every minute is signed by all thirteen rows (90 percent of minutes on 2026-10-25, 96 percent by eleven or more).
Who governs
A strict majority of the current rows (7 of 13). They can change the table, migrate the head to another covenant, or retire it to a plain key, and with it the reserve. Seven keys already control every minute, so governance adds no power that the price did not already grant; it is why the reserve is kept to a few weeks of fees. While ticker.cash runs all thirteen rows, the honest description is "one operator who cryptographically verified each exchange".
What nobody can do
- Post a statement for another minute or exchange: both are in the signed message. A statement is valid for every deployment on purpose: it is a fact, not a vote.
- Count a row twice: row indexes must be strictly ascending and inside the table.
- Claim more rows than signed:
kis the length of the row list the signatures were checked against. - Strand the head with a far-future minute: the minute is bound to chain time.
- Keep a Ticker alive past its day: its lifetime is keyed to its own timestamp, and only an update can destroy it. Burn one in an update: a consumer's re-emission must carry the same commitment and a toll.
- Forge Tickers: the category's only minting NFT is the head, and the covenant pins every output.
§03One minute
- At second two of minute
m, every notary fetchesGET <pinned path>from its pinned host over its own TLS session, checks status200, extracts the price, rounds it to cents, confirmsmis still current, and signs for timestamp60m. A failed fetch is retried twice, six seconds apart, never past second 45. The statement is served atGET /v1/statement?seq=m. - A builder polls every row's API. As soon as all rows have signed
m, or at second twenty with at least seven, it builds one transaction: input 0 the head, then up to ten Tickers whose day is up by median-time-past, then any top-up coins; output 0 the head with60m ‖ n+1 ‖ n+1 ‖ median ‖ k ‖ tableHash, outputs 1 and 2 Tickers with the first 17 bytes. - The transaction's locktime is set to the latest of
60m − 6 hand the recycle times of the Tickers it spends; every input carries sequence0xfffffffe. - On one-minute blocks the update lands in the next block. When a block is slow, the next minute's update spends the unconfirmed head.
- Consumers spend Tickers and re-emit them with the toll. A day after its minute, a Ticker is recycled by the next update and its sats return to the reserve.
§04The messages
statement = 0x4e ‖ source(1) ‖ timestamp(4, u32 LE) ‖ price(4, u32 LE) (10 B)
approval = 0x52 ‖ category(32) ‖ oldTableHash(20) ‖ nextLockingBytecode ‖ nextCommitment[17..]
signature = Schnorr(secp256k1) over sha256(message) (64 B; what OP_CHECKDATASIG verifies)
price = US cents per BCH; timestamp = 60 × floor(unix_time / 60), the start of the minute
A statement names the exchange and nothing else: not a table position, not a deployment. It is a signed public fact, which is why a second oracle instance with its own category and reserve can consume the same statements; the suite demonstrates exactly that. The covenant rebuilds the message from the source byte stored in the row's table entry, the timestamp and the supplied price. Only the price and the signature travel in the transaction. Approvals bind the category: governance stays per instance.
The table is n × (pubkey 33 ‖ source 1), 442 bytes for thirteen rows. Every spend supplies it in full and the covenant checks its hash160 against the head's commitment. The manifest publishes it row by row with each notary's operator and API.
§05The transaction
| Part | Bytes (k = 7 / 13) | Note |
|---|---|---|
| redeem script | 577 | 518 B covenant + 55 B Ticker lock, pushed; identical for every deployment |
| notary table | 445 | 13 × 34 B, pushed |
| signatures | 451 / 835 | 64 B each |
| prices | 29 / 53 | u32 cents |
| rows, timestamp, claimed median, function selector | 19 / 25 | |
| input overhead | 43 | outpoint, sequence, varint |
| head output | 116 | P2SH-32 + 37-byte commitment |
| two Ticker outputs | 232 | 116 B each, P2S, 792 sats each (the dust floor) |
| each recycled Ticker input | + 43 | returns 792 sats plus any tolls |
| total | 1,922 / 2,336 | fee is exactly size + 1 sat |
The fee is exact because the transaction is fixed-width before it is built: the Oracle input carries no transaction signature and Schnorr signatures never vary in size. At one update a minute that is 0.028 to 0.034 BCH a day. The 2 × 792 sats parked in Tickers come back a day later, so the reserve pays fees only.
§06The covenants
Oracle.cash (518 bytes, cashc 0.14, P2SH-32) has two functions. update(table, ts, rows, prices, sigs, median):
- The head is input 0;
ts.length == 4,median.length == 4,k = rows.length ≥ 7. - Rank the claimed median: at most
(k−1)/2prices below it, at mostk/2above it, and it is one of them. That is the unique lower median for any k. hash160(table) == commit[17..37);int(ts) > int(commit[0..4));tx.time ≥ int(ts) − 21600;sequence = int(commit[8..12)) + 1.- For each row: indexes strictly ascending;
entry = table[row];checkDataSig(sig, 0x4e ‖ source ‖ ts ‖ price, pubkey). - Exactly three outputs. Output 0: same locking bytecode, same category with minting capability, commitment
ts ‖ sequence ‖ sequence ‖ median ‖ k ‖ tableHash. Outputs 1 and 2: the Ticker lock, same category, immutable, the first 17 bytes, exactly 792 sats. value(out0) ≥ Σ value(inputs) − 2·792 − 1000 − 2·len(unlocking data).
rotate(table, rows, sigs): the head is input 0; the table hashes to the commitment; 2·rows.length > n; exactly one output that keeps the category and minting capability and whose commitment starts with the current 17-byte snapshot; every signer approved 0x52 ‖ cat ‖ tableHash ‖ out0.lockingBytecode ‖ out0.commitment[17..]; the value rule as above without the Tickers. The live v27 head was reached this way from v26: a rotation into the new lock, carrying category, reserve, price and k.
Ticker.cash (55 bytes, P2S): consume() recreates the Ticker at the same index with the same lock, category and commitment and at least 1,000 sats more; recycle() requires input 0 to be this category's minting head and tx.time ≥ timestamp + 86400 from its own commitment, and nothing else, so the NFT is burned.
Source, compiled artifacts and the VM test suite are in contracts/ of the repository; the suite drives a forged transaction into every require and carries the red-team findings against v25 as negative tests.
§07Integrate
On chain
Spend a Ticker as an input of your transaction and recreate it at the same output index with 1,000 sats more (Ticker.consume enforces lock, category, commitment and toll). Pin the category from the manifest, then read the commitment and decide what k you accept. The first 16 bytes are laid out exactly like the price messages existing BCH oracles publish (the Oracles.cash layout), so code that parses those parses a Ticker:
require(tx.inputs[1].tokenCategory == oracleCategoryVm); // 32 bytes, reversed display id: an immutable NFT, never the head
bytes c = tx.inputs[1].nftCommitment; // 17 bytes
require(c.length == 17);
int t = int(c.slice(0, 4)); // timestamp: start of the observed minute
int seq = int(c.slice(8, 12)); // the oracle's update counter (bytes 4..8 repeat it)
int price = int(c.slice(12, 16)); // US cents per BCH
int k = int(c.slice(16, 17)); // rows that signed this minute
require(t >= minTime); require(t <= maxTime); // your freshness floor and ceiling
require(k >= 11); // six keys to fool you; 13 for seven; 7 for four
The reference consumer contracts/PriceGated.cash (53 bytes) does exactly this. Keep a maxTime ceiling: without one, a counterparty can hold a Ticker and surface it later. Two consecutive Tickers have consecutive counters, which is what settlement contracts in the AnyHedge family check; a Ticker lives a day, so such a pair must be used within a day of the later one.
Off chain
Everything a client needs is in /manifest.json: category, head and Ticker locking bytecode, the table and its hash, and an Electrum pool. The head's current commitment is the minting NFT of the category at the head address; blockchain.scripthash.listunspent with include_tokens returns it. Every v27 oracle shares the head address, so filter by category. This page's stats view is built on nothing else, and it checks the on-chain table hash against the manifest's.
XO templates
Three XO wallet templates live in xo/ of the repository, built and checked against the @xo-cash packages themselves: ticker-read decodes a Ticker commitment into timestamp, counter, price and rows; ticker-stamp pays at the oracle price by co-spending the newest Ticker and re-emitting it with the toll, so the payment carries the rate it was priced at; ticker-genesis instantiates a new oracle over a table of your choice with your own reserve, in two actions, because the category is the txid of the coin spent at input 0. Import xo/dist/*.json into the XO Web Dev Wallet. An XO engine has already paid at the oracle price on chipnet with the stamp template (cb2195f4…, 16:29 UTC on 2026-10-25: Ticker in, Ticker plus toll out, 5,000 sats paid), and instantiated a second oracle over the same table with the genesis template (03796b8f…, 16:43 UTC); its first update (26bfb3e8…) was built from the very same notary statements, which name no deployment. A fourth template, ticker-rotate, changes the table by majority: one role per row, each row pastes its approval, a CashAssembly loop folds them into the covenant’s rotate() arguments, and the chain enforces 7 of 13; eight XO wallets rotated a test instance on chipnet with it (310fa7aa…). The engine does not yet execute data actions or discover outputs it does not own, so the read is a decoder and the Ticker input is supplied by the app; the one thing a template cannot reach is the txid of its own input 0, so a genesis still takes the category as a variable; a nine-line engine change we prototyped closes that gap with a built-in libauth already has. xo/README.md spells out each point.
§08Run a notary
A notary is one small Rust binary, one exchange endpoint, one HTTPS request a minute. It holds no funds, needs no exchange account, opens no inbound port except a read-only HTTP API for builders, and uses a few kilobytes a minute.
ticker-notary --home ~/.ticker-notary-7 init --source 14 \
--category <deployment category> --api 0.0.0.0:7107 --operator "your name" # source 14 = Gate.io
ticker-notary --home ~/.ticker-notary-7 run # systemd unit in deploy/systemd/
ticker-notary --home ~/.ticker-notary-7 pubkey # what goes into the table
Joining this oracle's table is a table change: the current rows approve a table with your key and source (§09). But your statements are useful before that: they name no deployment, so anyone may genesis a head whose table lists your key, and a month of statements that match the exchanges is your track record. The same holds the other way round: anyone can run an oracle over any set of public notaries, with their own table and reserve, and nobody has to agree to it.
§09Governance
The rows govern. Anything that moves the head out of its current state other than an update needs approvals from a strict majority of the current table, each a Schnorr signature over 0x52 ‖ category ‖ oldTableHash ‖ nextLockingBytecode ‖ nextCommitment[13..]. Three uses of the same function:
- Table change: same Oracle lock, tail = hash160 of the new table. Add, replace or drop rows. The category, the minute, the price and k carry over; consumers notice nothing.
- Migration: a successor covenant's lock, with whatever tail its state needs after
seq ‖ price ‖ k. The category survives the protocol. - Retirement: a plain P2PKH, empty tail. The reserve and the identity go to that key.
ticker-node approval-request --table new-table.json # prints old hash, next lock, tail
ticker-notary --home ~/.ticker-notary-N sign-rotate --old-table-hash … --next-lock … --tail … > approvals/row-N.json
ticker-node rotate --table new-table.json --approvals approvals/ # builds, broadcasts, rewrites manifest.json
Approvals bind the old table hash, so they are valid only against the table they were written for. Never rotate back to a table that was used before: an old approval set for that exact transition would become valid again. The v26 to v27 upgrade was a migration of this kind: seven rows approved the v27 lock as the next locking bytecode, and the head moved with its category, reserve, price and k.
§10Reference
Parameters
| rows / floor | 13 / 7 (k is written; the bar is the consumer's) |
| median | lower median in US cents (unique for any k) |
| Tickers per update | 2 × 792 sats, immutable, P2S; toll 1,000 sats per consume; recyclable a day after their timestamp |
| fee cap | 1,000 sats + 2 per byte of the head input's unlocking data (about 2× the honest fee) |
| fee policy | exactly size + 1 sat |
| time bound | timestamp − 21,600 ≤ locktime ≤ median-time-past; counter +1 per update |
| statement window | a notary signs minute m only while 60m ≤ now < 60m + 90 s, once |
| governance | strict majority of current rows |
Commitments
Ticker (17 B): timestamp u32 LE [4] ‖ messageSequence u32 LE [4] ‖ dataSequence u32 LE [4] ‖ price u32 LE [4] ‖ k u8 [1]
head (37 B): Ticker ‖ tableHash [20]
bytes 0..16 are a standard oracle price message (little-endian signed ints, price in cents)
Exchanges
The source byte in every statement names an entry of an append-only registry (node/core/src/sources.rs): host, path and the JSON field the notary signs. An id is never redefined; replacing an exchange means a new id and a table change. Ids 4 (Binance.US), 8 (EXMO) and 9 (Independent Reserve) were retired on 2026-10-24 when their BCH/USD markets went quiet. Six rows quote USD, three USDC, four USDT, so a single stablecoin losing its peg leaves at least seven unaffected rows.
| row | source | exchange | quote | field |
|---|---|---|---|---|
| 0 | 1 | Kraken | USD | /result/BCHUSD/c/0 |
| 1 | 2 | Coinbase | USD | /data/amount |
| 2 | 3 | Gemini | USD | /last |
| 3 | 16 | Binance | USDC | /price |
| 4 | 5 | Bitstamp | USD | /last |
| 5 | 6 | Crypto.com | USD | /result/data/0/a |
| 6 | 7 | Bitfinex | USD | /0/7 |
| 7 | 14 | Gate.io | USDT | /0/last |
| 8 | 15 | Bitget | USDT | /data/0/lastPr |
| 9 | 10 | OKX | USDC | /data/0/last |
| 10 | 11 | KuCoin | USDC | /data/price |
| 11 | 12 | Bybit | USDT | /result/list/0/lastPrice |
| 12 | 13 | HTX | USDT | /tick/close |
Live deployment
| network | — |
| category | — |
| head | — |
| Ticker lock | — |
| table hash | — |
| genesis | — |
| table (registry ids, rows 0–12) | — |
§11FAQ
Why write k instead of requiring more rows?
Because with thirteen rows you cannot have both full outage tolerance and full forgery resistance in the same minute, and the builder's choice of statements is invisible to the chain. v25 required seven and treated every minute as a seven; since v26 the floor stays at seven so the oracle stays live with six rows down, and the real count is written so a reader who needs eleven or thirteen can insist on it.
What if three rows are down for a day?
The oracle keeps writing minutes with k = 10. A consumer that insists on k ≥ 11 waits; one happy with k ≥ 7 continues. No arrangement of thirteen rows avoids that choice; the oracle makes it the reader's.
Why does the notary fetch the exchange itself instead of using TLSNotary?
Because the chain trusts the notary's key either way. In v25 the notary was the TLSNotary verifier of a session a third party drove; what consumers trusted was still the notary's signature. Fetching directly gives the same guarantee (certificate validation against public roots) with one dependency fewer, no session to open, and a fetch that takes well under a second.
Why is the snapshot a standard oracle message?
Because it costs nothing and removes a translation layer. The 16-byte Oracles.cash message is what AnyHedge and BCH Bull read: one key signs one a minute, price in cents. Our Ticker carries the same 16 bytes, verified by consensus instead of a key, followed by k. Anything that parses their messages parses ours. What a Ticker cannot do is produce their key's signature, so contracts that verify that signature need a one-line change to accept a Ticker input instead.
Can I run my own oracle over these notaries?
Yes. Statements are signed facts that name no deployment. Genesis a head with your own reserve and a table listing any notaries' keys (ours, yours, a mix), run a builder, and you have an oracle with its own category that your consumers can pin. The notaries never need to know. Your instance's governance is its own rows.
Why can't the chain check that the minute is fresh?
Because a valid transaction must stay valid forever; consensus can bound time from below (locktimes) but never from above. v27 uses the lower bound to stop a far-future minute, and leaves freshness to the reader.
Who pays?
The head. Each update spends the exact fee from its reserve and parks 2 × 792 sats in Tickers, which come back a day later; consumers add 1,000 sats to every Ticker they use, which comes back with it. Anyone can top the reserve up by adding a coin to an update.
Is this real money?
The live deployment is on chipnet, Bitcoin Cash's six-months-early test network. Mainnet follows once independent notaries hold rows.