▌ TICKER BCH / USD · chipnet · docs
homedocsstatsgithub
Docs · BCH/USD ticker
chipnet · v14

Decentralized BCH/USD ticker.

Thirteen publishers, each pinned to one operator-diverse exchange, attest prices to a covenant on Bitcoin Cash. The covenant commits the per-cycle median on chain. No admin keys; the covenant is the only on-chain rule.

§01Overview

ticker.cash is a BCH/USD price feed that lives entirely on Bitcoin Cash chipnet. Every 60 s, ≥ 7 of 13 publishers refresh their slot commits with a fresh price observation; whichever publisher wins the race broadcasts Oracle.update, which commits the per-cycle median to chain.

The on-chain code is three CashScript covenants: Oracle, PublisherSlot, Ticker. No proxies, no admin paths, no off-chain governance signers. The Oracle covenant address is permanent for the life of the deploy; behavioural changes require a fresh genesis at a new address.

§02Trust model

The trust root is a 7-of-13 publisher quorum. Each publisher fetches the price from its pinned CEX over TLS, signs (sourceId, price, ts, publisherPkh, cycleSeq, hash160(serverName)) with its slot-pinned key, and the covenant commits the result. The Oracle.update median absorbs ≤ 6 misbehaving publishers per cycle without bias.

Sources are operator-diverse. 9 USD spot markets (Kraken, Coinbase, Gemini, Binance.US, Bitstamp, Crypto.com, Bitfinex, EXMO, Independent Reserve — 4 US, 5 non-US), 2 USDC (OKX, KuCoin), 2 USDT (Bybit, HTX). No operator family appears twice; no quote-currency dominates. Both stablecoin clusters could depeg the same direction simultaneously and the median still lands in the 9-USD cluster.

Publisher collusion model

What a colluding ≥ 7 of 13 publishers can do:

  • Move the on-chain median to any value they all agree to sign.

What no collusion can do:

  • Skip the ≥ 7 distinct-publisher quorum check — enforced in script.
  • Replay a stale attestation — PublisherSlot.attest() requires newCycleSeq > slot.cycleSeq (strict-greater).
  • Hide a cycle from observers — every state transition is an on-chain tx.
  • Mint new slots after genesis — slot category closed forever by CashTokens consensus.
  • Run the chain faster than the 60 s minimum stride enforced by Oracle.cash.

No admin keys. No upgrade path. Changing the publisher set requires a fresh PublisherSlot covenant, new genesis, and a slot fleet migration. The new address coexists with the old.

Collapsed-trust note. All 13 publisher slots currently run under a single operator on shared infrastructure. The 7-of-13 quorum is structurally enforced by the covenant, but its security only realises its full value once the federation is split across independent operators — manifest distribution, per-operator key custody, joint genesis ceremony.

§03Cycle flow

  1. Publisher polls the Oracle UTXO for lastTs and seq (the next cycle attests at seq + 1).
  2. Publisher fetches the price from its pinned CEX over TLS.
  3. Publisher broadcasts PublisherSlot.attest. The slot covenant verifies the publisher ECDSA-DER sig, the CN/SAN hash, and the monotonic cycleSeq advance, then rewrites the slot commit in place.
  4. Publisher waits ~25 s for peer slot refreshes to appear in mempool.
  5. Once ≥ 7 slots carry the current cycleSeq, any publisher builds Oracle.update consuming the Oracle UTXO + ≥ 7 slot inputs, re-emits each slot unchanged at its matching output index, and mints 2 mutable Ticker NFTs.
  6. First publisher to broadcast wins. Losers retry on the new tip.

Each cycle's full signature path is in §04.

§04Signatures

Five signatures appear on the path from "an exchange quoted a price" to "your dApp's tx settles." One is produced and verified per slot refresh — the heart of the trust model. The rest are standard BCH input sigs. Every signature ties to a specific publisher, source, and cycle.

Step 1 · Slot refresh (×13 publishers, ~60 s)

   Exchange                Publisher        PublisherSlot
   (1 of 13)               (1 of 13)        covenant
   ─────────               ─────────        ─────────────
        │                       │                  │
        │ ◄─── TLS ────────────┤                  │
        ├── price ────────────► │                  │
        │                       │                  │
        │                       │ ECDSA-DER sig 1  │
        │                       │ signs:           │
        │                       │   sourceId       │
        │                       │   ‖ price        │
        │                       │   ‖ ts           │
        │                       │   ‖ publisherPkh │
        │                       │   ‖ cycleSeq     │
        │                       │   ‖ hash160(serverName)
        │                       │                  │
        │                       │ Tx sig 2 (P2PKH) on slot.attest
        │                       ├────────────────► │
        │                       │                  │
        │                       │       verifies sig 1,
        │                       │       cycleSeq strictly advances,
        │                       │       hash160(SN) matches slot's pinned CN hash

Step 2 · Oracle.update (race winner)

   ≥ 7 slots      Race-winner publisher       Oracle covenant + N × slot.consume
   ─────────      ─────────────────────       ─────────────────────────────────
       │                  │                              │
       │                  │  Tx sig 3 (P2PKH)            │
       │                  │  (no off-chain sig needed)   │
       │                  ├────────────────────────────► │
       │                  │                              │
       │                  │              Oracle.update enforces:
       │                  │                position-checked median,
       │                  │                ≥ 7 distinct + sorted pkhs,
       │                  │                newTs - prevTs ≥ 60,
       │                  │                2 mutable Ticker mints.
       │                  │              Each slot.consume enforces
       │                  │              identical slot re-emit at its
       │                  │              own input/output index.

Step 3 · Consumer

   Consumer dApp                    Ticker covenant + consumer's own gate
   ─────────────                    ──────────────────────────────────────
        │                                       │
        │  Tx sig 4                              │
        │   ├─ in[0] = consumer covenant        │
        │   │    (auth via covenant unlock —    │
        │   │     e.g. checkSig over recipient  │
        │   │     key, pushed into unlock script)│
        │   └─ in[1] = Ticker NFT (no sig —     │
        │              Ticker.cash is anyone-   │
        │              can-spend)               │
        │  out[0] = business output             │
        │  out[1] = Ticker re-emit              │
        ├─────────────────────────────────────► │
        │                                       │
        │                       Ticker.cash:  re-emit at out[1] identical
        │                       Consumer:     price gate, freshness gate

Signature inventory

#SignerAlgorithmSignsVerified by
0Exchange TLS certTLSThe TLS sessionPublisher's TLS client, CN/SAN pinned
1Publisher (1 of 13)ECDSA-DERsourceId ‖ price ‖ ts ‖ publisherPkh ‖ cycleSeq ‖ hash160(serverName)PublisherSlot.attest() slot-pinned key
2Publisher walletP2PKHThe slot.attest txBCH consensus
3Race-winner walletP2PKHThe Oracle.update txBCH consensus
4Consumercovenant unlock (or P2PKH if the consumer skips a covenant)The consumer's own tx (auth shape depends on whether the spender is a covenant or a plain wallet)BCH consensus + consumer covenant gate

Why each binding matters

  • publisherPkh in the publisher sig. Locks the attestation to the slot's pinned identity — covenant rejects the sig if it doesn't match the on-chain pkh.
  • hash160(serverName) in the publisher sig. A publisher can't fetch from Coinbase and relabel the price as Kraken's — the covenant pins each sourceId to a specific CN hash at genesis.
  • cycleSeq in the sig. A signature produced for cycle N can't be replayed at N+1; the slot's stored cycleSeq is strict-monotonic.
  • No off-chain sig on Oracle.update. Anyone can broadcast it. The covenant is the only judge; the race winner just pays the fee.
What no signature in this system grants. No signer can change the rules of the covenant, roll back a confirmed cycle, override the median, or mint new slots after genesis. Every signature is a witness inside a fixed game; the rules are in contracts/Oracle.cash + contracts/PublisherSlot.cash, deployed once, immutable forever.

§05Verify

Anyone can independently verify that the on-chain median matches what publishers attested. The Oracle covenant doesn't need a trusted indexer — the chain has everything.

Step 1 · Find the latest Oracle.update tx

Query the Oracle covenant's address for its sole UTXO. The UTXO's birthing tx is the most recent Oracle.update. The tx outputs are: [0] Oracle re-emit, [1..N] the N (7–13) slot re-emits in input order, [N+1] and [N+2] the two new Ticker NFTs, optional [N+3] change. Decode tx.outputs[0].nftCommitment as the 19-byte Oracle commit:

0x60 | seq(4) | lastTs(4) | medianPrice(8) | activeCount(2)  (LE throughout; medianPrice = USD × 1e8)

Step 2 · Read each slot input's pre-update commit

Walk tx.inputs[1..N]. Each input is a PublisherSlot UTXO — fetch its tx_hash:tx_pos, then look at THAT tx's output to get the slot's nftCommitment as it sat on chain when Oracle.update consumed it. Decode the 39-byte slot commit:

0x73 | sourceId(2) | publisherPkh(20) | price(8) | timestamp(4) | cycleSeq(4)

Each slot's cycleSeq must equal oracle.seq (the cycle's new sequence number, written by this update). If any slot's cycleSeq doesn't match, the covenant would have rejected the tx — but the check is also yours to confirm.

Step 3 · Recompute the median

Take the price field from each of the 7-13 slot inputs. Sort ascending. The covenant uses "lower-middle" median: index floor((N - 1) / 2). For N=13 that's index 6 — the 7th smallest. The Oracle commit's medianPrice must equal that value.

Step 4 · Verify each publisher signature

For deeper assurance: look up each slot input's birthing tx — the prior slot.attest that wrote the commit you just read. That tx's input[0] unlock script contains the publisher's ECDSA-DER signature (70–72 B) plus the raw serverName and price/ts/cycleSeq pushes. (The covenant's checkDataSig accepts a 64 B Schnorr signature too, but the deployed publisher fleet signs ECDSA.) Recompute the publisher digest:

sha256( u16LE(sourceId) ‖ u64LE(price) ‖ u32LE(ts) ‖ pkh20 ‖ u32LE(cycleSeq) ‖ hash160(serverName) )

Verify it against the publisher's pubkey (also pushed in the unlock script). And verify hash160(serverName) matches the slot covenant's pinned sourceCNHashes[sourceId] entry.

The CashTokens consensus rules already enforce this. A tx that violated any of these checks would not have been accepted by miners; the on-chain state you see has already cleared them. Re-doing the math is verification of the chain, not of an oracle's word.

Tooling

A reference verifier is planned (ticker-verify — single-purpose Rust binary that pulls the chain via Electrum and runs the four steps above, exiting non-zero on any mismatch). Until then, the steps above can be scripted with any cashscript-aware tooling.

§06Integrate

One path: spend a Ticker NFT as an input in your transaction.

On-chain

Spend a Ticker NFT as an input in your transaction. The Ticker is mutable, anyone-can-spend, and self-replicates via the Ticker covenant. Your tx becomes a chain-descendant of the Oracle.update that minted the Ticker — BCH consensus refuses to confirm your tx in any chain history that excludes the update.

input[0]   your consumer covenant
input[1]   Ticker NFT (from the pinned Oracle category)
output[0]  your business logic (payment, mint, settle…)
output[1]  Ticker re-emit  (Ticker.cash enforces structural equality)
output[2]  optional change

Decode tx.inputs[1].nftCommitment as the 17-byte commit:

0x80 | seq(4) | lastTs(4) | medianPrice(8)

Little-endian throughout. medianPrice / 10⁸ = USD. Pin the Oracle category (LE-reversed) as a covenant arg so an attacker can't feed you a Ticker from a different oracle.

Reference covenant: contracts/examples/PriceGatedRelease.cash — locks BCH, releases on price ≥ strike. ~64 B script body.

Barrier semantics caveat. The reference example takes a constructor-baked minLastTs floor and rejects any Ticker older than that — but the floor is fixed at deploy time, not a rolling "current price" window. Once the gate has been historically reachable (any Ticker at or after minLastTs cleared the strike), it stays open until spent. For sliding-window "tx.locktime − lastTs ≤ N" freshness, the consumer covenant has to add that check itself; a generic freshness witness for that use case is planned.

§07Operate

One stateless daemon, ticker-node. Each operator runs one process per slot.

Publisher

Polls the Oracle UTXO, fetches its pinned source over TLS, refreshes its slot via slot.attest(), waits ~25 s for peers, then races to broadcast Oracle.update. ~2.2 k sats per cycle just for slot.attest, plus a share of the ~12.8 k Oracle.update tx fee averaged across racers — works out to ~3.2 k sats per cycle per slot (see §09 for the full breakdown).

$ ticker-node --publisher --slot 0

Contract addresses, publisher pkhs, and Electrum endpoints come from the manifest at $TICKER_HOME/manifest.json; the source pin is positional (slot iSOURCES[i], baked into the binary); the per-slot secp256k1 publisher key is loaded from $TICKER_HOME/publisher.key (mode 0600).

Resource cost (per publisher, measured live)

One ticker-node process. Numbers below are from the running chipnet deploy (slot 0, > 1 hour uptime, single-process measurement on Linux x86-64).

Memory~4 MiB RSSVmRSS = 4160 KiB, of which only 624 KiB is anonymous heap; the rest is shared file mappings (binary, glibc, CA bundle). 13 co-located publishers fit in ~60 MiB total.
CPU~0.03 % of one core1 s of CPU per 3300 s of wall time at 60 s stride. Each cycle = ~18 ms of CPU (slot.attest sign + Oracle.update build + Electrum I/O). Idle between cycles.
Threads3Main + publisher loop + stats server.
Disk (state)~20 KiB$TICKER_HOME/ holds manifest (~1.8 KiB), publisher.key (64 B), advisory state file (~200 B). No DB.
Disk (binary)1.7 MiBStatic-linked Rust ELF, includes embedded cashc artifacts (~55 KiB) + Mozilla CA bundle.
Disk (logs)~30 MiB/monthStructured JSON via journald, ~1 MiB/day. Compressed at rest.

Network bandwidth (per publisher)

Each cycle the daemon: fetches its pinned source over HTTPS (~3 KiB round-trip), broadcasts its slot.attest tx (~2.2 KiB wire), watches Fulcrum via Electrum WS subscription, and broadcasts an Oracle.update attempt (~12.8 KiB wire) which has a ~1/13 chance of being the canonical winner. Race-lost broadcasts still consume upload bandwidth but pay zero fee.

Upload per cycle~18 KiBslot.attest (2.2) + Oracle.update broadcast (12.8) + Electrum requests (~2) + CEX GET (~1).
Download per cycle~12 KiBlistunspent responses (slot category returns ~6 KiB), Oracle UTXO push, CEX response.
Per day~43 MiB1440 cycles × ~30 KiB.
Per month~1.3 GiBWell under any entry-level VPS bandwidth allowance.

Funding (BCH per cycle)

Every cycle the publisher's funder wallet pays fees for its slot.attest tx plus a share of the Oracle.update tx if it wins the race. At 1 sat/byte mempool floor:

slot.attest~2,200 sats/cycle~2.2 KiB wire × 1 sat/byte. Paid every cycle.
Oracle.update share~1,000 sats/cycle12.8 KiB tx ÷ 13 racers (one winner per cycle, cost amortised across the federation).
Total per publisher~3,200 sats/cycle= 4.6 M sats/day = 0.046 BCH/day per slot.

On chipnet sats are free (testnet); the current deploy runs perpetually on test funds. On mainnet, at a reference BCH/USD of $300, one publisher costs ~$13.80/day (~$415/month) in tx fees alone. The 13-publisher federation total: ~$180/day. Costs scale linearly with stride — halving to 30 s would double the fee burn; doubling to 120 s would halve it.

Minimum-viable VPS

For one publisher slot:

  • 1 vCPU (uses < 0.1 %; spec is for headroom + restart handling)
  • 256 MiB RAM (uses < 10 MiB; spec is for OS + journald + buffer)
  • 5 GiB disk (uses < 100 MiB inc. logs; spec is for OS + 1 yr log retention)
  • 100 GiB/month bandwidth (uses ~1.3 GiB; spec is generous)
  • Linux x86-64 + systemd (the supported deployment target)

The cheapest tier of any mainstream provider (Hetzner CX11, Vultr $3.50, Linode Nanode $5) is overspec'd. The 13-publisher federation today co-locates on a single CX22-class VPS using ~60 MiB RAM total.

Optional: own BCHN + Fulcrum

The daemon talks to any Electrum-compatible Fulcrum. By default it uses a 3-endpoint failover pool (one operator-run, two public). Running your own BCHN + Fulcrum is optional — it tightens the trust model (no Fulcrum operator to trust for UTXO reads) but adds an order of magnitude to the resource budget: a chipnet BCHN node needs ~2 GiB RAM, ~10 GiB disk for the chain (chipnet height ~308 k blocks ≈ 250 MiB pruned, 2-3 GiB unpruned), and Fulcrum adds another ~1-2 GiB RAM for indexing. Reasonable if you're already running BCH infra; not required for a publisher.

Source. github.com/toorik2/ticker.cash. Federation setup uses ticker-ops setup-all after generating a 32-byte seed.

§08Reference

Parameters

Publisher count (N)13One slot per (publisher, source). Closed at genesis.
Source count139 USD + 2 USDC + 2 USDT, operator-diverse.
Quorum floor (Tfloor)7Minimum distinct publishers per cycle.
Quorum ratchet~50%Threshold = max(Tfloor, ⌈0.5 × oldActive⌉). Active count decays 10%/cycle on participation drop.
Cycle stride (min)60 sEnforced by the covenant (Oracle.cash:85: require(newTs - prevTs >= 60)); publisher daemon races to broadcast as soon as the floor elapses + quorum forms.
Tickers per cycle (K)2Mutable; anyone-can-spend; self-replicate.
Price scaleUSD × 108medianPrice / 1e8 = USD.
NetworkchipnetMainnet follows after stability period.

Commit layouts

Oracle19 B · version 0x600x60 | seq(4) | lastTs(4) | medianPrice(8) | activeCount(2)
Slot39 B · version 0x730x73 | sourceId(2) | publisherPkh(20) | price(8) | timestamp(4) | cycleSeq(4)
Ticker17 B · version 0x800x80 | seq(4) | lastTs(4) | medianPrice(8)

Live contracts (chipnet · v14 · deployed 2026-05-30)

Oraclebchtest:p0nrdzy7…r2ug7jck
Slotbchtest:pvq90xk4…5yy6wch
Tickerbchtest:pw8gptmx…nrd62lp
Oracle category3dd6b6c3c57040d35df36f1bc9b9099b8d307b553a229683a250a67a15beb7ee
Slot categoryde1c438b6969c90782fc637d759b609b1b2ac0880d7219669a689c47fb1b3ba5
Oracle genesis tx203a78fd…ed191868b
Slot genesis tx611cc605…532521e71f0

§09FAQ

Is this real money?

No. ticker.cash runs on chipnet, BCH's persistent testnet. Sats are free from a faucet; the published price is a real on-chain commit but it's chipnet-only. Mainnet is a future ceremony — there's no fixed date.

How is this different from Chainlink, Pyth, RedStone?

Those systems aggregate prices off-chain and post the result on chain; the on-chain step is a write by a trusted multisig or DON. ticker.cash aggregates on chain — the covenant itself position-checks the median across slot inputs, verifies the distinct-pkh quorum, enforces the stride floor. The chain is the aggregator. There's no off-chain "writer" to trust beyond the publisher quorum.

What if 7+ publishers go down at once?

The covenant's quorum floor blocks Oracle.update below 7 distinct slot inputs at the matching cycleSeq. The chain pauses. Once enough publishers come back online, the next Oracle.update resumes the stream — same Oracle UTXO, new median, advancing seq. No state is lost. The last published cycle is still spendable as a Ticker NFT, just stale.

Why 60-second cycles? Could it go faster?

60 seconds is the covenant's enforced minimum stride (Oracle.cash:85): require(newTs - prevTs >= 60). Picked to balance CEX rate limits and per-day sat burn against consumer latency. The actual measured stride hovers slightly above 60 s depending on quorum-wait + propagation. Faster would require a covenant migration.

Why CashTokens, not OP_RETURN / sidechains / etc.?

CashTokens NFTs give us four properties that nothing else on BCH provides together: (1) a slot's identity is enforced by covenant + consensus, not by a script we wrote, (2) the slot category is closed forever after genesis, so no one can mint new slots, (3) tickers self-replicate via the Ticker covenant so consumers don't need to re-mint anything, (4) on-chain composability — a consumer's covenant can spend a Ticker as a price gate without intermediate trust. OP_RETURN lacks all of these.

Who runs the publishers?

Currently a single operator (this project) runs all 13 publisher slots on one VPS. The manifest + per-slot key layout supports a future multi-operator distribution, but the federation hasn't split yet. The trust assumption is honest about this — see §02's collapsed-trust note.

Can I run my own slot?

Yes, the binary is the same (ticker-node --publisher --slot N) — but slot identity is pinned at genesis to a specific pubkey hash, and adding a 14th slot is a hard cutover (new covenant, new genesis, slot fleet migration). For now there's no open process to join the federation; the structure exists for when there is.

How do consumers handle freshness?

The Ticker NFT's commit carries lastTs. A consumer's covenant can require tx.locktime - lastTs ≤ N for any freshness window N. The reference example contracts/examples/PriceGatedRelease.cash uses a simpler shape — a deploy-time minLastTs floor (require(int(tickerCommit.slice(5, 9)) >= minLastTs), line 47). That gives you "anytime after this fixed wall-clock", not "current price ≥ strike now". A generic sliding-window freshness witness is planned.

What does it cost to operate?

About 2.8 k sats per cycle per publisher: ~2.2 k for the slot.attest tx (1 sat/byte × ~2.2 KB) plus a 1/13 amortized share of the Oracle.update fee (~8 k for the race winner → ~0.6 k each). At ~60 cycles/hour federation-wide that works out to roughly 4 M sats/day per slot in steady state. The protocol burns sats; it doesn't earn them.

usd.ticker.cash · docs · chipnet
github