Overview
The indexer follows Kaspa's chain tip live, classifies every covenant event (born / moved / retired), decodes spend-time script reveals, and serves the whole index as static JSON from a CDN. Reads are cheap and cacheable — poll as often as you like.
Networks
Every path takes a network segment. Swap testnet-10 for mainnet anywhere. Mainnet's first smart coins arrived July 2, 2026; their full history starts there.
Client libraries
Two tiny zero-dependency clients ship in the repo — clients/js/kascov.mjs (Node 18+ and browsers) and clients/py/kascov.py. Each is a single file wrapping the endpoints below. One sharp edge: for the event stream, construct the client with the worker origin as its base — new Kascov('testnet-10', 'https://kascov-worker-12056584181.europe-west4.run.app') — because the default kascov.io base sits behind a CDN that buffers SSE. Everything else works fine through the default.
GET/data/{network}-live.json
The small, fast feed — network stats, the chain tip, and the ~150 newest life events. This is the one to poll (a few KB, refreshes every few seconds).
Request
curl -s https://kascov.io/data/testnet-10-live.json
Response · 200
{
"network": "testnet-10",
"stats": { "covenants": 192852, "active": 14190, "burned": 178662,
"events": 876383, "live_value": 307204412838957,
"last_activity_daa": 513875477 },
"tip_daa": 513875491,
"tip_at_ms": 1783746272524,
"processed_daa": 513875490,
"generated_at_ms": 1783746272524,
"recent_events": [
{ "covenant_id": "2ab7dcd2…35115",
"kind": "transition", "txid": "f9bbca…f2ba",
"accepting_daa": 513875477, "seq": 5, "tx_index": 50 }
]
}
| Field | Type | Notes |
| stats.covenants | int | distinct smart coins ever seen |
| stats.active | int | coins with a live UTXO right now |
| tip_daa · processed_daa | int | chain tip vs. last block applied — the gap is the honest sync lag |
| recent_events[].kind | string | genesis · transition · burn |
| recent_events[].seq | int | the event's 0-based position in its own coin's timeline |
| recent_events[].tx_index | int? | 0-based index in the accepting block's accepted-tx list (consensus acceptance order); omitted on rows indexed before capture |
GET/data/{network}.json?after_daa=&after_id=&limit=
The grid — stats plus one summary row per covenant, ordered by last activity, newest first. A bare request is a first page capped at 20,000 rows; when more remain the response carries next_after_daa + next_after_id — pass them back to keep walking.
| Query | Type | Notes |
| after_daa | int | exclusive cursor — resume below this last_activity_daa |
| after_id | hex | tie-breaker within the boundary DAA (the last row's covenant_id) |
| limit | int | page size, default 5000, capped at 20000; invalid values degrade to the bare first page |
Response · 200
{
"stats": { … },
"covenants": [
{ "covenant_id": "e55322e7…60697", "name": "sleepy-olive-crane",
"status": "active", "born_value": 460831908462, "live_value": 230415954231,
"event_count": 2, "live_utxos": 1, "lineage_complete": true,
"genesis_daa": 513863444, "last_activity_daa": 513870198,
"template": "p2pk state" }
],
"next_after_daa": 513870178,
"next_after_id": "a348a978…f3a29"
}
| Field | Type | Notes |
| name | string | the coin's friendly name — derived deterministically from the id, the same name everywhere kascov shows it |
| status | string | active or burned |
| born_value · live_value | int | sompi held at birth / right now |
| last_activity_daa | int | the DAA of the coin's newest life event — the sort key of this feed |
| template | string? | recognized contract, or a state shape like p2sh commitment |
| next_after_daa · next_after_id | int? · hex? | present only when another page exists — feed them to ?after_daa=&after_id= |
GET/data/{network}/events?after_daa=&after_seq=&limit=
The chain-wide event feed — every life event on the network, oldest first, in the canonical deterministic order: accepting_daa, then tx_index, then txid. Every event-shaped surface (this feed, the live feed, the stream, webhooks) serves the same object, so you write one parser. Walk history from any point by passing the response's cursor pair back.
| Query | Type | Notes |
| after_daa | int | resume at this DAA — 0 starts from the beginning of history |
| after_seq | int | how many events of that boundary DAA were already taken — pass the returned pair straight back; a stable cursor because order inside a DAA is total and history only appends at higher DAAs |
| limit | int | page size, clamped server-side |
Response · 200
{
"events": [
{ "covenant_id": "2ab7dcd2…35115", "seq": 5, "kind": "transition",
"txid": "f9bbca…f2ba", "accepting_daa": 513875477,
"accepting_block": "58ef8d…c44f0", "tx_index": 50,
"payload_len": 137 }
],
"next_after_daa": 513875477,
"next_after_seq": 12
}
| Field | Type | Notes |
| events[].seq | int | 0-based position in the event's own coin's timeline |
| events[].tx_index | int? | consensus acceptance order within the accepting block; omitted (not null) on rows indexed before capture |
| events[].payload_len | int? | byte length of the transaction's v1 payload; omitted when it carried none |
| next_after_daa · next_after_seq | int | the resume cursor — feed to ?after_daa=&after_seq= to keep walking |
GET/data/{network}/stream?covenant=
Server-sent events — one JSON message the moment the indexer sees each life event. Connect to the worker origin directly: the kascov.io CDN (Firebase) buffers streams, so an SSE connection opened there never delivers. The stream opens with a : connected hello and sends a : ka keep-alive comment every 25s. Treat messages as hints and confirm through the polled feeds.
| Query | Type | Notes |
| covenant | hex | optional — exactly 64 hex characters narrows the stream to one coin's events; anything else is a 400, never a silent wildcard |
Request — note the worker origin
curl -N 'https://kascov-worker-12056584181.europe-west4.run.app/data/testnet-10/stream'
The stream
: connected
data: {"covenant_id":"2ab7dcd2…","kind":"transition",
"txid":"f9bbca…","accepting_daa":513875477,
"tx_index":50}
| Field | Type | Notes |
| kind | string | genesis · transition · burn — plus reorg notices {"kind":"reorg","rolled_back":N} (filtered streams never see them) |
GET/data/price.json
The KAS→USD rate behind the site's ≈ $ hints — one tiny object, no network segment. Answers 503 while no fresh rate is available; treat any non-200 as "no price right now".
Request
curl -s https://kascov.io/data/price.json
Response · 200
{
"kas_usd": 0.1132,
"updated_at_ms": 1751799000000,
"source": "median of spot venues"
}
| Field | Type | Notes |
| kas_usd | number | US dollars per 1 KAS |
| updated_at_ms | int | when the rate was last refreshed (unix ms) |
| source | string | where the rate came from |
GET/feed.xml
What’s new, as an Atom feed — the same milestones as the changelog, one entry each with a stable id, newest first. Lives on the site root next to /sitemap.xml (no network segment), cached for an hour. Point any feed reader at it. Served by newer workers; older deploys answer 404.
Request
curl -s https://kascov.io/feed.xml
GET/data/{network}/c/{covenant_id}.json
One coin's whole life — the complete event timeline and every UTXO with decoded scripts, recognized templates, and spend-time reveals. 404 if the id is unknown.
Response · 200
{
"covenant_id": "a348a978…f3a29", "name": "brave-jade-osprey",
"status": "burned", "event_count": 3, "lineage_complete": true,
"genesis_txid": "82aabd…25dcb", "genesis_daa": 513863030,
"last_activity_daa": 513870178,
"events": [
{ "seq": 0, "kind": "genesis", "txid": "82aabd…25dcb",
"accepting_daa": 513863030, "accepting_block": "bc5aaf…8daac",
"tx_index": 56 },
{ "seq": 2, "kind": "burn", "txid": "8e7040…3e2bb",
"accepting_daa": 513870178, "accepting_block": "3d1c00…45e7e2",
"tx_index": 18 }
],
"events_truncated": false,
"utxos": [
{ "outpoint": "82aabd…25dcb:0", "value": 230415954231, "live": false,
"template": "p2sh commitment",
"state_fields": [ { "name": "program_hash", "value": "d9a9e982…f475d17" } ],
"revealed_template": "SilverScript · Escrow",
"revealed_fields": [ { "name": "arbiter_hash", "value": "f4445661…" } ] }
]
}
| Field | Type | Notes |
| name | string | the coin's friendly name — deterministic from the id |
| lineage_complete | bool | false means kascov started watching mid-life; earlier history is honestly missing |
| events_truncated | bool | true when the timeline was capped — event_count still counts everything |
| events[].payload · payload_len | hex? · int? | the tx's v1 payload — inlined as hex when ≤ 512 bytes, collapsed to its byte length above that |
| events[].with_covenants | array? | up to 4 other coins the same transaction moved (multi-contract flows) |
| utxos[].revealed_template | string? | the contract a P2SH state actually ran, decoded and hash-verified when it was spent |
| …_fields[] | array | { name, value } — labeled constructor arguments |
| holders[] | array | up to 100 p2pk-state owners: { pubkey, controls_now, states_seen, first_seen_daa, last_seen_daa } |
| utxos[].zk_system | string? | on zk-using states, a size heuristic over the proof push: risc0 (≥1024 B) or groth16 (128–256 B) |
| utxos[].revealed_zk_system | string? | the same heuristic applied to the program revealed at spend |
GET/data/{network}/coins?ids=
Batch summaries — several coins in one request, each row in the exact shape the snapshot grid serves. ids is comma-separated 64-hex covenant ids; ids the index doesn't know are simply absent from the answer, never an error.
Request
curl -s 'https://kascov.io/data/testnet-10/coins?ids=e55322e7…,a348a978…'
Response · 200
{
"coins": [
{ "covenant_id": "e55322e7…60697", "name": "sleepy-olive-crane",
"status": "active", "last_activity_daa": 513870198, … },
{ "covenant_id": "a348a978…f3a29", "name": "brave-jade-osprey",
"status": "burned", "last_activity_daa": 513870178, … }
]
}
GET/data/{network}/tx/{txid}.json
What did this transaction do, in covenant terms? Newer workers answer the whole story — the per-covenant events, the cells created and spent, and the decoded token actions — behind the site's #/{network}/tx/{txid} pages. Older workers answer only covenant_id / covenant_ids; every added field is additive, so feature-detect and fall back gracefully.
Response · 200
{
"txid": "8e7040…3e2bb",
"covenant_id": "b11acf6c…459c6",
"covenant_ids": [ "b11acf6c…459c6" ],
"accepting_daa": 513870178, "accepting_block": "3d1c00…45e7e2",
"events": [
{ "covenant_id": "b11acf6c…459c6", "name": "brave-jade-osprey",
"seq": 2, "kind": "burn", "tx_index": 18 }
],
"cells": {
"created": [ { "covenant_id": "…", "index": 0,
"value": 230415954231, "template": "p2sh commitment" } ],
"spent": [ { "covenant_id": "…", "txid": "82aabd…25dcb", "index": 0,
"value": 230415954231,
"revealed_template": "SilverScript · Escrow",
"has_witness": true } ]
},
"token_actions": [
{ "token_id": "9cc4fb51…68f50", "name": "humble-crimson-magpie",
"token_kind": "transfer", "amount": 1000000 }
]
}
Not covenant traffic · 404
{ "error": "not found" }
| Field | Type | Notes |
| covenant_id · covenant_ids | hex · array | the coin(s) this transaction touched — the only fields every worker serves |
| accepting_daa · accepting_block | int? · hex? | newer workers — where the chain accepted this transaction |
| events[] | array? | newer workers — one row per covenant life event: covenant_id, name, seq, kind (genesis · transition · burn), tx_index? |
| cells.created[] | array? | newer workers — covenant state this transaction made: covenant_id, output index, value (sompi), template? |
| cells.spent[] | array? | newer workers — covenant state it consumed: the outpoint's txid + index, value, revealed_template?, and has_witness (the captured unlocking script — replayable through the spend debugger) |
| token_actions[] | array? | newer workers — the decoded token view: token_id, name, token_kind (mint · transfer · burn), amount? in raw token units |
GET/data/{network}/addr/{address-or-pubkey}.json
Which smart coins has this address touched? Accepts kaspa:… / kaspatest:… or raw 32/33-byte pubkey hex. Matches p2pk covenant states.
Response · 200
{
"address": "kaspatest:qp9…hzy0",
"pubkey": "4b3402be…e9f2",
"covenants": [
{ "covenant_id": "…", "name": "…", "role": "controls_now" }
]
}
| Field | Type | Notes |
| covenants[].role | string | controls_now · states_seen |
GET/data/{network}/digest.json
The last 24 hours in one object — births, moves, burns, value born, the busiest coin, the biggest birth, and how many coins are alive. Refreshes about once a minute.
Response · 200
{
"births": 24762, "moves": 25948, "burns": 55074,
"value_born": 4831200000000, "alive": 6566,
"busiest": { "covenant_id": "…", "name": "curious-jade-zebra-6366", "events": 1185 },
"biggest_birth": { "covenant_id": "…", "name": "rapid-violet-seal-b788", "value": 937465200000000 }
}
GET/data/{network}/templates.json
What runs on this network — recognized script templates aggregated over every state UTXO. Unrecognized scripts are counted, never hidden.
Response · 200
{
"templates": [
{ "name": "p2sh commitment", "live_states": 6297, "live_value": 62970000000,
"ever_seen": 14459, "covenants": 5990, "revealed_runs": 3 }
],
"unrecognized": { "ever_seen": 12, "live_states": 4 }
}
| Field | Type | Notes |
| revealed_runs | int | compiled contracts proven at spend time; a tx sweeping N states counts N |
GET/data/{network}/activity.json?range=24h
The histogram behind the activity chart — life-event counts per DAA bucket. Empty buckets are omitted; treat them as zeros.
| Query | Type | Notes |
| range | enum | 1h · 6h · 24h · 48h · all (default 24h) |
Response · 200
{
"range": "24h", "bucket_daa": 6000,
"buckets": [ { "daa": 509540000, "births": 812, "moves": 903, "burns": 1740 } ]
}
GET/data/{network}/families.json
Covenant apps — clusters of coins that moved together in a transaction (multi-contract flows), union-found over shared txids. Each family lists its members with their recognized template.
Response · 200
{
"families": [
{ "size": 2, "members": [
{ "covenant_id": "09ef275e…", "template": "p2sh commitment", "shared_txs": 8 },
{ "covenant_id": "901be291…", "template": "p2sh commitment", "shared_txs": 8 }
] }
]
}
GET/data/{network}/galaxy.json?fmt=2&tier=core
The whole-network app graph behind the galaxy map — precomputed positions, weighted edges, and status for every smart coin. Built server-side and kept warm, so even a 168k-coin network answers instantly.
| Query | Type | Notes |
| fmt | enum | 2 — columnar arrays instead of one object per node (much smaller payload) |
| tier | enum | core — the big clusters only, for a fast first paint; fetch the full graph after |
Unknown params and unknown values degrade to the legacy full-object shape — old clients keep working unchanged.
GET/data/{network}/lanes.json
Based-app traffic by namespace — KIP-21 lanes, 4-byte app tags, and the JSON-inscription bucket, sorted by event count. Printable tags get their ASCII label; the rest show as 0x….
Response · 200
{
"tip_daa": 509571904,
"lanes": [
{ "label": "kspr", "hex": "6b737072", "ascii": true,
"kind": "tag", "events": 40312, "covenants": 6120 },
{ "label": "JSON inscriptions", "hex": null, "ascii": false,
"kind": "inscription", "events": 8123, "covenants": 950 }
]
}
| Field | Type | Notes |
| lanes[].kind | string | lane (KIP-21 namespace) · tag (4-byte app tag) · inscription (JSON payloads) |
| lanes[].hex | hex? | the raw 4-byte namespace; null for the JSON-inscription bucket |
GET/data/{network}/lane/{ns}
One namespace's dashboard — headline counts, the 50 newest events, and an hourly activity series over the lane's whole life. {ns} is the 4-byte tag as exactly 8 hex characters — anything else is a 400.
Request
curl -s https://kascov.io/data/testnet-10/lane/6b737072
Response · 200
{
"namespace": "6b737072",
"events": 40312, "covenants": 6120,
"recent": [ { "covenant_id": "…", "txid": "…",
"accepting_daa": 509571333, "kind": "transition" } ],
"activity": [ { "daa": 509540000, "count": 812 } ],
"bucket_daa": 36000
}
| Field | Type | Notes |
| bucket_daa | int | bucket width of the activity series — 36,000 DAA ≈ 1 hour |
GET/data/{network}/inscriptions.json
The JSON-inscription breakdown — events and distinct coins per decoded label (KRC-20 ops and friends).
Response · 200
{
"inscriptions": [
{ "label": "krc-20 mint", "events": 5123, "covenants": 640 }
]
}
GET/data/{network}/tokens.json
The covenant-token directory behind the tokens page — every KCC20-shaped coin the indexer decoded, its ticker and fields read straight from the chain's bytes, plus a conservative validation verdict per token: status is verified only when every event in the token's history matched a known KCC20 rule and supply is conserved; anything unknown or ambiguous stays unvalidated with a reason — never a false "valid". The response explains itself in note — surface it to your users too. Older workers answer the pre-validation shape (status is the coin's active/burned liveness, no supply fields) — treat those tokens as unvalidated.
Response · 200
{
"note": "validated from chain — verified means every event matched …",
"tip_daa": 513875491, "tip_at_ms": 1783746272524,
"tokens": [
{ "covenant_id": "9cc4fb51…68f50", "name": "humble-crimson-magpie",
"template": "KCC20 token",
"fields": { "tick": "NACHO", "op": "deploy" },
"live_value": 100000000,
"status": "verified",
"supply": 2100000000, "minted": 2100000000, "burned": 0,
"holders": 12,
"last_activity_daa": 513875477 }
]
}
| Field | Type | Notes |
| tokens[].fields | object | the decoded key/values exactly as the covenant wrote them — no registry, no labels kascov invented |
| tokens[].live_value | int? | sompi the coin holds right now |
| tokens[].status | string | verified · invalid · unvalidated — conservative: verified means every event in the history matched a known rule |
| tokens[].invalid_reason | string? | why the verdict isn't verified — present on invalid (and unvalidated) tokens |
| tokens[].supply | int? | live supply in raw token units — minted − burned |
| tokens[].minted / burned | int? | lifetime totals in raw token units |
| tokens[].holders | int? | owners with a non-zero balance right now |
GET/data/{network}/token/{covenant_id}
One token's whole story behind its token page — the directory row, the top 100 balances, every event classified as mint / transfer / burn with amounts and owners, and the validation verdict. Same conservative contract as the directory: a token is verified only when every checked event matched a known KCC20 rule. 404 when the id isn't a token this worker knows (or the worker predates token pages).
Response · 200
{
"token": { "covenant_id": "9cc4fb51…68f50",
"name": "humble-crimson-magpie", "template": "KCC20 token",
"fields": { "tick": "NACHO" }, "status": "verified",
"supply": 2100000000, "minted": 2100000000, "burned": 0,
"holders": 12, "last_activity_daa": 513875477 },
"balances": [ { "owner": "3f9a…c221", "balance": 1250000000 } ],
"events": [ { "txid": "…", "accepting_daa": 513875477,
"kind": "transition", "token_kind": "mint",
"amount": 1000000, "owner_to": "3f9a…c221" } ],
"validation": { "checked": 266 }
}
| Field | Type | Notes |
| token | object | the same row the directory serves for this token |
| balances | array | the top 100 balances, largest first — owner is the controlling pubkey, balance is raw token units |
| events[].token_kind | string | mint · transfer · burn — the canonical event object plus the token classification, amount and owner_from/owner_to |
| validation | object | the verdict's working: checked events walked; reason says what stopped a verified verdict when the status isn't verified |
GET/data/{network}/lifespans.json
How long do smart coins live? A bucketed lifespan histogram over every retired coin, plus the median. Times derive from DAA at 10 per second, so median_ms is an estimate.
Response · 200
{
"buckets": [ { "label": "< 1 min", "count": 41230 } ],
"median_daa": 1840, "median_ms": 184000, "total": 83181
}
GET/data/{network}/reorgs.json
The indexer's own reorg ledger — the last 500 virtual-chain reorgs it applied, newest first. Every entry is a rewind the index actually performed, not a heuristic.
Response · 200
{
"reorgs": [ { "daa": 509571904, "at_ms": 1751799000000, "rolled_back": 2 } ]
}
GET/data/{network}/consistency.json
An automated cross-check between independent ecosystem indexers — kascov's decoded token set compared against another indexer's, token by token. Differences usually mean one of us has a bug; we fix ours and share theirs kindly. Served by newer workers; older ones 404.
Response · 200
{
"network": "mainnet", "checked_at_ms": 1783948200000,
"our_tip_daa": 513875477, "other_source": "…",
"other_blue_score": 513875470,
"tokens_ours": 270, "tokens_other": 268, "intersection": 265,
"agree": 261, "differ": 4,
"only_kascov": 5, "only_other": 3, "not_comparable": 0,
"details": [ { "token_id": "…", "field": "supply",
"ours": 2100000000, "theirs": 2099999000 } ],
"note": "tips differ — small disagreements near the tip are expected"
}
| Field | Type | Notes |
| network · checked_at_ms | string · int | which network was compared, and when the check ran |
| our_tip_daa | int | kascov's chain tip at check time |
| other_source · other_blue_score | string · int | the other indexer's identity and its chain position — two indexers at different tips can honestly disagree |
| tokens_ours · tokens_other · intersection | int | how many tokens each side knows, and how many both do |
| agree · differ | int | of the intersection: tokens whose compared fields match, and tokens where they don't |
| only_kascov · only_other | int | tokens one side has indexed that the other hasn't (yet) |
| not_comparable | int | tokens skipped because the two sides expose incompatible shapes for them |
| details[] | array | the actual disagreements, per token — what we say, what they say |
| note | string | plain-language context for reading the numbers |
GET/data/{network}/search?q=&limit=
Find a coin — by covenant-id hex prefix (4+ characters), friendly-name prefix, or template substring. Each hit says which way it matched.
| Query | Type | Notes |
| q | string | required, 1–64 characters; 400 otherwise |
| limit | int | max results, default 10, clamped |
Request
curl -s 'https://kascov.io/data/testnet-10/search?q=mece'
Response · 200
{
"query": "mece",
"results": [
{ "id": "b4ade48e…", "name": "brave-amber-lynx-b4ad",
"template": "SilverScript · Mecenas",
"status": "active", "matched": "template" }
]
}
| Field | Type | Notes |
| results[].matched | string | id · name · template |
GET/data/{network}/debug/{txid}
Replay a real on-chain covenant spend through the actual script engine, with the witness captured at index time — a concrete per-opcode trace of what really ran. The transaction context is fabricated, so signature/introspection checks may diverge from the original; the response says so in note.
Response · 200
{
"ok": true, "pass": true, "verdict": "script accepted",
"covenant_id": "…", "outpoint": { "txid": "…", "index": 0 },
"value": 500000000,
"trace": [ { "op": "OpBlake2b", "dstack": ["…"], "astack": [] } ],
"trace_truncated": false,
"note": "replayed with a fabricated tx context"
}
| Field | Type | Notes |
| ok | bool | false when the txid spent no tracked state, or no witness was captured (reason says which) |
| trace_truncated | bool | the trace is capped at 2,000 opcodes |
GET/share/{network}/{covenant_id}
The crawler-visible page for one coin (the SPA is hash-routed, so scrapers never see #/… urls) — OG/Twitter meta plus a redirect into the app for humans. Its picture is a server-rendered PNG card, and /sitemap.xml lists every share page so search engines find every coin. These live on the site root, not under /data.
The card image
GET /og/{network}/{covenant_id}.png
→ 1200×630 Open Graph card (name, avatar,
holdings, story), rendered on demand
The index
GET /sitemap.xml
→ every coin's /share/… url
POST/data/{network}/simulate
Run a hypothetical spend of a compiled contract through the real script engine, off-chain — the same PASS/FAIL a node would decide, with the violated rule in plain English on failure. Pure computation: the network segment is cosmetic, and responses are no-store.
Request body
{
"program_hex": "20d9a9…87", // ≤ 20,000 hex chars
"entrypoint": "spend", // spend · reclaim · cold · inherit
"recipient": "buyer", // buyer · seller · other · self
"value": 500000000, // the state coin, sompi
"amount": 499999000, // output 0; default value − 1000
"trace": true // capture per-opcode stacks
}
Response · 200
{
"ok": true, "pass": false,
"verdict": "…", "rule": "output 0 must pay the buyer exactly value − 1000",
"template": "SilverScript · Escrow",
"trace": [ { "op": "OpDup", "dstack": ["…"], "astack": [] } ],
"note": "simulated with a stand-in signer"
}
POST/data/{network}/preflight
Will this transaction pass? Post a transaction as JSON (the SDK / RPC shape — {version, inputs, outputs, lockTime, …}) and get a verdict before you broadcast: per-input compute-budget checks (the classic “script units exceeded” trap), compute/storage mass against the block limit, an honest fee estimate — and for inputs that carry both their signatureScript and their utxo.scriptPublicKey, a real run through the script engine with the budget the transaction declares. Nothing is broadcast, nothing is stored: responses are no-store and the transaction goes into the call and nowhere else. The network segment picks the mass limits. Served by newer workers; older ones answer 404. Also behind the playground’s check-a-transaction mode.
Request body — the transaction itself
{
"version": 1,
"inputs": [
{ "previousOutpoint": { "transactionId": "…", "index": 0 },
"signatureScript": "…", "sequence": 0,
"sigOpCount": 1, "computeBudget": 20,
"utxo": { "amount": 1000000000,
"scriptPublicKey": { "version": 0, "script": "…" } } }
],
"outputs": [ { "value": 999999000,
"scriptPublicKey": { "version": 0, "script": "…" } } ],
"lockTime": 0
}
Response · 200
{
"ok": true, "verdict": "will_fail",
"findings": [
{ "severity": "error", "code": "compute_budget_missing",
"message": "input 0 is a v1 input with no computeBudget — its
effective allowance is 9,999 script units and one
CheckSig needs 100,000",
"input_index": 0, "suggestion": "set computeBudget: 10" }
],
"masses": { "compute": 12345, "storage": 0,
"transient": 512, "limit": 500000 },
"fee": { "estimate_sompi": 202400, "note": "an estimate, not consensus" },
"executed": [
{ "input_index": 0, "pass": false,
"script_units_used": 100000, "allowance": 9999 }
]
}
| Field | Type | Notes |
| verdict | string | ready — every check passed · will_fail — a node would reject this as-is · incomplete — not enough of the transaction to decide |
| findings[] | array | each check’s answer: severity (error · warn · info), a stable machine-readable code, a plain-English message, the offending input_index?, and a concrete suggestion? (the fix, e.g. set computeBudget: 10) |
| masses | object? | compute / storage / transient against the network’s block limit; omitted when budget findings block the mass calculation |
| fee | object? | estimate_sompi + note — labeled an estimate honestly: no consensus min-fee primitive exists |
| executed[] | array? | per executable input: pass, script_units_used, allowance — the real engine, the transaction’s own declared budget |
POST/data/{network}/compile
Compile SilverScript source server-side. Body is { source, args } — source up to 40,000 characters, at most 16 constructor args of ≤200 characters each.
Response · 200
{ "ok": true, "hex": "20d9a9…87" }
// or
{ "ok": false, "error": "compile error: …" }
POST/data/{network}/publish
Verify-and-publish, Etherscan-style: submit { source, args }; the worker compiles it and stores the source keyed by the compiled program's blake2b-256 hash. Any coin whose revealed program hashes the same now shows your source as community-verified.
Response · 200
{ "ok": true, "hash": "d9a9e982…f475d17",
"template": "SilverScript · Mecenas" }
GET/data/{network}/verified/{hash}
The published source for a program hash — what /publish stored. { "ok": false } when nothing is published for that hash.
Response · 200
{ "ok": true, "source": "contract Mecenas(…) { … }",
"args": "…", "template": "SilverScript · Mecenas",
"verified_at": 1752130000000 }
POST/data/{network}/subscribe
Register a webhook: your URL gets one JSON POST per matching covenant event, delivered as it happens. Filter to one coin with covenant_id (must be exactly 64 hex characters — anything else is rejected, never a silent wildcard) and/or one event kind; omit both for everything. The response hands you a generated secret — shown once, never readable back: every delivery is signed with it, and only someone holding it can unsubscribe the hook. Unsubscribe with POST /data/{network}/unsubscribe, the returned id, and that secret.
Request body
{
"url": "https://example.com/hook",
"covenant_id": "b4ade48e…", // optional, 64 hex
"kind": "burn" // optional: genesis · transition · burn
}
// → { "ok": true, "id": 7, "secret": "9f2ab4…" } // save it — shown once
// unsubscribe: { "id": 7, "secret": "9f2ab4…" }
// → { "ok": true, "deleted": true }
Each delivery
POST your-url · application/json
X-Kascov-Signature: 3f9c41… (keyed with your subscription's secret)
{
"network": "testnet-10",
"covenant_id": "b4ade48e…",
"kind": "burn",
"txid": "1461f8…07ed",
"accepting_daa": 509571520,
"tx_index": 18
}
| Semantics | | Notes |
| authenticity | | every delivery carries X-Kascov-Signature: the hex keyed blake2b-256 of the exact request body, keyed with the secret /subscribe returned — recompute it over the raw bytes you received and compare before trusting the event |
| delivery | | up to 3 attempts with backoff, 5s timeout, redirects never followed; a subscription that keeps failing is retired automatically |
| targets | | public http(s) hosts only — every resolved address is checked, and loopback / private / link-local targets are rejected (SSRF guard) |
| guarantees | | best-effort hints, not a durable feed — confirm through the polled endpoints |
POST/data/{network}/deploy
One-click deploy: the server births your compiled contract as a real covenant on testnet-10 using its own faucet key — no local toolchain. Gated off by default: the route exists only when the server is armed (KASCOV_DEPLOY_KEY) and the network is testnet-10; otherwise it answers 404. Never on mainnet.
Request body
{
"program_hex": "20d9a9…87", // ≤ 20,000 hex chars
"value": 1000000000 // 1–10 TKAS, in sompi
}
Response · 200
{ "ok": true,
"covenant_id": "b4ade48e…",
"network": "testnet-10" }
| Limit | | Notes |
| global | | token bucket — 5 burst, refilling 1 per 10 minutes (≈144/day); throttled requests get 429 |
| per IP | | 20 deploys per day (best-effort) |
| in flight | | one deploy at a time — they share the funding wallet |