Introduction
KasPulse serves live signed price feeds for dashboards, automation, research, and experimental Kaspa covenants. A full signed feed carries the exact message, public keys, signatures, freshness, and safety flags needed for verification. Token feeds include a KRON SDK cross-reference alongside kascov in the same response; this provider comparison is unsigned advisory metadata. Five signing keys exist and three valid signatures are required, but one operator controls all five today. The data API is live; price gates and slashing have only run on Kaspa testnet-10, and there is no mainnet key.
Two kinds of feed, and they are not the same product. Test kind against a set, never inline against one string — it has moved twice already, and each time every consumer comparing it inline stopped matching in silence. Two kinds ship today: "major" and "kcc20-pool". A consumer filtering for a retired kind matches nothing, which is the intended failure mode: it matches nothing loudly rather than pricing something wrong quietly. The full rename history, with dates: the changelog.
| kind | what the price is | read these |
| "major" | KAS · BTC · ETH — a MAD-filtered median across independent exchange venues. Read sources[] for which ones, this round | num_sources spread_bps outliers |
| "kcc20-pool" | Kaspa L1 KCC20 AMM pool covenants, read from kascov. An exact integer rational — the covenant's own gated marginal, or the newest verified pool state when that marginal is withheld — with the txid of the executed fill it is anchored to. Every admitted fill is re-run against the covenant's audited program in kaspulse's own code before it can move a price | basis last_trade_age_s anchor_txid price_source_url move_10pct_usd |
Retired kinds and the fields that went with them — including pool_age_s, venues[], peg_ok and the envelope’s peg block — are documented with their dates and the measurements behind them in the changelog. One wire-compatibility note you need today: peg_ok is still parsed by the API verification clients (their verify logic rejects peg_ok == false), and no feed sets it any more — so that check is permanently inert rather than removed, and an old client keeps working unchanged. Do not add it to new code.
All endpoints are GET (plus HEAD/OPTIONS); live data is served Cache-Control: no-store. Poll — there is deliberately no push stream.
# all signed feeds, one round — plus the L1 census and refusals
curl $ORIGIN/v1/feed
# one pair (dash form, case-insensitive)
curl $ORIGIN/v1/feed/KAS-USD
# an L1 KCC20 pair — 16-hex token prefix, two legs per market
curl $ORIGIN/v1/feed/KCC20.c58c826d0aa9cee6-USD
curl $ORIGIN/v1/feed/KCC20.c58c826d0aa9cee6-KAS
# light catalog for dashboards
curl $ORIGIN/v1/feeds
The signed message
The keystone of the whole API. For every feed, the committee signs the ASCII string
kaspulse/v2|PAIR|mant|expo|ts|round
Pair names and token labels are different layers. A major uses its familiar market pair, such as KAS/USD. A KCC20 pair is built from the token’s covenant ID — KCC20.<first 16 hex>/USD and …/KAS — so the signed machine identity never depends on mutable metadata. The full 64-hex token_covenant_id is always published and remains the canonical key. For people, listed_name/listed_ticker carry the KRON registry’s label for a listing kascov bound to that exact covenant — the label itself is the registry’s word, not an on-chain fact; claimed_* remains separate, self-asserted deployer metadata; display_name is the stable ID-derived fallback. Never key storage on any human label.
where PAIR is slash form (KAS/USD) and the rest are decimal integers (expo may be negative). The signed price is mant × 10^expo — exact at any magnitude. The digest is unkeyed blake2b-256 of those ASCII bytes, and each node’s signature is standard BIP340 Schnorr over that 32-byte digest as the message m (the digest is not hashed again outside BIP340’s own tagged hashing). signers[i] are 32-byte x-only pubkeys, index-paired with signatures[i].
Worked example, captured from a live round:
# the message string, verbatim from the feed:
kaspulse/v2|KAS/USD|295500000|-10|1783791092|183775
# its blake2b-256 digest (this is BIP340's m):
0f86dadc725bf4cc21900ab008ee1f9d75884935d9e6a0e4a48db2f4d8cda88d
A feed is VALID when ≥ threshold distinct signatures verify and the message’s PAIR|mant|expo|ts|round fields equal the JSON’s pair/mant/expo/signed_ts/signed_round — the field binding stops a lying server from serving values the signatures don’t cover. Every kaspulse verifier checks both.
// JS message-domain core; pin was authenticated outside this origin
digest = blake2b256(utf8(feed.message))
valid = countDistinct(i => pin.signers.includes(feed.signers[i]) &&
bip340_verify(feed.signers[i], digest, feed.signatures[i]))
ok = valid >= pin.threshold && fieldsBound(feed)
Use the JavaScript or Python API client below to perform these checks. Authenticate the committee through a separate trusted channel, persist that pin, and reject stale or halted prices before use.
Prices are signed on change plus a 5s heartbeat — signed_ts/signed_round belong to the signature, the envelope’s timestamp/round to the serve tick. Full grammar, normative and served from this origin: /docs/MESSAGE-FORMAT.md.
Zero-dependency clients
One file each, no installs, no keys. Both fetch a feed and verify it locally (distinct signatures, field binding, covenant binding, and safety flags), and both self-test their crypto core at load — a broken core refuses to verify rather than risk a fake ✓. The CLI command does not enforce a clock age or an independently stored committee pin; money-moving code must also call checkedValue/checked_value with a maximum age and verifyWithCommittee/verify_with_committee with a committee artifact persisted or compared outside the feed’s origin.
# JS — Node 18+ or the browser
curl -O $ORIGIN/clients/kaspulse.mjs
node kaspulse.mjs verify KAS/USD $ORIGIN
# Python 3.9+ — stdlib only
curl -O $ORIGIN/clients/kaspulse.py
python3 kaspulse.py verify KAS/USD $ORIGIN
# L1 KCC20: signature-verify AND re-derive every leg from kascov.
# Still zero dependencies — price_source_url is a plain HTTPS GET.
node kaspulse.mjs kcc20 $ORIGIN
python3 kaspulse.py kcc20 $ORIGIN # exit 1 if any leg does not reproduce
| JS | Python | what it does |
| k.kcc20Feeds() | k.kcc20_feeds() | the L1 feeds out of an envelope |
| k.kcc20Census() | k.kcc20_census() | the census + every refusal |
| k.kcc20Info(feed) | k.kcc20_info(feed) | pure: basis, anchor txid, gate counts, depth |
| await k.reDeriveKcc20(feed) | k.re_derive_kcc20(feed) | network: re-fetch kascov, decide the tier again in the client, demand the exact integer pair back |
These are API consumers, not the oracle backend. They verify signatures and signed-field binding. For L1 feeds, they can also re-fetch kascov and compare the exact rational. That is a cross-check of kascov, not independence from it. They do not replay the backend’s admission gates or independently recompute the exchange median. The same L1 comparison is available on token feed pages in your browser.
Full envelope
GET/v1/feed
Every feed, one round, one document — around half a megabyte at the current feed count, and it also carries the L1 census and every refusal. Permanent aliases: /api/feed, /feed.json. For dashboards, poll /v1/feeds instead.
// captured from a live round (feeds abridged)
{
"round": 4468857779, "timestamp": 1787542163,
"threshold": 3, "num_nodes": 5, "transport": "websocket",
"kcc20": { /* the L1 census + every refusal — see "refusals" below */ },
"feeds": [ /* FeedObj × N — see /v1/feed/{PAIR} */ ]
}
| field | type | meaning |
| round | u64 | serve-tick round counter |
| timestamp | u64 | serve-tick unix seconds |
| threshold | u32 | signatures required per feed (3) |
| num_nodes | u32 | committee size (5) |
| transport | str | how majors arrive ("websocket") |
| kcc20 | obj | the L1 tier's census and its refusals — how many markets kascov indexes, how many kaspulse prices, and every market it declined with the gate it failed. Full shape below |
| feeds | [FeedObj] | all feeds — full shape below |
Single feed
GET/v1/feed/{PAIR}
One FeedObj. PAIR is dash form, case-insensitive: KAS-USD ↔ pair "KAS/USD". Unknown pair → real HTTP 404 with body {"error":"no such feed"} (the legacy alias /api/feed/{PAIR} now 404s too).
curl $ORIGIN/v1/feed/KAS-USD
// the shape of one feed (arrays abridged; live values, moves every round)
{
"pair": "KAS/USD", "kind": "major",
"price": 0.02955, "price_e8": 2955000,
"mant": 295500000, "expo": -10,
"sources": [
{ "name": "Gate.io", "price": 0.02955, "age_ms": 12467 },
{ "name": "KuCoin", "price": 0.02955, "age_ms": 12890 },
{ "name": "MEXC", "price": 0.029564, "age_ms": 12279 }
],
"num_sources": 3, "outliers": [], "divergent": false,
"halted": false, "degraded": false, "freshest_ms": 12279,
"low": 0.02955, "high": 0.029564, "spread_bps": 4.74, "median": 0.02955,
// majors are an instantaneous median — never TWAPed:
"twap": false, "twap_samples": 0, "twap_window_s": 0,
// depth is a pool concept; majors are CEX-sourced, so it is null:
"liq_wkas": 0.0, "thin": false,
"move_10pct_usd": null, "depth_2pct_usd": null,
"signers": [ "0dd71bf25a98da9ff89a661537997fc9…", /* ×5 */ ],
"threshold": 3,
"signatures": [ "7eb9f2973d6976da3dc8bdc29d66e697…", /* ×5 */ ],
"message": "kaspulse/v2|KAS/USD|295500000|-10|1783791092|183775",
"signed_ts": 1783791092, "signed_round": 183775,
"history": [ [1783791030, 0.02955], /* ~120 [ts, price] points */ ]
}
An L1 KCC20 feed carries the same shape plus a provenance block. Captured live from mainnet, abridged — the full field table is in L1 fields:
{
"pair": "KCC20.7a98370c0d6b037c/USD", "kind": "kcc20-pool",
"price": 0.00023107896435551974, "mant": 231078964, "expo": -12,
"leg": "USD",
"token_covenant_id": "7a98370c0d6b037c28089d7b77cd8229120804cb416ef592ddb99e23820544e4",
"listed_name": "Kron Token", "listed_ticker": "KRON",
"listed_known": true, "listed_checks_passed": true, // KRON registry row; the checks cover the list's covenant/genesis/creator claims, not the name string
"display_name": "rapid-jade-narwhal", // deterministic fallback from the id
"claimed_ticker": null, "claimed_name": null, "claimed_decimals": null, // UNVERIFIED if present
"token_status": "verified", "holders": 123, "supply": 1000000000000000, // kascov's decode state / index counts
"art_url": null, // kascov-hosted hash-verified art, when the token has any
"price_num_sompi": 45207783000000, "price_den": 55795904,
"price_unit": "sompi per token base unit", // signed unit never applies advisory decimals
"basis": "last_verified_state",
"basis_note": "kascov withheld spot (t_live 55795904 != held_by_covenant 55780625);
using the newest non-carried 1h pool candle close",
"verified_as_of_ms": 1787541301267, "last_trade_age_s": 850,
"anchor_txid": "6a7ed22829b4a3ea9fd076e1190b313b0acfdf53567331d2d1afe33f99bed4f1",
"anchor_daa": 521209446,
"newest_admitted_txid": "6a7ed22829b4a3ea9fd076e1190b313b0acfdf53567331d2d1afe33f99bed4f1",
"stale_fill": false,
"admitted": 38, "rejected_bracket": 12, "rejected_dust": 0, "rejected_co_covenant": 0,
"gate_window": 50,
"gate_note": "re-ran bracket_holds+invariant_holds over the newest 50 fills at (30,30) bps",
"market_covenant_id": "10f2155ebcd2e0cd55cc9354f78d43fa4e8b9f93de98683919d1f490da726c4f",
"skeleton": "KRON pool v1", "invariant_ok": true,
"exercised_trades": 3089, "reserve_kas": 452201.7,
"trades_24h": 27, "volume_24h_sompi": 13432755000000, "change_24h_bps": -2933,
"window_note": null,
"move_10pct_usd": 600.18, "depth_2pct_usd": 127.07, "thin": false,
"taker_fee_bps": 130, "resting_asks": 0, "unpriced_reason": null,
"last_fill": { "quote_sompi": 99100000000, "base_amount": 122546, "side": "buy" },
"price_source_url": "https://kascov.io/data/mainnet/token/7a98…44e4/candles?bucket=1h&phase=pool"
}
Provider cross-reference on the same token feed
KCC20 full feeds and catalog rows also include cross_reference. It compares kascov and KRON SDK spot observations for the same full token and pool covenant IDs, normalized to KAS per token base unit, even on a USD feed. This is a comparison of the raw spot observations, not of KRON spot with the signed time-weighted price.
// illustrative cross_reference excerpt; identity and timestamps abridged
"cross_reference": {
"signed": false,
"price_unit": "KAS per token base unit",
"status": "agree",
"num_providers": 2,
"num_markets": 1,
"deviation_bps": 0,
"threshold_bps": 100,
"providers": [
{ "id": "kascov", "name": "kascov", "role": "oracle_basis",
"basis": "spot", "price_kas": 0.25, "verification": "replayed-fills" },
{ "id": "kron-sdk", "name": "KRON SDK", "role": "cross_reference",
"basis": "spot", "price_kas": 0.25, "verification": "provider-reported",
"node_comparison": "unknown" }
]
}
agree means the difference is at most 100 basis points (1%); disagree means a larger difference. unavailable means a fresh matching observation is missing, while not_comparable means the pricing bases cannot be fairly compared. A historical verified state is not labelled as agreeing with current spot. Honor observation timestamps and expiry in Unix milliseconds; the freshness window is at most 60 seconds, and a recent fetch does not prove the age of underlying chain state.
deviation_bps uses the symmetric difference 10000 × |a − b| / ((a + b) / 2), where a and b are the two base-unit spot prices. It is null unless the observations are comparable. Each provider row carries observed_at_ms; respect the comparison’s expires_at_ms. An unavailable provider has a null price, not zero.
Two providers, one market. Provider rows explain the two data paths; KRON is a cross-reference, not another independent pool or price vote. Existing sources, num_sources, the median, and signing quorum retain their meaning. The comparison is unsigned advisory metadata and cannot override halted, oracle admission, or signature-verification policy. KRON’s own matched/unknown node-comparison report is distinct from KasPulse’s price agreement; neither is independent chain proof.
The fields worth honoring. A consumer that reads price and ignores the flags is doing it wrong — the flags are the oracle telling you when not to trust the number:
| field | type | meaning |
| halted | bool | do not consume this round. Set by the circuit breaker (a >20% one-round jump publishes the last good price until it persists), on any pool feed whose TWAP window is less than half full, on any kcc20-pool feed whose newest verified fill is older than 48 h (or whose age cannot be established at all), and on every feed whose price is a KAS price × KAS/USD when KAS/USD itself is halted — that is the kcc20-pool /USD leg. The L1 /KAS leg does not inherit that halt: it is not denominated in dollars, so it keeps publishing |
| move_10pct_usd | f64|null | On any pool feed, the number to read. USD trade size that moves this feed’s price 10%: the sell-side honest form min((V+R)·(1−1/√1.1), R) × KAS/USD, capped at the live reserve, where V is virtual KAS (0 on every graduated pool) and R the live reserve in sompi — a factor of 0.04653741 at δ=0.10, sell side, no fee term. Do not compare it to a figure from the removed EVM tier without the offset: that one used the buy-side form with the 30 bps fee divided back in (0.04895572), so an L1 number reads 4.94% below its old L2 counterpart on an identical reserve — enough to flip thin on a pool sitting near the $250 bar. null on majors. It is a trade size, not a net cost: the attacker still faces arbitrage and must hold the move across the TWAP window |
| depth_2pct_usd | f64|null | same at 2% — the everyday-slippage number |
| thin | bool | move_10pct_usd < $250. Real price, shallow book, cheaply moved — honor it |
| taker_fee_bps | u32 | kcc20-pool only — the trader’s full outlay in bps: 130 on L1 (30 bps retained in the reserve plus two P2PK fee legs of 30 and 70 bps paid outside the pool). Only the in-reserve 30 bps enters the constant-product arithmetic, which is why the depth formula uses that figure and this one is published separately. POOL-SPEC’s own caveat carries over: fee-leg floors on very small swaps are unconfirmed |
| divergent | bool | ≥2 sources and spread_bps > 500 — the venues genuinely disagree and the published price is between them, i.e. one no venue quotes |
| degraded | bool | fewer venues than nominal — widen your margins |
| peg_ok | bool|absent | No feed sets this any more. It was the WKAS/USDC bridge check on the removed EVM tier, and there is no bridge in an L1 or a major price path. It is still accepted on the wire and still parsed by the API verification clients, whose verify logic rejects a feed carrying peg_ok == false — that check is inert rather than deleted, so an old client keeps working unchanged. Do not add it to new code |
| freshest_ms | u64 | age of the freshest venue read at serve time. On a pool feed this is how recently we polled kascov — not how recently the price moved. That is last_trade_age_s, and on a quiet market the gap between them is days |
| outliers | [str] | venues MAD-filtered out of this round’s median |
All of the above is UNSIGNED advisory metadata. The committee signs exactly kaspulse/v2|PAIR|mant|expo|ts|round — pair, price, time, round. The flags, the depth figures, the TWAP fields and the whole L1 provenance block sit next to the attestation, not inside it: a compromised server could change them without breaking a signature. They are not added to the signed message because v2 is frozen and every verifier demands exact field equality — binding them is a v3 job, and we would rather say that than imply a guarantee we don’t deliver.
Everything else:
| field | type | meaning |
| pair / kind | str | "KAS/USD" · one of "major" | "kcc20-pool". Match on a set, never inline against one string — this one has changed twice and a retired value now matches nothing. Dates: the changelog |
| mant / expo | u64 / i32 | the exact signed price: mant × 10^expo |
| price | f64 | convenience float of mant × 10^expo — display only |
| price_e8 | i64 | legacy convenience integer for display/compatibility only. It is not signed by cov/v2; current covenants gate on the signed mant + expo value |
| sources | [obj] | per-venue {name, price, age_ms} |
| num_sources | usize | venues in the median after filtering |
| low / high / spread_bps / median | f64 | venue range, spread (bps) and raw median this round. With one source, low == high == price and spread_bps is 0.00 — that is “no second venue”, not agreement |
| twap | bool | true only when this is a kcc20-pool feed and its window is full (12 samples). False on majors — they are instantaneous medians — and false on an L1 feed until 12 pool rounds have accumulated: after a restart, and again after any gap in the pool reads longer than 30 s, which clears the window rather than letting stale samples pose as a warm one. A pool round is SLOW_EVERY (5 s) plus however long the kascov read takes, so read the elapsed time off twap_window_s, not off a clock |
| twap_samples | u32 | samples actually in the window right now (0 on majors) |
| twap_window_s | u32 | Measured: the elapsed seconds between the oldest and the newest sample actually in the window of the venue that set the depth figures. Not twap_samples × 5 s — that nominal form was wrong in both directions, because a pool round takes as long as the RPCs take. This is also a staleness bound: the price you are reading is an average over the last twap_window_s seconds, so part of it is that old |
| liq_wkas | f64 | pool depth in KAS; 0 on majors. On kcc20-pool it is the live covenant reserve and equals reserve_kas — there is exactly one covenant behind an L1 market, so there is no deepest-vs-shallowest split to worry about. The name is a leftover from the removed EVM tier, where it really was WKAS; it is kept because /v1 is frozen, and there is no wrapped asset anywhere in an L1 price path. Prefer move_10pct_usd either way: KAS is a moving target in dollars |
| signers / signatures | [hex] | 5 x-only pubkeys / 5 BIP340 sigs, index-paired |
| threshold | u32 | signatures required (3) |
| message | str | the exact signed string — see “the signed message” |
| signed_ts / signed_round | u64 | what the signature covers (≠ envelope tick) |
| history | [[u64,f64]] | last ~120 [ts, price] points — the only history there is |
Catalog
GET/v1/feeds
The light board: one small row per pair, built once per round. This is what dashboards should poll. Catalog rows are not signed — verify a pair’s full feed before acting on it.
// the shape of a catalog row (rows abridged; live values, moves every round)
{
"round": 4468857779, "timestamp": 1787542163, "count": 17,
"feeds": [
{ "pair": "KAS/USD", "kind": "major", "price": 0.02852,
"num_sources": 3, "halted": false, "degraded": false, "thin": false,
"liq_wkas": 0.0, "spread_bps": 4.74, "freshest_ms": 12279,
"move_10pct_usd": null },
// an L1 row carries extra keys — which tier priced it, the age of
// the fill behind it, and enough IDENTITY to draw the row, so a board
// never has to open the envelope just to label a token
{ "pair": "KCC20.a73cdef004099b19/USD", "kind": "kcc20-pool", "price": 0.04547,
"num_sources": 1, "halted": false, "degraded": false, "thin": false,
"liq_wkas": 295663, "spread_bps": 0.0, "freshest_ms": 145,
"move_10pct_usd": 392.42,
"basis": "spot", "last_trade_age_s": 9212,
"token_covenant_id": "a73cdef004099b191759d320de970451be0e10423a7eb15b07d5e51d050b47cd",
"listed_name": "The Ultimate test", "listed_ticker": "PEPE",
"listed_known": true, "listed_checks_passed": true,
"display_name": "humble-teal-lemur", "token_status": "verified",
"holders": 121, "claimed_ticker": null, "listed_art_url": null, "art_url": null }
]
}
Catalog rows carry everything a board needs for first paint: price risk, trade age, full covenant ID, bound listed_* registry identity, separate unverified claimed_* identity, deterministic display_name, status, holders, and witnessed art URLs. Token rows also carry the same cross_reference provider comparison described under Single feed. A renderer should lead with a bound listing, label its provenance, retain the covenant ID as the key, and fall back to the deterministic name/avatar — never to a label it ships itself. count counts signed feeds; one L1 market contributes a USD and a KAS leg, so count markets from the USD legs. Catalog identity and context are unsigned advisory metadata.
The L1 KCC20 tier
Why it exists. A KIP-17 covenant can only introspect the transaction that spends it. It therefore cannot read an AMM pool’s reserves without trading against that pool — and it cannot see an exchange at all. On Kaspa L1 an attested off-chain price is the only mechanism. On an EVM L2 the same contract just calls getReserves(), which is why an oracle is structurally necessary here and merely convenient there. That is the whole argument for this tier, and it is not a liquidity argument — L1 is a small pile of KAS. The live table is on #/l1; the tier of EVM feeds this project deleted for exactly that reason, with the measurement behind it, is in the changelog.
Where the data comes from — stated once, plainly. kaspulse fetches L1 market data from kascov (the covenant explorer), re-runs the covenant’s own bracket and invariant arithmetic in its own code before publishing, and ships the txid the price is anchored to. That is not independence: a systematic decode bug upstream yields a self-consistent set of integers that passes the re-check. kascov and kaspulse are written and run by the same person, on the same box, behind the same Caddy — co-location does not reduce that gap, it only makes it legible. The honest claim is “we re-verify every admitted fill against the covenant’s audited program and publish the txid”, never “independent of kascov”. One more known drift risk, disclosed rather than hidden: the per-skeleton fee table kaspulse’s bracket check uses is a hand-copy of a private table in kascov (fee_in_bps is never served over HTTP), so it would go stale silently if kascov added a build family. kaspulse’s allowlist refuses unknown skeletons rather than guessing a fee.
Two tiers of price, and the feed says which one you got. Never a self-computed reserve ratio — that reads 22% wrong on a mainnet pool today, because someone donated 100,000,000 tokens into its covenant.
| basis | what it is | when |
| "spot" | kascov’s live gated marginal: (virtual KAS + live reserve) / tokens held, as an exact integer rational. Virtual KAS is 0 on every graduated pool | published only while the covenant’s live token balance still equals the after-balance of its newest verified trade — an anti-donation gate |
| "last_verified_state" | the newest non-carried 1h pool candle close, as the same {base_amount, quote_sompi} integer pair. A verified past pool state, not a live marginal | when that gate withholds spot. It fires silently upstream and can withhold spot from any pool, including the deepest. The live split is the census strip on #/l1 and the per-feed basis, never a number typed in here — the split has been anywhere from 1 of 7 to 6 of 7 within a single day, which is precisely why no count is written here. basis_note carries the reconstructed condition |
Verified on every pool that carries both: the tier-2 candle close is byte-identical to the tier-1 spot pair. Tier 2 is not an approximation of tier 1 — it is the same arithmetic on the newest state kascov will vouch for.
Identity has layers. The full token_covenant_id is canonical. The KRON registry supplies familiar listed_name/listed_ticker labels joined to that ID, plus a witnessed art route when available. What listed_checks_passed attests is narrow and worth reading twice: kascov matched the list’s testable statements — covenant program, genesis txid, creator key — against its own chain index. It did not verify the name or ticker string; those remain the list publisher’s words. Advisory metadata, not Kaspa consensus or an endorsement. claimed_* is deployer-supplied and remains visibly unverified. display_name and the avatar are deterministic ID-derived fallbacks. Preserve all layers; never collapse listed metadata into claimed metadata.
Units — read this before you scale anything. Every L1 price KasPulse publishes is sompi per token BASE UNIT, and price_unit says so. Registry and deployer decimals are presentation metadata; they are not in the signed unit. If you apply either scale, do it explicitly and record the source.
Composition. TOKEN/KAS is the TWAPed pool leg (the existing 12-sample window). TOKEN/USD is that leg × the guarded KAS/USD major — the product is never windowed, because the KAS leg is defended by five to seven exchange venues and the token leg by a few hundred dollars of pool. Consequence you must handle: the /USD leg inherits a KAS/USD halt and the /KAS leg does not.
L1 fields
Every field below is unsigned advisory metadata, exactly like the depth and TWAP fields on the other kinds: the v2 message covers pair|mant|expo|ts|round and nothing else. They are what makes the price re-derivable, not what attests to it. A compromised server could change any of them without breaking a signature — which is why price_source_url points at something you can curl instead of asking you to believe them.
| field | type | meaning |
| leg | str | "USD" or "KAS". Two feeds per market: PAIR already domain-separates the signed message, so cross-leg replay is impossible and a consumer sees which leg moved by diffing two attestations |
| token_covenant_id | hex64 | The canonical asset identity. The pair string carries only the first 16 hex; this is the whole thing. Key storage on this, never on a human name or ticker |
listed_name listed_ticker listed_decimals | str|null str|null u32|null | Human-readable metadata from the KRON registry, joined by exact covenant ID. Promote it only when both listed_known and listed_checks_passed are true, and label it as registry metadata rather than chain consensus |
listed_known listed_checks_passed listed_list_name | bool bool str|null | Registry provenance. listed_checks_passed means kascov’s structural/chain checks passed; it is not an investment endorsement |
| display_name | str | kascov’s deterministic name, e.g. "humble-crimson-tortoise". Derived from the covenant id, so it is stable and unspoofable — but it carries no chain authority either. Display it; do not key on it |
claimed_ticker claimed_name claimed_decimals | str|null str|null u32|null | UNVERIFIED deployer metadata. Anyone can claim anything. Keep it separate from listed_* and label it when shown |
token_status holders supply | str|null u64|null u64|null | token_status is kascov’s decode state for the covenant program, not an endorsement. Holder count and supply are indexed facts; supply remains an unscaled base-unit integer |
listed_art_url art_url | str|null str|null | Same-ID image routes served by kascov. Registry art is emitted only for a known, structurally passing row with a positive witness; deployer art must match its committed hash. Never hotlink an arbitrary third-party URL; fall back to the deterministic avatar |
price_num_sompi price_den | u128 u128 | The exact price, as an integer rational. The price float is this pair evaluated once, at the signing boundary. Divide them yourself and you have the number with no float in the path |
| price_unit | str | always "sompi per token base unit". See units, above |
| basis | str | "spot" | "last_verified_state" — which tier produced this price. Do not collapse them in your UI |
| basis_note | str | a short human string: on tier 2, kascov’s withhold condition with the two balances that disagree |
| verified_as_of_ms | u64 | ms timestamp of the pool state this price is derived from |
| last_trade_age_s | u64|null | The freshness number on an L1 feed — not freshest_ms, which is only how recently we polled. Seconds since the newest fill kaspulse replayed and admitted — an executed trade, not a reserve touch. It is also the age kascov will vouch for: a pool can have an unadmitted on-chain fill newer than this, and that is the honest reading, not a bug |
anchor_txid anchor_daa newest_admitted_txid | hex64|null u64|null hex64|null | the transaction the price is derived from, and its accepting DAA score. On basis: "last_verified_state" that is the candle’s own last_txid, which can be older than newest_admitted_txid: kascov memoizes the pools/trades documents and the candles document separately, so a fill can be admitted by kaspulse’s gate re-run before the candle carrying it is served. Stamping the newer fill onto the older rational would advertise a stale price as current, so the two are published apart. Caveat, stated: the upstream payload carries a txid but no accepting block hash, and stock kaspad has no txid lookup — so a historical fill is not checkable against an arbitrary node. Live pool state is |
admitted rejected_bracket rejected_dust rejected_co_covenant gate_window gate_note | u32 ×5, str | kaspulse’s own re-run of the gates over the newest gate_window fills, using its own copies of bracket_holds and invariant_holds — pure i128, no division, no float, reproducible by anyone with the same integers. rejected_bracket is the anti-donation gate firing: the executed price fell outside the marginals the program itself computed before and after the trade |
market_covenant_id skeleton program_hash invariant_ok | hex64, str, hex64, bool | the venue (a token can migrate curve→pool, so this is advisory and the token id is the identity), the recognised program family, and the replayed two-sided constant-product verdict. Do not pin on program_hash — the KRON program embeds its own mutable state block, so its hash changes on every trade; pinning on it means a permanently dark oracle. kaspulse pins on skeleton against its own allowlist and publishes the hash as evidence |
reserve_kas exercised_trades | f64, i64 | live covenant reserve and lifetime replay-verified fill count |
trades_24h volume_24h_sompi change_24h_bps window_note | u64|null, i128|null, i64|null, str|null | the 24h aggregates, and all three are null together when kascov refuses to re-verify the window — window_note then carries its per-pool reason (for example, “complete 24h history refused: historical market … names a different token”). null is not 0: change_24h_bps: 0 would assert the market was flat, and the truth is that the window was not established |
| resting_asks | u32 | always 0 today. The on-chain book is ask-only by schema and empty on every mainnet token, under its own provenance disclaimer that nothing in it is a quote. Published as market structure; it is not a price source and cannot yield a mid |
| last_fill | obj | {quote_sompi, base_amount, side} of the newest swap. This is the average fill of that swap, biased by its size and direction — measured against spot it runs from −8.6% to +3.3% on live pools, and the sign tracks side every time. Provenance only. Never a price. |
| indexer_lag_daa | u64|null | how far kascov’s index sits behind its own node’s tip, in DAA score. Read this next to last_trade_age_s, always. On its own a growing trade age is ambiguous — it means either a quiet market or a stalled indexer, and those call for opposite reactions. null means kascov’s /health did not answer, which is not the same as zero lag; a feed carrying null here is one whose freshness you cannot currently check |
| anchor_confirmations | u64|null | tip_daa − anchor_daa: how deeply buried the fill this price is anchored to is. Published rather than enforced, so you set your own reorg tolerance instead of inheriting kaspulse’s. null when the tip is unknown — again not zero |
| stale_fill | bool | true once last_trade_age_s passes 6 h. A flag, not a gate — the feed keeps publishing. Past 48 h it stops being a flag and halted is set instead |
| unpriced_reason | str|null | null on a healthy feed. Non-null means the price on this round has no provenance behind it — do not consume it |
| price_source_url | str | The exact request a third party curls to re-derive this price. The point of the whole block |
Quickstart — integrate an L1 KCC20 price
Ten minutes, no dependencies, and the last step is the one that matters: re-derive the number from kascov yourself and compare.
# 1 — find the markets. Filter kind, take the /USD legs (2 feeds per market).
curl -s $ORIGIN/v1/feeds | jq '.feeds[]
| select(.kind=="kcc20-pool" and (.pair|endswith("/USD")))
| {pair, price, basis, last_trade_age_s, move_10pct_usd, thin, halted}'
# 2 — take one feed. Everything you need to re-derive it is in the JSON.
curl -s $ORIGIN/v1/feed/KCC20.c58c826d0aa9cee6-USD | jq '{
price, price_num_sompi, price_den, price_unit,
basis, basis_note, last_trade_age_s, anchor_txid,
token_covenant_id, display_name, claimed_ticker,
move_10pct_usd, thin, halted, price_source_url }'
# 3 — VERIFY THE SIGNATURES. One file, zero deps. Never skip this.
node kaspulse.mjs verify KCC20.c58c826d0aa9cee6/USD $ORIGIN
# 4 — RE-DERIVE THE PRICE from the upstream source, without trusting us.
# price_source_url is the exact request kaspulse read this round.
curl -s "$(curl -s $ORIGIN/v1/feed/KCC20.c58c826d0aa9cee6-USD | jq -r .price_source_url)"
# ...then check num/den against that document, and the anchor txid on an explorer.
# Or use the API client's exact-rational comparison (not a gate replay):
node kaspulse.mjs kcc20 $ORIGIN
Then honor five things, in this order. halted — do not consume, and remember the /USD leg inherits a KAS/USD halt while the /KAS leg does not. unpriced_reason — non-null means no provenance this round. basis — decide for yourself whether last_verified_state is good enough for what you are doing. last_trade_age_s — set your own line; kaspulse publishes stale_fill: true above 6 h and sets halted above 48 h (enforced by the hosted service). move_10pct_usd — set your own line rather than trusting the thin boolean, which is only < $250.
// JS — the same shape as any other feed, plus the two L1 gates
const f = await k.feed('KCC20.c58c826d0aa9cee6/USD');
if (f.pair !== 'KCC20.c58c826d0aa9cee6/USD') throw new Error('unexpected pair');
if (!k.verifyWithCommittee(f, persistedPin).ok) throw new Error('identity or signatures');
if (f.halted || f.unpriced_reason) throw new Error('do not consume');
if (f.last_trade_age_s > 6*3600) console.warn('stale fill', f.basis);
if (f.move_10pct_usd < myFloor) console.warn('thin for my size');
// exact, no float in the path — sompi per token BASE UNIT
const exact = BigInt(f.price_num_sompi) * 10n**18n / BigInt(f.price_den);
Two traps if you put this price behind a covenant. These limits are explained in the integration guide.
Refusals — kcc20 on the envelope
kascov indexes far more mainnet markets than KasPulse admits to its signed oracle, so every market the oracle declines is published with the gate it failed, in the envelope’s kcc20 block. A market is skipped when it is an LP-share token (an LP share is not a price), when its discovery_state is unrecognized or unrevealed, when its skeleton is outside kaspulse’s allowlist, when invariant_ok is false, when it has fewer than three replay-verified fills, or when kaspulse’s own gate re-run rejects it. A KRON SDK observation or provider agreement does not change admission to the signed oracle.
curl -s $ORIGIN/v1/feed | jq '.kcc20 | {network, source, pools_total, markets_total,
priced, skipped_count, taker_fee_bps, kascov_kas_usd, disagreement_bps}'
# group the refusals by reason:
curl -s $ORIGIN/v1/feed | jq -r '.kcc20.skipped[].unpriced_reason' | sort | uniq -c | sort -rn
| field | type | meaning |
| network / source | str | "mainnet", and the public kascov base a third party can curl. An upstream API or network outage can interrupt the token tier. These values identify the data source; they are not configuration instructions or independent evidence of availability. |
pools_total markets_total priced skipped_count | usize ×4 | the census: graduated pools upstream, all markets upstream, feeds kaspulse published, markets it refused |
| snapshot_ms / census_ms | u64 | when the priced snapshot and the (slower) full-market census were taken |
| taker_fee_bps | u32 | 130 — see the per-feed field |
kascov_kas_usd disagreement_bps | f64|null | Advisory only, and read kascov_kas_usd_source before reading the gap. kascov serves either its own single unsigned exchange read ("kraken"/"coingecko") or kaspulse’s own signed price ("kaspulse") once its hop to this oracle succeeds. On the latter, disagreement_bps is null: comparing this feed to itself is not a cross-check, and printing a ≈0 bps agreement would be a fabricated one. disagreement_note says which case you are in. Never fed into kaspulse’s MAD filter either way — that would double-weight a venue kaspulse already reads directly, or close the loop for real |
| indexer | obj | {status, lag_daa, tip_daa, last_sync_ok_ms, source, note} from kascov’s own /health — the only place upstream that serves a tip. Without it a stalled indexer and a quiet market are the same observation on the wire: both just show last_trade_age_s climbing. Every field is null when /health did not answer, and null is not zero: an unanswered health check cannot support the claim “perfectly in sync” |
| skipped | [obj] | {market_covenant_id, token_covenant_id, display_name, source, unpriced_reason} per refused market |
Rendered, with every refusal grouped and expandable, on #/l1.
Committee
GET/v1/committee
The five signer pubkeys, the threshold, and the two signing-domain tags — the material a consumer pins. Both zero-dep clients and the guide’s pin workflow read this endpoint once, out of band, then persist the result. It is served by the same origin as the feeds, so on its own it proves transport consistency, not oracle identity: compare or store it through a second channel before trusting it with money.
curl $ORIGIN/v1/committee > committee-pin.json
// captured live; signers[] are 32-byte x-only pubkeys, hex
{
"threshold": 3, "num_nodes": 5,
"signers": [ "0dd71bf2…086b8f4a", /* ×5 */ ],
"message": "kaspulse/v2",
"covenant": "kaspulse/cov/v2 — bound preimage: tag(15) ‖ blake2b256(PAIR)[0..8] ‖ expo(i8) ‖ round(u64 BE) ‖ ts(u64 BE) ‖ mant(minimal-LE), …",
"updated_ts": 1788010821
}
updated_ts moves when the committee document is regenerated, not when keys change — the signers have been unchanged since 2026-07-27, and any future rotation will be a dated changelog entry, never a silent swap.
Health
GET/health
Liveness for uptime checks: status 200 when ok, 503 when not. ok := build_age_ms < 5000 && feeds_total ≥ 1 && feeds_live ≥ 1.
curl $ORIGIN/health
# → {"ok":true,"round":4468857779,"uptime_s":86432,"build_age_ms":412,
# "feeds_total":17,"feeds_live":17,"pools":7}
Share pages & OG cards
GET/share/{PAIR}
A crawler-visible page with OpenGraph meta (title, description, live price card) that redirects humans to the SPA’s #/feed/{PAIR}. Paste it in a chat and it unfurls with a live price card. Unknown pair → 404.
GET/og/{PAIR}.png
The 1200×630 card itself: pair, live price, sparkline, the trust line and thin/halted badges rendered on the card. Only served by builds with the og feature (the deployed image); otherwise 404. Cached ~60s.
L1 pairs work here too — the pair name was deliberately built from the charset these routes accept ([A-Za-z0-9-._]), which is the reason it is KCC20.<16hex> and not the colon-separated form the design first reached for. Dash form is case-insensitive, so KCC20.c58c826d0aa9cee6-USD and its uppercase spelling are the same card.
curl $ORIGIN/share/KAS-USD
curl -o card.png $ORIGIN/og/KAS-USD.png
# an L1 KCC20 market
curl $ORIGIN/share/KCC20.c58c826d0aa9cee6-USD