Under the hood
API
The read-only endpoints and the relay endpoint, with request and response shapes.
Conventions
| Base URL | https://vpm.playhunch.xyz |
| Auth | None. Every GET is public, read-only and cached; nothing here holds a secret. |
| Amounts | USDG in its smallest unit (6 decimals) as a decimal string: "25000000" is 25.00 USDG. |
| Prices | Chainlink answers at 8 decimals as a decimal string: "22566018707" is 225.66. |
| Times | Unix seconds (numbers). |
| Outcomes | The strings "UP" and "DOWN" (0 and 1 on-chain). |
| Freshness | Chain reads carry readAt (when they were read) and stale (true when the chain could not be read and this is the last good read). |
| Stability | Fields may be added, never renamed or removed. |
Anything these endpoints return can also be read straight from the contracts; the API is a cache, not a source of truth. See Contracts. Before the contracts are deployed the read endpoints answer with empty lists and "deployed": false, and the relay refuses.
Endpoints
| Method and path | What it returns | Cache | Status |
|---|---|---|---|
GET /api/prices | The latest Chainlink reading for every ticker | 15 s | Served |
GET /api/markets | Every market with its book and state | 15 s | Served; empty until launch |
GET /api/markets/[id] | One market with every position | 5 s | Served; empty until launch |
GET /api/positions?owner= | One address's positions across markets | 5 to 15 s | Served; empty until launch |
GET /api/proof | Counters, settled markets, refund drill, fee sweeps | 60 s | Served; empty until launch |
GET /api/health | Whether the keeper's jobs are keeping up | none | Served |
POST /api/relay/enter | Relays a signed bet; returns the transaction hash | none | Served; refuses until launch |
GET /api/prices
One multicall of latestRoundData across the Chainlink proxies in the deployment file. If the chain read fails, the response is still 200 with the last good readings and status: "stale-cache" (or "unavailable" if there has never been one), so a caller can show the age instead of nothing.
{
"status": "live", // "live" | "stale-cache" | "unavailable"
"readAt": 1790712345, // when these readings were taken
"readings": [
{
"ticker": "NVDA",
"name": "NVIDIA",
"feed": "0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15",
"answer": "22566018707", // 8 decimals: 225.66018707
"roundId": "18446744073709552722",
"updatedAt": 1790711765 // the round's updatedAt
}
// … TSLA, AAPL, COIN
]
}GET /api/markets
Every market listed by the factory, newest first, read with view calls. 503 if the chain has never been readable.
{
"deployed": true,
"status": "deployed",
"entriesPaused": false,
"readAt": 1790712345, "stale": false,
"markets": [
{
"id": "12",
"href": "/m/12",
"ticker": "NVDA",
"family": "weekly", // "daily" | "weekly" | "drill"
"question": "Will NVDA finish the week UP? · Tue Sep 29 → Fri Oct 2",
"phase": "live", // "opens" | "live" | "frozen" | "resolved" | "void"
"status": "Live", // the same, in words: "Resolved UP", "Void", …
"winner": null, // "UP" | "DOWN" once resolved
"strikeTime": 1790688600,
"finalTime": 1790971200,
"strike": { "answer": "22410000000", "roundId": "18446744073709552790", "at": 1790688012 },
"strikeProblem": null,
"live": { "answer": "22566018707", "roundId": "18446744073709552799", "at": 1790711765 },
"change": { "direction": "UP", "bps": 69, "text": "+0.69%" },
"pool": { "up": "120000000", "down": "80000000" }, // accepted, seed included
"totals": { "pool": "200000000", "up": "120000000", "down": "80000000",
"pendingUp": "0", "pendingDown": "0", "paidOut": "0" },
"headroom": { "up": "2300000000", "down": "3500000000" },
"limits": { "minEntry": "1000000", "maxEntry": "100000000", "feeBps": 200 },
"acceptingBets": true,
"maxStrikeAge": 93600, "maxFinalAge": 93600, "voidableAt": 1791230400,
"seedPerLeg": "10000000", "opener": "0x…", "openedAt": 1790680000,
"specId": "0x…",
"feed": "0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15",
"stockToken": "0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC",
"rules": { "heading": "How this market settles.", "segments": [ … ], "text": "…" }
}
]
}GET /api/markets/[id]
One market, its headroom per side and every position in entry order. Seed positions carry "seed": true. After the bell it also carries the two rounds the round finder proved and what the resolver’s preview says about them; after settlement, each position carries what it was paid and what an ordinary pool would have paid. 404 if Hunch never listed the id.
{
"market": { /* as in /api/markets */ },
"headroom": { "up": "2900000000", "down": "1900000000" },
"positions": [
{
"id": "57",
"marketId": "12",
"owner": "0x…",
"side": "UP",
"offered": "20000000",
"accepted": "20000000", // null until its batch is matched on chain
"refused": "0",
"accrued": "40000000", // paid if UP won now; only goes up
"payout": null, // after settlement: paid for the settlement, after the fee
"classicPayout": null, // ordinary-pool comparison, after resolution
"seed": false,
"opener": false, // placed by Hunch's own listing wallet
"finalized": true, "refunded": false, "claimed": false,
"vintage": "21345678", // Ethereum block of entry (0 for the seed)
"settlement": { "gross": "0", "fee": "0", "net": "0", "refund": "0", "total": "0", "deliverable": false },
"paidOut": null,
"enteredAt": 1790688900, // from logs; null when they cannot be read
"entryTx": "0x…",
"payoutTx": null
}
],
"resolution": null, // once settled: { "outcome": "UP" | "DOWN" | "FLAT" | "VOID",
// "reason": null | "flat" | "stale" | "paused" | "timeout",
// "strikeRound", "finalRound", "tx" }
"head": { "blockNumber": "…", "l1BlockNumber": "…", "timestamp": 1790712345 },
"entriesPaused": false,
"vintageBlock": null, // the open batch's Ethereum block
"kappa": "30",
"books": [ { "principal": "…", "acc": "…", "capacity": "…", "vested": "…", "demand": "…", "live": "…" }, { … } ],
"finder": null, // after the bell: { "ok", "strikeRound", "finalRound", "strike", "final", "expected", "problem" }
"preview": null, // after the bell: { "status", "name", "strikeAnswer", "strikeAt", "finalAnswer", "finalAt" }
"activity": true, // entry times and transaction links were read from logs
"readAt": 1790712345, "stale": false
}GET /api/positions?owner=0x…
Every position an address holds, across markets, with totals. The address is never logged and never sent to analytics. 30 requests a minute per IP.
{
"owner": "0x…",
"deployed": true,
"positions": [ { "marketId": "12", "question": "…", "ticker": "NVDA", "href": "/m/12",
"phase": "live", "status": "Live", "winner": null, "finalTime": 1790971200, "open": true,
/* position fields as above */ } ],
"totals": {
"staked": "70000000",
"accrued": "96250000", // what each open position is paid if its side wins now, summed
"paid": "0", // settlement payouts already sent, after the fee
"accepted": "70000000",
"deliverable": "0", // what claims and refunds would deliver now
"open": 2
},
"readAt": 1790712345, "stale": false
}GET /api/proof
Everything the Proof page shows: contracts and feeds, the Safe’s threshold, counters (each with the call it came from), settled markets with their rounds, the refund drill and fee sweeps. Hunch’s own wallets are excluded from the bettor count and reported separately.
{
"status": "deployed", // or "not-deployed"
"contracts": [ { "name": "HunchVPM", "role": "…", "address": "0x…", "deployTx": "0x…", "verified": true } ],
"feeds": [ { "name": "NVDA / USD", "address": "0x…", … } ],
"safe": { "address": "0x…", "threshold": 2, "owners": 3 },
"counters": [
{ "label": "Markets opened", "value": "24", "unit": "count",
"source": "https://robinhoodchain.blockscout.com/address/0x…?tab=read_contract",
"sourceLabel": "factory.listingCount()", "note": null }
],
"settled": [ { "id": "12", "question": "…", "outcome": "UP", "strike": { … }, "final": { … },
"resolveTx": "0x…", "positions": 9, "totalPaid": "412500000" } ],
"refundDrill": null, // { "id", "status": "listed" | "refunded", "reason", "voidTxUrl", "refunds": [ … ] }
"feeSweeps": [ { "tx": "0x…", "amount": "1200000", "at": 1790712345 } ],
"bettors": { "distinct": 31, "excluded": [ "0x…" ], "operatorBets": 4, "bets": 88 },
"usdg": { "staked": "…", "accepted": "…", "seeded": "…", "paidToBettors": "…", "paidOut": "…",
"feesTaken": "…", "feesAccrued": "…", "feesSwept": "…" },
"missing": [], // log-based sections that could not be read just now
"readAt": 1790712345, "stale": false
}GET /api/health
200 when every check on the keeper page holds, 503 otherwise, listing what failed. Never cached. Suitable for an uptime monitor.
{
"ok": false,
"deployed": true,
"nowSec": 1790712345,
"checks": [
{ "name": "rpc-head", "ok": true, "detail": "latest block is 1 s old" },
{ "name": "settlement", "ok": false, "detail": "market 12 is 41 min past its bell" },
{ "name": "keeper-eth", "ok": true, "detail": "0.0081 ETH" },
{ "name": "market-reads", "ok": true, "detail": "2 open markets read current (oldest 4s)" },
{ "name": "market-logs", "ok": true, "detail": "/m/1 entry times and links read (2 entries)" }
]
}POST /api/relay/enter
Relays a signed bet (see Gasless betting). The relayer checks it, simulates it, sends enterWithAuthorization, waits up to ten seconds for the receipt and returns the transaction hash. It cannot change what was signed, and anyone can send the same call directly instead.
Request
{
"from": "0x…", // the signer; the position's owner
"marketId": "12",
"outcome": 0, // 0 = UP, 1 = DOWN
"amount": "25000000", // USDG units; 1 to 100 USDG in the beta
"validAfter": "0",
"validBefore": "1790712645", // at least 30 s and at most 1 hour from now
"salt": "0x…", // 32 random bytes
"signature": "0x…", // EIP-712 over USDG's ReceiveWithAuthorization
"chainId": 4663, // optional: checked if present
"hunchVpm": "0x…" // optional: checked if present
}Response
{ "ok": true, "txHash": "0x…", "nonce": "0x…", "receipt": "confirmed" } // or "pending" / "reverted"| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed. |
| 400 | bad_signature | Wrong domain, or the signature does not match this wallet, market, side and amount. |
| 400 | contract_signer | A smart-contract wallet (ERC-1271) signed it: gasless bets need a regular wallet signature. Pay gas yourself. |
| 400 | expired | Outside validAfter / validBefore, or valid for longer than an hour. |
| 400 | amount_out_of_bounds | Below the minimum or above the maximum bet. |
| 403 | region_blocked | Stock-price markets are not offered where the request came from. |
| 404 | market_not_found | Hunch never listed this market. |
| 409 | market_closed | Not taking bets: past the bell, settled, or new bets paused. |
| 409 | already_used | This signature was already used. Sign a new bet. |
| 422 | insufficient_balance | Not enough USDG in the signing wallet on Robinhood Chain. |
| 422 | simulation_failed | The call would revert; the message says why. |
| 429 | rate_limited | More than 10 requests a minute from this IP or this signer. |
| 502 | relay_failed | The relayer could not confirm the send. It may have landed: send the SAME body again (USDG accepts a signature once, so this never bets twice). |
| 503 | busy | Many bets landed in this Ethereum block. Send the same body again after retryAfter seconds. |
| 503 | relay_unavailable | The relayer is off or out of gas. Send the call yourself. |
| 503 | not_deployed | Hunch is not deployed yet. |
Rate limits: 10 relay requests a minute per IP and per signer, per server instance. GET endpoints are cached at the edge and have no limit beyond fair use, except positions (30 a minute per IP).
GET /api/cron/[job]
For the scheduler only: open, resolve and deliver run the keeper’s jobs and return its report. Every request needs Authorization: Bearer with the deployment’s cron secret; anything else is 401. Every job is idempotent, and every action it takes, anyone can take.