Threshold is a decentralized prediction market platform built on the Kaspa blockchain. It combines a real-time price oracle with binary outcome markets, powered by KRC-20 tokens and LMSR automated market making.
Live demo: threshold-kaspa.vercel.app
- Oracle fetches BTC, ETH, and KAS prices from CoinGecko + CoinMarketCap every 15 seconds
- Aggregator computes a median-based spot index with outlier filtering
- Anchor writes a CBOR-encoded proof to the Kaspa blockchain (via public Resolver nodes)
- Markets let users bet on price thresholds (e.g. "BTC >= $100,000 before March 1")
- KRC-20 tokens (YES/NO) represent shares, deployed on-chain via Kasplex inscriptions
- LMSR pricing ensures continuous liquidity - no order book needed
User connects wallet (Kasware/Kastle)
-> Buys YES shares on "BTC >= $100K"
-> Sends KAS to platform address
-> Receives KRC-20 YES tokens
-> If BTC hits $100K before deadline: each YES token pays 1 KAS
-> If not: NO token holders get paid
Vercel (Frontend)
threshold-kaspa.vercel.app
|
Next.js rewrites
/api/pm/* /api/oracle/*
|
Railway (Backend)
oracle-kaspa-production.up.railway.app
|
+------------+------------+
| |
Oracle API PM API
/health /pm/events
/latest /pm/trade
/verify/:txid /pm/quote
| |
| LMSR Engine
| KRC-20 Service
| |
+-------+ +-----------+--+
| |
Kaspa Testnet-10
(via public Resolver)
Oracle data flow:
CoinGecko ----+
|---> Aggregator ---> Bundle ---> Hash ---> CBOR ---> Kaspa TX
CoinMarketCap-+ |
v
data/bundles/<h>.json
|
v
HTTP API :3000
| Module | Description |
|---|---|
collector/ |
Price fetching from CoinGecko + CoinMarketCap (key rotation) |
aggregator/ |
Median calculation, outlier filtering (1% threshold), quorum check |
proofs/ |
Bundle creation, SHA-256 hashing, local archival |
kaspa-anchor/ |
On-chain anchoring via CBOR payloads (Resolver or direct RPC) |
api/ |
Oracle HTTP API (/health, /latest, /bundle/:h, /verify/:txid) |
pm/engine/ |
Market resolution engine, oracle sync loop |
pm/math/ |
LMSR (Logarithmic Market Scoring Rule) pricing |
pm/krc20/ |
KRC-20 token deployment, minting, transfers via Kasplex |
pm/api/ |
Prediction market HTTP API |
server.ts |
Combined entrypoint (single port for Railway) |
scripts/verify-tx.ts |
Standalone TX verification tool |
Next.js 14 app with TailwindCSS. Pages:
/pm- Markets overview with live prices and scrolling ticker/pm/market/:id- Market detail with trading panel, chart, trade history/pm/wallet- Portfolio view with positions and P&L
Wallet support: Kasware, Kastle, and demo mode.
# Clone and install
git clone <repo-url>
cd oracle-kaspa
npm install
# Configure environment
cp .env.example .env
# Edit .env with your keys:
# KASPA_PRIVATE_KEY=<hex-private-key>
# KASPA_ADDRESS=kaspatest:<your-address>
# CMC_API_KEY=<coinmarketcap-api-key>
# Start Oracle (port 3000)
npm start
# Start PM server (port 3001)
npm run pm
# Start frontend (port 3002)
npm run pm:webOr run everything on one port:
npm run start:combinednpm run smoke:rpcExpected output:
Connecting to ws://127.0.0.1:17210 (network: testnet-10)...
Connected!
Server info: { ... }
UTXOs for kaspatest:...: 5
Balance: 50000000000 sompi
RPC smoke test passed!
npm startExpected output:
Starting Kaspa Spot Oracle...
Network: testnet-10
Interval: 60s (+-5s jitter)
[CMC] Initialized with 5 API key(s)
Connected to Kaspa. Address: kaspatest:...
API server listening on http://localhost:3000
[TICK] 2025-01-15T10:30:00.000Z
Providers: 2/2 OK
coingecko: $96250.00
coinmarketcap: $96248.50
Index: $96249.25 [OK]
Bundle: a1b2c3d4e5f6g7h8...
TX: abc123def456...
Done in 1523ms
Copy a TXID from the oracle logs, then:
npm run verify:tx -- <txid>Expected output:
=== Oracle TX Verification ===
TXID: abc123def456789...
Connecting to ws://127.0.0.1:17210...
Fetching transaction...
Payload (hex): a4616400616836...
Payload size: 47 bytes
--- Decoded Payload ---
p (price): $96249.25
n (num_sources): 2
d (dispersion): 0.0016%
h (bundle_hash): a1b2c3d4e5f6g7h8
Validation: PASSED
--- Local Bundle Verification ---
Bundle found locally!
Hash verification: PASSED (a1b2c3d4e5f6g7h8)
--- Bundle Summary ---
Tick ID: 2025-01-15T10:30:00.000Z
Network: testnet-10
Price: $96249.25
Sources: coingecko, coinmarketcap
Dispersion: 0.0016%
Verification complete.
The oracle provides cryptographic verifiability:
-
On-chain: The Kaspa transaction contains CBOR-encoded
{p, n, d, h}:p= BTC/USD price (2 decimal places)n= number of sources usedd= dispersion (max-min)/medianh= first 16 chars of bundle hash
-
Off-chain: The full bundle is stored and accessible via API:
- Contains raw responses from each provider
- Contains aggregation metadata (outlier filtering, etc.)
-
Verification: Anyone can verify by:
SHA256(canonical_json(bundle))[0:16] === on_chain_h
This proves the anchored price was computed from the specific provider responses.
Each oracle tick anchors a CBOR-encoded payload to Kaspa:
{
d: 0.0001, // dispersion ratio between sources
h: "f4895c9144fd6bb9", // SHA-256 hash of full proof bundle (16 chars)
n: 2, // number of valid price sources
p: 68497.66 // aggregated price (USD)
}Keys are alphabetically sorted for deterministic encoding. Payload fits within 80 bytes.
Each market has a YES and NO token pair deployed as KRC-20 inscriptions on Kaspa:
- Deploy: Commit-reveal inscription (~1000 KAS protocol fee per token)
- Buy: Platform transfers pre-minted tokens to buyer
- Sell: Tokens return to platform pool
- Resolution: Winning tokens pay 1 KAS each, losing tokens are worthless
Token tickers follow the pattern: Y{ASSET}{MONTH}{INDEX} / N{ASSET}{MONTH}{INDEX}
(e.g. YBTCCA = YES BTC March Market A)
| Endpoint | Description |
|---|---|
GET /health |
Full health status (oracle + kaspad) |
GET /latest |
Latest bundle + txid |
GET /bundle/:h |
Bundle by hash (16 hex chars) |
GET /verify/:txid |
Verify a transaction (64 hex chars) |
curl http://localhost:3000/health | jq{
"status": "healthy",
"timestamp": "2025-01-15T10:35:00.000Z",
"oracle": {
"network": "testnet-10",
"last_tick_id": "2025-01-15T10:30:00.000Z",
"last_updated_at": "2025-01-15T10:30:05.000Z",
"lag_seconds": 295,
"last_txid": "abc123def456...",
"last_hash": "a1b2c3d4e5f6g7h8",
"last_price": 96249.25,
"providers_ok": 2,
"providers_total": 2
},
"kaspa": {
"is_synced": true,
"virtual_daa_score": "123456789",
"utxo_count": 5,
"balance_sompi": "50000000000"
}
}Status values: healthy (all systems operational), degraded (some providers failing or lag 60-120s), unhealthy (critical issue).
curl http://localhost:3000/latest | jq{
"latest": {
"h": "a1b2c3d4e5f6g7h8",
"hash_full": "a1b2c3d4e5f6g7h8...",
"txid": "abc123def456789...",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"bundle": {
"tick_id": "2025-01-15T10:30:00.000Z",
"network": "testnet-10",
"responses": ["..."],
"index": {
"price": 96249.25,
"sources_used": ["coingecko", "coinmarketcap"],
"num_sources": 2,
"dispersion": 0.000016
}
}
}curl http://localhost:3000/verify/<txid> | jq{
"txid": "abc123...",
"network": "testnet-10",
"status": "PASSED",
"tx_found": true,
"block_time": "2025-01-15T10:30:00.000Z",
"payload_hex": "a4616400...",
"payload_size": 47,
"decoded": {
"p": 96249.25,
"n": 2,
"d": 0.000016,
"h": "a1b2c3d4e5f6g7h8"
},
"validation": { "valid": true, "errors": [] },
"bundle_found": true,
"hash_verified": true,
"bundle_summary": {
"tick_id": "2025-01-15T10:30:00.000Z",
"network": "testnet-10",
"price": 96249.25,
"sources_used": ["coingecko", "coinmarketcap"],
"dispersion": 0.000016
},
"provider_responses": [
{ "provider": "coingecko", "price": 96250.00, "ok": true, "error": null },
{ "provider": "coinmarketcap", "price": 96248.50, "ok": true, "error": null }
]
}Status values: PASSED (TX found, payload valid, bundle verified), PARTIAL (TX valid but bundle not found locally), FAILED (validation or hash verification failed), ERROR (TX not found or invalid TXID).
| Endpoint | Description |
|---|---|
GET /pm/events |
All events with oracle prices |
GET /pm/event/:id |
Event detail with markets |
GET /pm/market/:id |
Market detail with trades |
GET /pm/quote |
Get trade quote (price, shares, fees) |
POST /pm/trade |
Execute a trade |
GET /pm/wallet/:addr |
User positions and balance |
GET /pm/oracle |
Current oracle state |
POST /pm/sync |
Force oracle sync |
The backend runs both Oracle and PM APIs on a single port via src/server.ts.
Environment variables:
| Variable | Value | Required |
|---|---|---|
KASPA_PRIVATE_KEY |
Hex-encoded private key | Yes |
PLATFORM_PRIVATE_KEY |
Same as above (for KRC-20 ops) | Yes |
KASPA_NETWORK |
testnet-10 |
Yes |
KASPA_RPC_URL |
Empty = use public Resolver | No |
USE_REAL_KRC20 |
true for on-chain tokens |
Yes |
KASPLEX_INDEXER_API |
https://tn10api.kasplex.org |
Yes |
CMC_API_KEY |
CoinMarketCap API key | Yes |
API_PORT |
3000 |
No |
PM_API_PORT |
3001 |
No |
Railway auto-deploys from the main branch. The railway.toml configures the build.
| Variable | Value |
|---|---|
BACKEND_URL |
https://<railway-app>.up.railway.app |
NEXT_PUBLIC_PLATFORM_ADDRESS |
Platform's Kaspa testnet address |
Set Root Directory to apps/pm-web in Vercel project settings.
- Backend: TypeScript, Node.js 20, Kaspa WASM SDK, cbor-x
- Frontend: Next.js 14, React 18, TailwindCSS
- Blockchain: Kaspa testnet-10, KRC-20 via Kasplex
- Infrastructure: Railway (backend), Vercel (frontend)
- Pricing: LMSR (Logarithmic Market Scoring Rule)
"No UTXOs available"
- Fund your testnet wallet at https://faucet.kaspa.org/
"Transaction not found"
- Wait for confirmation (1-2 seconds on testnet)
- Check kaspad is synced
"All CMC keys rate-limited"
- CoinGecko-only mode still works
- Anchoring continues with 1 source (status: STALE)
API returns 404 for bundle
- Oracle must be running to store bundles
- Check
data/bundles/directory exists
ISC