kaspulse · builder guide

An oracle-gated covenant on Kaspa testnet-10 in 15 minutes

The working end-to-end path: fetch a threshold-signed price from kaspulse, verify it yourself three ways (browser → one-file client → full re-derivation), then make Kaspa L1 enforce it — lock TKAS behind a covenant that releases only when the committee signed the price and the price clears your strike, and spend it. Along the way: the four traps that stop most first runs.

Read this before you start. Every command below is real: the oracle outputs on this page were captured from live runs, and the covenant and slashing flows are the same code this repo proved on testnet-10 (consumer_live, slash_live). Testnet runs are ephemeral, so no stale txids are pinned here — wherever you see a ‹value› in angle quotes, the tool prints your own fresh one, explorer-linked, as you run. Two things to know: (1) the hosted committee now dual-signs — the API message format and blake2b(price_bytes) under each feed's covenant object (see /v1/committee + Feed::verify_covenant). This guide's local 3-key demo committee remains the offline walkthrough; production gates should use hosted covenant sigs. (2) on-chain consumers are proven on testnet-10; standing feeds via cargo run --bin standing --features onchain publish to TN10 on deviation + heartbeat. Mainnet publish is next. (The one canonical status block is the README's “Status, honestly” section — this box makes the same claims, never stronger.) In the code blocks: green lines are tool output as captured; grey are comments.

0Prerequisites — 5 minutes, once

You need a Rust toolchain (stable) and the kaspulse source. The onchain feature pulls in the pinned rusty-kaspa transaction stack — the first build takes minutes, not seconds; every later build is incremental.

# clone the kaspulse repo (linked in the footer), then:
cd kaspulse
cargo build --features onchain     # first build compiles the Kaspa stack — go make coffee

You also need a funded testnet-10 key at ~/.kaspulse/tn10.key (a 64-hex Schnorr secret key — testnet funds only, worth nothing). The easiest path is the one the kascov guide uses: kascov-lab keygen prints a fresh key's address, and kaspulse's tools read kascov-lab's key file automatically as a fallback — if you already did that guide, you are done with this step.

# in the kascov repo (github.com/Knitser/kascov) — prints your TN10 address:
cargo run -p kascov-lab -- keygen
address:         kaspatest:qq…

# pin the key where kaspulse's tools look first:
mkdir -p ~/.kaspulse && cp /tmp/kascov-lab-key.hex ~/.kaspulse/tn10.key

Fund the printed address at faucet-testnet.kaspanet.io — it is Cloudflare-gated, so use a browser, not curl. Then confirm it arrived:

cargo run -p kascov-lab -- balance
balance: 100.00000000 TKAS

In a hurry? One command runs this entire guide — generates the demo committee, deploys the price gate, waits for confirmation, and spends it with a live signed price:

cargo run --bin gate --features onchain -- demo --strike 0.02 --value 3

The rest of this page does the same thing one step at a time, so you can see every moving part.

1Get a signed price — and verify it

Ask the oracle for KAS/USD. $BASE is whatever origin serves this page — http://localhost:8080 if you run the oracle locally (cargo run --bin oracle), or the hosted origin you are reading this on.

▶ captured from a live run, July 2026
BASE=http://localhost:8080     # or this page's own origin
curl -s $BASE/v1/feed/KAS-USD | jq '{pair, price, mant, expo, signed_ts, message}'
{
  "pair": "KAS/USD",
  "price": 0.02786,
  "mant": 278600000,
  "expo": -10,
  "signed_ts": 1784428033,
  "message": "kaspulse/v2|KAS/USD|278600000|-10|1784428033|69"
}

The price is exact integers, not a float: mant × 10^expo = 278600000 × 10−10 = $0.02786. The message is the exact ASCII string all 5 committee nodes signed — kaspulse/v2|PAIR|mant|expo|ts|round. Any 3 of the 5 signatures make the price valid. Now verify it, three ways, escalating in paranoia:

Level 1 — one click. Open the KAS/USD feed page and press verify. Your browser recomputes the blake2b digest and checks all 5 BIP340 Schnorr signatures itself, in ~120 lines of dependency-free JS. No server involved in the verdict.

Level 2 — one file, your machine. The same verifier as a zero-dependency script (clients/js/kaspulse.mjs, or clients/py/kaspulse.py for Python). It self-tests its crypto against official BIP340 vectors before it will verify anything:

▶ captured from a live run, July 2026
node clients/js/kaspulse.mjs verify KAS/USD $BASE
KAS/USD  http://localhost:8080  signed_round 93
  node 0  ✓  0dd71bf25a98da9ff89a661537997fc9b3f3ae5eb37b23778f11aaec086b8f4a
  node 1  ✓  d6ce84b11b72c0e4eaccf1070b96d90611d6ba8c8d43022a84a4268b43bf3bd2
  node 2  ✓  40ffebd6b99e56b258d58574b41d493842c94152c8798827bd1bc4b4adc5d514
  node 3  ✓  7d4e175938b3b2cc0883f11bfd805eec6c92e7efba1a52af47edfc326cc48864
  node 4  ✓  458220ba8737cdd5702ba602239a294d00ef1dbc60078d458d35a2bb066fdd0b
  bound=true (signed message fields == JSON fields)
✓ VALID — 5/5 signatures verify (threshold 3), price = 0.027875

(bound=true matters: it checks the signed message's pair/mant/expo/ts fields equal the JSON fields next to it — so the API cannot show you one price while the signatures cover another.)

Level 3 — trust nothing, re-derive everything. cargo run --bin verify checks every feed's signatures, then goes around the oracle entirely: it re-fetches the exchanges itself, recomputes the median, and re-reads the KRC-20 DEX pools on-chain:

▶ captured from a live run, July 2026 (trimmed — one line per feed)
cargo run --bin verify
kaspulse verify — pulling all feeds from http://127.0.0.1:8080/api/feed
signatures (need 3 of each feed's nodes):
  ✓ KAS/USD    5/5 valid
  ✓ BTC/USD    5/5 valid
  ✓ ETH/USD    5/5 valid
  … one line per feed (24 feeds at this run) …
independent KAS/USD check (re-fetching exchanges myself):
  Kraken: $0.027870
  KuCoin: $0.027900
  Gate.io: $0.027850
  Bybit: $0.027890
  MEXC: $0.027872
  my median $0.027872  vs  feed $0.027890  (0.06% drift) ✓
independent KRC-20 check (re-reading the DEX pools myself):
  20 tokens re-read on-chain · 20 reproduce the feed (within TWAP tolerance)
VERDICT: honest — every feed signed by the threshold, KAS matches the market,
and the KRC-20 pools reproduce on-chain. No trust required.

You just audited the oracle. Everything after this point makes Kaspa L1 itself do that audit — at spend time, in script.

2The covenant Kaspa will enforce

The price gate is a P2SH covenant: a coin only spendable by presenting a price that (a) is at or above a strike and (b) carries a valid Schnorr signature from every committee key — verified by the node's script engine with post-Toccata's OpCheckSigFromStack, not by any off-chain service. The redeem script (built by kaspulse-sdk's covenant::price_gate_redeem, the byte-identical script consumer_live proved on TN10):

opcodeswhat the Kaspa VM does
OpDup ‹strike_e8› OpGreaterThanOrEqual OpVerify duplicate the spender's price_bytes, push the strike (e.g. $0.02 → 2000000), numeric compare — abort unless price ≥ strike
OpBlake2b OpToAltStack hash the price bytes — this 32-byte digest is exactly what the committee signed — and stash it on the alt stack
OpFromAltStack OpDup OpToAltStack ‹pk2› OpCheckSigFromStack OpVerify restore the digest (keeping a copy), verify node 2's signature over it — abort on failure
OpFromAltStack OpDup OpToAltStack ‹pk1› OpCheckSigFromStack OpVerify same for node 1
OpFromAltStack ‹pk0› OpCheckSigFromStack node 0's check leaves the final true/false

price_bytes — the on-chain price encoding. The price (price_e8, dollars × 108) is pushed as a minimal little-endian script number, because OpGreaterThanOrEqual compares script numbers. Real values, which you can recompute yourself:

priceprice_e8price_bytes (hex)
$0.02 (the strike below)200000080841e
$0.02786 (the captured live price)2786000d0822a
$0.02955 (an earlier capture)2955000f8162d

The demo committee signs schnorr(blake2b-256(price_bytes)) — for d0822a that digest starts 338a2f50…. This is a different, simpler encoding than the hosted committee's pipe-delimited message from step 1 — which is exactly why this guide generates a local demo committee (see the honest box).

3Deploy it

Generate the 3-key demo committee (written as gate-node-0.keygate-node-2.key in the current directory):

cargo run --bin gate --features onchain -- keygen
wrote ./gate-node-0.key
wrote ./gate-node-1.key
wrote ./gate-node-2.key
node 0 x-only pubkey: ‹64 hex — yours›
node 1 x-only pubkey: ‹64 hex — yours›
node 2 x-only pubkey: ‹64 hex — yours›

Look at the covenant before funding it — address prints the redeem script and the testnet-10 P2SH address that commits to it:

cargo run --bin gate --features onchain -- address --strike 0.02
redeem (120 bytes): ‹your redeem hex — deterministic from your 3 pubkeys + the strike›
TN10 P2SH address: kaspatest:‹…›

Watch your covenant get born on kascov → paste the redeem hex into kascov.io/decode and see it disassembled opcode-by-opcode — you'll recognize every line of the table from step 2, with your own pubkeys and strike in the pushes.

Now fund it — 3 TKAS from your ~/.kaspulse/tn10.key, locked behind the gate:

cargo run --bin gate --features onchain -- deploy --strike 0.02 --value 3
connected to TN10 · covenant P2SH kaspatest:‹…›
🎟️  DEPLOYED — 3.00 TKAS locked behind the price gate
   tx ‹your deploy txid›
   https://explorer-tn10.kaspa.org/txs/‹your deploy txid›
next (after ~25s confirm): gate spend --strike 0.02

Click the printed explorer link: that is your coin, on the real chain, spendable by no signature at all — only by a price that satisfies the script.

4The traps

Four things break first runs. gate handles all four for you — but you will hit every one of them the moment you build your own consumer, so here they are with their exact symptoms.

(a) ComputeBudget — three OpCheckSigFromStacks are not free. Post-Toccata, every input of a v1 transaction commits a per-input computeBudget (1 unit = 10,000 script units), and one Schnorr signature check costs 100,000 units — so three CSFS need ≥ 30 units before counting witness bytes, which are charged 1:1 on top. Commit too little and the node rejects with:

failed to verify the signature script: script units exceeded
the amount committed in the input: used=100000, limit=9999

(limit=9999 is the free allowance you get when your committed budget is effectively 0 — the classic symptom of tooling that silently drops the field.) The budget is covered by the signature hash, so you cannot patch it in after signing. gate commits ComputeBudget(120) on the covenant input and 50 on the fee input; committed budget is charged as mass whether used or not, so the fee scales with it — generous, not 65535. The full anatomy of this trap is in the kascov guide, step 3.

(b) The ~25-second confirmation wait. Kaspa blocks are ~100 ms, but the public node's UTXO index needs a moment. Run spend too soon and you get the honest recovery message, not a failure:

coin not in UTXO set yet — wait ~25s for confirm, then re-run `gate spend`

Nothing is lost — the coin is deployed; re-run and it spends. (gate demo polls for the coin so you never see this.)

(c) The second input still needs a plain signature. The spend has two inputs: input 0 is the covenant coin (unlocked by the witness, no key), input 1 is a plain fee-paying UTXO from your wallet — and that one must be signed SIG_HASH_ALL like any normal spend, after the covenant witness is in place (the sighash covers it). Forget input 1's signature — the one that "never needed anything" in single-input tools — and the whole transaction is rejected.

(d) Non-minimal price_bytes breaks everything, silently at first. Script numbers must be minimally encoded: $0.02786 is d0822a, never d0822a00. Pad it and two things break at once — the numeric comparison rejects the push, and the blake2b digest changes so the committee signatures no longer match. Use the SDK's price_bytes() (zero is the empty push; a high top bit gets a 0x00 pad byte; negatives set 0x80) rather than hand-rolling.

5Spend it

spend fetches the live price from the oracle, verifies it first (signatures + field binding + freshness, via the SDK — it refuses unverified prices), has the demo committee sign blake2b(price_bytes), builds the witness, and broadcasts:

cargo run --bin gate --features onchain -- spend --strike 0.02
connected to TN10 · covenant P2SH kaspatest:‹…›
oracle KAS/USD = $0.027860 (verified: 3-of-5 sigs + field binding)  ·  strike $0.020000
witness: [3 sigs, price_bytes(2786000), redeem] = 321 bytes
💸 UNLOCKED — released ‹payout› TKAS: Kaspa L1 verified 3 committee sigs + price ≥ strike.
   tx ‹your spend txid›
   https://explorer-tn10.kaspa.org/txs/‹your spend txid›

The witness pushes, bottom→top: [sig0, sig1, sig2, price_bytes, redeem]. The node ran the whole table from step 2 — three independent signatures plus the price condition, verified by L1 consensus, no oracle server in the loop at spend time. (If the price had been below the strike, gate refuses before broadcasting — and if you force it, the chain refuses too. That's the point.)

How fast does a signed price become a spendable on-chain fact? Measured on live TN10 via a public node: 1.39 s average tick→UTXO (1510/1154/1507 ms over three rounds; own-node path is sub-second) — see KASPA-EDGE.md in the repo, reproduce with cargo run --bin latency --features onchain.

6Prove it to yourself

Don't take this page's word for what just happened — the proof is all public.

Read the witness off the chain. Open your spend txid on the TN10 explorer and expand input 0's signature script: you will see your five pushes — three 64-byte signatures, your 3-byte price, your 120-byte redeem script.

Re-verify the three signatures yourself — same one-file client from step 1, no oracle involved:

python3 - <<'EOF'
import sys; sys.path.insert(0, 'clients/py')
from kaspulse import blake2b256, bip340_verify

price_bytes = bytes.fromhex('d0822a')        # the price push from YOUR witness
digest = blake2b256(price_bytes)             # what the demo committee signed
pubkeys = ['‹node 0 pubkey›', '‹node 1 pubkey›', '‹node 2 pubkey›']   # from `gate keygen`
sigs    = ['‹sig 0›', '‹sig 1›', '‹sig 2›']  # the three 64-byte pushes from the witness
for i, (pk, sig) in enumerate(zip(pubkeys, sigs)):
    print(f'node {i}:', bip340_verify(bytes.fromhex(pk), digest, bytes.fromhex(sig)))
EOF
node 0: True
node 1: True
node 2: True

Replay the script on kascov. Paste your spend txid into the kascov.io search (network: testnet-10) and hit “⧉ replay this spend” — it runs your captured witness through the real TxScriptEngine and streams the opcode-by-opcode trace: the strike comparison, the blake2b, all three OpCheckSigFromStacks, the final verdict. The same trace is one curl away: curl -s https://kascov.io/data/testnet-10/debug/‹your spend txid› | jq .verdict.

7Variations — and what keeps nodes honest

Flip the gate: a liquidation trigger

The gate compares in one direction. Flipping one opcodeOpGreaterThanOrEqualOpLessThanOrEqual — turns the payout into a liquidation/stop trigger that releases only when the price falls to the strike. In the SDK that is one argument:

// release iff price ≥ strike (this guide):
price_gate_redeem_dir(&committee, strike_e8, Gate::AtOrAbove)
// release iff price ≤ strike (liquidations, stop conditions):
price_gate_redeem_dir(&committee, strike_e8, Gate::AtOrBelow)

Range settle

range_settle_redeem(&committee, low_e8, high_e8) releases only when low ≤ price ≤ high — two OpDup-compare-OpVerify blocks in front of the same committee tail, same witness shape. Options expiries, bounded payouts, "settle inside the band" contracts.

What keeps nodes honest: the equivocation bond

A price gate is only as good as the committee's honesty — so kaspulse's economic-security story is a covenant too. Alongside every round, a node signs a fixed 24-byte attestation record: blake2b(pair)[0..8] ‖ round_be ‖ mant_be. Sign two different prices for the same slot and those two records are a cryptographic proof of double-signing — and a bond covenant (covenant::bond::bond_redeem) lets anyone who holds that proof seize the node's bond, verified purely by Kaspa L1 script. Real records, which you can recompute (blake2b-256("KAS/USD") starts b84ad8389aa2ebb0…):

slot: pair-id ‖ round 42price (mant, big-endian)
rec1 ($0.029)b84ad8389aa2ebb0 000000000000002a00000000002c4020
rec2 ($0.058)b84ad8389aa2ebb0 000000000000002a0000000000588040

Same first 16 bytes, different last 8 → slashable. The script checks exactly four things (two OpCheckSigFromStacks, an OpSubstr slot-equality, an OpSubstr price-inequality) — which yields four cases, and only one of them slashes. This run is the local Kaspa script engine executing the real bond covenant:

▶ captured from a live run, July 2026
cargo run --bin slash --features onchain
bonded oracle node key: af36ed050dd1919b7c1aaefe586232c6b8ba9fb7b88fcbcd70ae64766f295163
[1] real equivocation (slot=KAS/USD#42, $0.029 vs $0.058) → bond SLASHED: true
[2] different rounds (honest update)          → NOT slashable: true
[3] same price twice (no conflict)            → NOT slashable: true
[4] forged 2nd sig (attacker's key)           → NOT slashable: true
✅ Script-enforced slashing WORKS. A node that double-signs a price loses its bond to
   whoever catches it — verified purely by Kaspa L1 (OpCat/OpSubstr/OpCheckSigFromStack),
   no committee and no governance.

And it ran on the real chain, not just the local engine: slash_live posts a real 3-TKAS bond on TN10 and then slashes it with a real double-signing proof — we slashed our own bond, on purpose. Reproduce it end-to-end (your own bond, your own fresh txids):

cargo run --bin slash_live --features onchain
🔒 BONDED — 3.00 TKAS posted by the node behind the slashing covenant
   tx ‹your bond txid›
⚠️  caught: the node signed KAS/USD round 42 as BOTH $0.029 AND $0.058 — equivocation.
💥 SLASHED — the bond was seized on-chain by proving the double-signing, verified by Kaspa L1.
   tx ‹your slash txid›

One honest limitation, stated in the code itself: the bond covenant ships its slashing path only — the honest node's reclaim-timelock branch is marked unproven in src/slash.rs, so the SDK does not expose it until it is proven. The operator's view of bonds, monitoring, and running a signer of your own: docs/OPERATOR.md in the repo (also linked from the docs hub).