Skip to content

Under the hood

API

The read-only endpoints and the relay endpoint, with request and response shapes.

Conventions

API conventions
Base URLhttps://vpm.playhunch.xyz
AuthNone. Every GET is public, read-only and cached; nothing here holds a secret.
AmountsUSDG in its smallest unit (6 decimals) as a decimal string: "25000000" is 25.00 USDG.
PricesChainlink answers at 8 decimals as a decimal string: "22566018707" is 225.66.
TimesUnix seconds (numbers).
OutcomesThe strings "UP" and "DOWN" (0 and 1 on-chain).
FreshnessChain reads carry readAt (when they were read) and stale (true when the chain could not be read and this is the last good read).
StabilityFields 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

Endpoints
Method and pathWhat it returnsCacheStatus
GET /api/pricesThe latest Chainlink reading for every ticker15 sServed
GET /api/marketsEvery market with its book and state15 sServed; empty until launch
GET /api/markets/[id]One market with every position5 sServed; empty until launch
GET /api/positions?owner=One address's positions across markets5 to 15 sServed; empty until launch
GET /api/proofCounters, settled markets, refund drill, fee sweeps60 sServed; empty until launch
GET /api/healthWhether the keeper's jobs are keeping upnoneServed
POST /api/relay/enterRelays a signed bet; returns the transaction hashnoneServed; 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.

200 application/json
{
  "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.

200 application/json
{
  "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.

200 application/json (404 if no such market)
{
  "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.

200 application/json (400 if owner is not an address)
{
  "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.

200 application/json
{
  "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.

200 or 503 application/json
{
  "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

application/json
{
  "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

200 application/json
{ "ok": true, "txHash": "0x…", "nonce": "0x…", "receipt": "confirmed" }   // or "pending" / "reverted"
Relay errors
StatuserrorMeaning
400invalid_requestA field is missing or malformed.
400bad_signatureWrong domain, or the signature does not match this wallet, market, side and amount.
400contract_signerA smart-contract wallet (ERC-1271) signed it: gasless bets need a regular wallet signature. Pay gas yourself.
400expiredOutside validAfter / validBefore, or valid for longer than an hour.
400amount_out_of_boundsBelow the minimum or above the maximum bet.
403region_blockedStock-price markets are not offered where the request came from.
404market_not_foundHunch never listed this market.
409market_closedNot taking bets: past the bell, settled, or new bets paused.
409already_usedThis signature was already used. Sign a new bet.
422insufficient_balanceNot enough USDG in the signing wallet on Robinhood Chain.
422simulation_failedThe call would revert; the message says why.
429rate_limitedMore than 10 requests a minute from this IP or this signer.
502relay_failedThe 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).
503busyMany bets landed in this Ethereum block. Send the same body again after retryAfter seconds.
503relay_unavailableThe relayer is off or out of gas. Send the call yourself.
503not_deployedHunch 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.