Skip to content

HodlOdds API

API documentation

Crash odds for covered SOL tokens, one request at a time.

0.04 means about 4 in 100 tokens like this one lose 90% or more of their price within the next 60 minutes. Higher means riskier, but a low number does not mean safe: smaller or slower drops are not counted, and the odds change minute by minute.

On this page

Quickstart for agents

  1. Inspect the metadata

    Check /v1/status and /v1/meta first. A live feed alone does not mean paid access is enabled: require public_beta and payments.ready from metadata.

  2. Set your payment limits

    Use a dedicated local payer with USDC on the exact advertised Solana network. Obtain the expected payment recipient independently, then allow only that recipient, USDC mint, network and a fixed per-call maximum. These examples explicitly authorize mainnet risk calls up to $0.01; for the board use its advertised price with a consciously raised cap. Devnet uses different USDC and network identifiers.

  3. Choose a client and keep the receipt

    Use the official x402 2.25.0 client for the selected scheme: Solana exact or Base EIP-3009 exact authorization. Base signing needs no token approval or SPL Memo. Pin the network, USDC contract, recipient and maximum total before signing. Never choose an offered network automatically just because it appears first. The examples below explicitly target Solana; Base customers use the SDK's EVM signer and the Base option advertised by the service.

    The Python imports and version were checked against the official x402 2.25.0 wheel and client source. The TypeScript package versions and interfaces could not be verified in the offline reference bundle: the example is a review starting point, not a tested version pin. Validate compatible official packages, pin them and retain the lockfile before funding a client. Do not assume their version equals the Python version.

    The SDK signs locally. HodlOdds never receives your private key. The facilitator supplies the transaction fee payer. Store payment receipts; do not log signed payment payloads or automatically retry after an ambiguous result. The examples use each library's payment client; they do not construct the x402 payment protocol themselves.

Request examples

1. Inspect the free metadata

curl -i https://api.hodlodds.com/v1/status
curl -s https://api.hodlodds.com/v1/meta
curl -i 'https://api.hodlodds.com/v1/risk/REPLACE_WITH_MINT'
# A fresh covered mint returns 402 while /v1/meta reports public_beta: true (open beta since 20 October 2026).
curl -i 'https://api.hodlodds.com/v1/board?sort=lowest_risk&limit=10&min_liquidity_sol=50'

2. Python client

# Python >=3.10; save as quickstart.py.
# python -m pip install 'x402[httpx,svm]==2.25.0'
# Set KEYPAIR_FILE, EXPECTED_PAY_TO and TOKEN_MINT in your environment.
# KEYPAIR_FILE is your local solana-keygen JSON file (64-byte array).
import asyncio, base64, json, os
from pathlib import Path
from x402 import x402Client
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.svm.constants import (
    SOLANA_MAINNET_CAIP2, USDC_MAINNET_ADDRESS,
)
from x402.mechanisms.svm.signers import KeypairSigner
from x402.mechanisms.svm.exact.register import register_exact_svm_client

NETWORK = SOLANA_MAINNET_CAIP2
PAY_TO = os.environ["EXPECTED_PAY_TO"]  # Confirm independently with HodlOdds.
MAX_ATOMIC = 10_000  # $0.01 USDC, six decimals; board totals are 5_000 per delivered coin.

def approve(version, offers):
    if version != 2:
        return []
    return [r for r in offers if r.scheme == "exact"
            and r.network == NETWORK and r.asset == USDC_MAINNET_ADDRESS
            and r.pay_to == PAY_TO and 0 < int(r.amount) <= MAX_ATOMIC
            and (r.extra or {}).get("paymentFlow") == "authorization"]

async def main():
    key_bytes = bytes(json.loads(Path(os.environ["KEYPAIR_FILE"]).read_text()))
    if len(key_bytes) != 64:
        raise ValueError("Expected a 64-byte solana-keygen keypair file")
    signer = KeypairSigner.from_bytes(key_bytes)
    client = x402Client()
    register_exact_svm_client(client, signer, networks=[NETWORK], policies=[approve])
    async with x402HttpxClient(client, timeout=30, follow_redirects=False) as http:
        # Each call can pay once: no outer retry loop on timeouts or errors.
        response = await http.get("https://api.hodlodds.com/v1/risk/" +
                                  os.environ["TOKEN_MINT"])
        receipt = response.headers.get("PAYMENT-RESPONSE")
        if receipt:
            result = json.loads(base64.b64decode(receipt, validate=True))
            print("Settlement:", result.get("success"), result.get("transaction"))
        print("HTTP", response.status_code)
        response.raise_for_status()
        answer = response.json()
        print(answer["row"]["p_crash_60m"], answer["expires_at"])

asyncio.run(main())

3. TypeScript client

// Official TypeScript-library example; validate against your locked packages.
// Choose and verify compatible versions, then pin them with --save-exact:
// npm install --save-exact @x402/core@<verified-version> \
//   @x402/fetch@<verified-version> @x402/svm@<verified-version> \
//   @solana/kit@<compatible-version>
// Set KEYPAIR_FILE, EXPECTED_PAY_TO and TOKEN_MINT. Use Node with TS support
// or your project's TypeScript runner. Never commit the keypair file.
import { readFile } from "node:fs/promises";
import { x402Client } from "@x402/core/client";
import { wrapFetchWithPayment } from "@x402/fetch";
import { ExactSvmScheme } from "@x402/svm/exact/client";
import { createKeyPairSignerFromBytes } from "@solana/kit";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}
const network = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
const asset = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const payTo = required("EXPECTED_PAY_TO"); // Confirm independently.
const raw = JSON.parse(await readFile(required("KEYPAIR_FILE"), "utf8"));
if (!Array.isArray(raw) || raw.length !== 64 ||
    !raw.every(x => Number.isInteger(x) && x >= 0 && x <= 255)) {
  throw new Error("Expected a 64-byte solana-keygen keypair file");
}
const signer = await createKeyPairSignerFromBytes(new Uint8Array(raw));
const client = new x402Client().register(network, new ExactSvmScheme(signer));
client.registerPolicy((version, offers) => version !== 2 ? [] : offers.filter(r =>
  r.scheme === "exact" && r.network === network && r.asset === asset &&
  r.payTo === payTo && BigInt(r.amount) > 0n && BigInt(r.amount) <= 10_000n &&
  r.extra?.paymentFlow === "authorization"));
const paidFetch = wrapFetchWithPayment(fetch, client);
// No outer retries: reconcile a timeout or ambiguous receipt before paying again.
const response = await paidFetch("https://api.hodlodds.com/v1/risk/" +
  encodeURIComponent(required("TOKEN_MINT")), { redirect: "error" });
const receipt = response.headers.get("PAYMENT-RESPONSE");
if (receipt) {
  const result = JSON.parse(Buffer.from(receipt, "base64").toString("utf8"));
  console.log("Settlement:", result.success, result.transaction);
}
console.log("HTTP", response.status);
if (!response.ok) throw new Error("Reconcile the response before retrying");
console.log(await response.json());

Machine-readable contract: OpenAPI 3.1. Agent overview: llms.txt. Response examples in the contract are explicitly illustrative data. The forecast ends at horizon_end, not 60 minutes after your request.

These clients request fresh answers only and never fall back automatically. To opt in to last-known odds, explicitly request /v1/risk/{mint}/last and set the payment cap to its advertised price (default 2_000 USDC atomic units). Inspect stale, age_s and horizon_remaining_s before using the response.

How to read a score

HodlOdds estimates whether a completed one-minute close in SOL will be at least 90% below the reference close within the displayed 60-minute forecast window. It does not measure every kind of loss or whether a token can be sold.

The independent trading system operated by the publisher may trade these tokens. It receives no customer searches, API purchases or order flow. Payment does not change a score.

Freshness

  1. Minute closesCompleted one-minute bars
  2. Scored ~20 s laterComputation and publication can delay availability
  3. Until the next updateRefresh once per minute

Scores are recomputed every 60 seconds, about 20 seconds after each minute closes, from completed one-minute bars up to as_of. Each score covers the 60 minutes after its as_of time and is valid only until the next update; the odds for the same token can rise sharply from one minute to the next. Scoring starts around :20; computation and publication can delay availability. For decisions, refresh immediately before acting and once per minute for continuous monitoring, checking as_of for a new minute. The API freshness cutoff is separate: by default, data becomes stale more than 120 seconds after issued_at or after expires_at. A new request does not restart the forecast window.

If the trade stream that feeds the scores has a gap (for example while the data collector restarts), minutes scored over that gap are not published: the feed turns stale for a few minutes and the current-odds routes (/v1/risk/{mint} and /v1/board) are refused free until the inputs are complete again; the opt-in /v1/risk/{mint}/last route can still sell the last stored answer, labelled stale with its age, within its 30-minute limit.

Historical confidence

Confidence describes how closely historical predictions in this probability range matched what happened, and how much evidence backs that. It is not a guarantee for this token.

Historical validation of weekly BF refits, September 1 through October 14, 2026 excluding September 20; already-seen data, not measured calibration of the frozen serving fit.

The frozen serving fit was also measured in a pre-registered forward test (16–20 October 2026, 551,983 labelled predictions on 14,161 coins): observed/expected crash rate 0.96, calibration slope 1.02, and all nine probability bands within tolerance. The 2–5%, 5–10% and 10–20% bands over-predicted; the bands below 2% and from 20% to 70% under-predicted (0.5–1%: predicted 0.73%, observed 1.32%; 40–70%: predicted 55.3%, observed 63.0%). The confidence levels below do not yet use that test and will be reviewed after the descriptive look on 30 October 2026. A few days of data do not establish long-run calibration.

The additive confidence field uses rule hodlodds.confidence.v1. It never changes the probability, rank, alert threshold or feed freshness. A confidence level is an ordinal presentation label, not a statistical confidence interval or a claim that a token can be sold.

Let error be the absolute difference between observed incidence and mean prediction, and let tol = max(0.02, 0.25 × pred). Relative error is error / pred; when pred is zero it is zero only if observed is also zero, otherwise infinite. Approximate positives are n × observed.

  • High: at least 100 positive landmarks, error ≤ tol/2 and relative error ≤25%.
  • Medium: otherwise, at least 20 positive landmarks, error ≤ tol and relative error ≤50%.
  • Low: otherwise, including missing or invalid evidence.

Below about 2 %, observed crash rates have run up to about twice the predicted value (under one percentage point in absolute terms). Answers from 0.5% to 2% carry confidence: low; answers below 0.5% carry confidence: medium at most, with direction: historically under-predicted.

The relative limits prevent a small absolute error from hiding an incidence rate much larger than predicted. Direction says historically under-predicted or over-predicted only when error > tol/2 or relative error >25%.

Tokens younger than 120 minutes, or with unknown age, cannot receive high confidence. The historical bands include young tokens, but do not validate age-specific calibration or complete feature history. Non-SOL rows always receive experimental, with no SOL validation statistics attached.

All counts below are landmarks, which can repeat the same mint or crash. Approximate positives are not independent incidents. Per-band distinct-mint counts and confidence intervals are unavailable. The levels below assume a known age of at least 120 minutes.

Probability rangeLandmarksMean predictedObservedApprox. positivesLevelDirection
0–0.5%1,379,8490.0240%0.0348%480mediumhistorically under-predicted
0.5–1%33,2340.7277%1.3300%442lowhistorically under-predicted
1–2%43,0101.4784%2.4064%1,035lowhistorically under-predicted
2–5%77,5153.3702%3.7954%2,942highNone
5–10%54,0377.0499%5.6313%3,043mediumhistorically over-predicted
10–20%30,09514.0440%11.3740%3,423mediumhistorically over-predicted
20–40%17,59127.8544%27.3094%4,804highNone
40–70%11,91853.2333%63.6684%7,588mediumhistorically under-predicted
70–100%7,16083.8896%85.1816%6,099highNone

This is the frozen reference evidence table, also available with its source hash and rule in /v1/meta. New response rows carry their own publisher-supplied evidence. Old snapshots without the additive field remain valid and have no inferred confidence level. On the separate last-known route, stale data applies the one-level downgrade described below; the original evidence and probability stay unchanged.

Coverage

Coverage is limited to supported, active SOL-quoted pump.fun graduates. Non-SOL pairs are experimental rankings: mid-range scores historically underpredicted incidence. Their scores are returned free with HTTP 404, never sold as probabilities.

Endpoints

Routes and prices

GET

/v1/status

Free

Feed state, incidents and uptime

Parameters and filters

ParameterDetails
—No parameters.

Key response fields

state
Current feed state.
incidents
Feed interruptions.
uptime_30d_pct
Feed uptime over the observed part of 30 days.
GET

/v1/meta

Free

Model, coverage, prices and payment network

Parameters and filters

ParameterDetails
—No parameters.

Key response fields

public_beta
Whether paid access is enabled; also require payments.ready.
payments.ready
Payment readiness.
payments.prices
Current prices.
GET

/v1/coverage/{mint}

Free

Observed stage and reason for coverage, including unknown mints

Parameters and filters

ParameterDetails
mintToken mint.

Key response fields

stage
Observed coverage stage.
reason
Reason for coverage.
as_of
Observation time; an old observation is not a fresh probability.

Free mint coverage

GET /v1/coverage/{mint} returns hodlodds.coverage.v1, without payment headers and cacheable for 60 seconds. The stages are covered, experimental_non_sol, graduated_inactive, bonding_curve and unknown. Read the reason and observation time as_of: an old observation is not a fresh probability, and an inactive label does not prove there were no trades elsewhere. Unknown means not observed in the bounded directory, not that the mint does not exist. The directory is bounded to 8,000 entries or 832 KiB, whichever is reached first (newest creations, then recent and older inactive graduates); when a bound was reached, directory_truncated is true and an unlisted mint may still have been observed. Last trade time and graduation may be unknown; curve tokens have no crash probability. A risk route's free 404 includes the same coverage fields, plus its error and any available experimental information.

GET

/v1/risk/{mint}

$0.01 USDC

One covered SOL token

Parameters and filters

ParameterDetails
mintToken mint.

Key response fields

as_of
Completed one-minute bars up to this time (UTC epoch seconds).
issued_at
Snapshot issue time (UTC epoch seconds).
expires_at
Fresh expiry (UTC epoch seconds).
horizon_end
The forecast ends here (UTC epoch seconds); the window does not restart with your request.
row.p_crash_60m
Crash odds for the current row.
row.summary
Plain-English odds; last-known answers prefix the elapsed whole minutes. Low odds are not a safety signal.
row.confidence
Historical evidence; an ordinal presentation label, not a statistical confidence interval.
row.dd_60
Last SOL close / maximum completed-minute SOL close over the last 60 minutes − 1; 0 is the hour high, −0.996 is 99.6% below it.
row.already_crashed
True at dd_60 ≤ −0.90; false when not 90% below the trailing-hour high. An older collapse reads false: see crashed_within_24h. Null when drawdown is unknown.
row.crashed_within_24h / row.last_crash_as_of
Observed crash in an accepted snapshot within 24 hours, and its latest as_of (UTC epoch seconds, else null). Snapshots missed during outages are not seen; after a gap of more than an hour in accepted snapshots, a restore or a fresh start, both fields are null for up to 24 hours.
row.note
Present for either crash flag; below 10% odds the same recent-collapse warning appears in summary. Higher odds describe a further drop and the earlier collapse.

Responses carry as_of, issued_at, expires_at, horizon_end (UTC epoch seconds), model hashes, coverage, a historical calibration band and the current row. Each SOL row adds a summary: below 0.1% it says “under 0.1%”; below 10% it uses one decimal, otherwise a whole percent. Frequencies use 1,000 below 1%, otherwise 100. Last-known summaries start “Last known (N min old): ”. Experimental rows have no probability sentence. The forecast horizon does not restart when you request it. Band observations describe historical groups, not a confidence interval for one token.

dd_60 = close / max(close over the last 60 minutes) − 1. It is 0 at the hour high and −0.996 after a 99.6% fall. already_crashed is true when dd_60 ≤ −0.90, false when the token is not 90% or more below its high of the trailing 60 minutes; an older collapse reads false — see crashed_within_24h. It is null when drawdown is unknown. crashed_within_24h remembers this threshold in accepted snapshots for 24 hours; last_crash_as_of is the latest such snapshot timestamp, or null. Observation-based memory cannot see snapshots the cloud missed during outages; after a gap of more than an hour in accepted snapshots, a restore or a fresh start, both fields are null for up to 24 hours. If recent-crash memory is unavailable, both memory fields are null, the crash filter falls back to already_crashed only and the low-risk defaults reason ends with “recent-crash memory unavailable”; scores remain available. The crash context appears in both summary and note. When already_crashed is true, note reads: “This token already fell 90% or more from its high of the last 60 minutes. The odds cover a further 90% drop from the current price; they are not a recovery or safety signal.” Below 10% odds the summary ends with the same note; at higher odds it states the preceding collapse. Memory-only notes give the last observation time in UTC and say that low odds are not a safety signal. These fields do not change the model probability or coverage.

GET

/v1/risk/{mint}/last

$0.002 USDC

Opt-in last-known odds, at most 30 minutes old

Parameters and filters

ParameterDetails
mintToken mint.

Key response fields

as_of
Completed one-minute bars up to this time (UTC epoch seconds).
issued_at
Snapshot issue time (UTC epoch seconds).
expires_at
Fresh expiry (UTC epoch seconds).
horizon_end
The forecast ends here (UTC epoch seconds); the window does not restart with your request.
row.p_crash_60m
Crash odds for the current row.
row.summary
Plain-English odds; last-known answers prefix the elapsed whole minutes. Low odds are not a safety signal.
row.confidence
Historical evidence; an ordinal presentation label, not a statistical confidence interval.
row.dd_60
Last SOL close / maximum completed-minute SOL close over the last 60 minutes − 1; 0 is the hour high, −0.996 is 99.6% below it.
row.already_crashed
True at dd_60 ≤ −0.90; false when not 90% below the trailing-hour high. An older collapse reads false: see crashed_within_24h. Null when drawdown is unknown.
row.crashed_within_24h / row.last_crash_as_of
Observed crash in an accepted snapshot within 24 hours, and its latest as_of (UTC epoch seconds, else null). Snapshots missed during outages are not seen; after a gap of more than an hour in accepted snapshots, a restore or a fresh start, both fields are null for up to 24 hours.
row.note
Present for either crash flag; below 10% odds the same recent-collapse warning appears in summary. Higher odds describe a further drop and the earlier collapse.
stale
Whether the stored answer is stale.
age_s
Elapsed seconds since issued_at.
horizon_remaining_s
max(0, horizon_end - now)

Last-known odds: explicit opt-in

GET /v1/risk/{mint}/last sells the covered SOL row in the latest stored snapshot only when now - issued_at ≤ 1800 seconds. Exactly 1800 seconds is allowed; older data returns free HTTP 410 with error: "Last known odds too old", age_s and as_of. No stored snapshot returns 503. Unknown or experimental non-SOL tokens return free 404, with available experimental information.

Last-known odds are never sold as fresh. The response explicitly includes stale, freshness: "live" | "stale", age_s and horizon_remaining_s = max(0, horizon_end - now). Its horizon_end remains as_of + 3600; expires_at remains the original snapshot's fresh expiry, even when already past. This is an old forecast, not a new prediction.

When stale, confidence is downgraded one level: high → medium, medium → low, and low stays low. Its reasons add stale data: N min old, where N is the elapsed whole minutes. Experimental confidence is unchanged; experimental rows are never paid answers. Missing legacy confidence stays missing. The probability, historical evidence and underlying model do not change.

The route also works while the feed is live. In that case the odds equal the fresh route's odds, stale is false and confidence is unchanged. The price is still lower: choosing this cheaper route means explicitly accepting its last-known semantics on every request. There is no /v1/board/last. A fresh risk route's free 503 may include a hint and last_known_age_s when this alternative is eligible; the hint is not permission to pay or an automatic redirect.

GET

/v1/board

$0.005 USDC per delivered coin

Rank covered SOL tokens or watch your own list

Parameters and filters

ParameterDetails
mintsOptional comma-separated list of 1–100 distinct valid Solana mints. Duplicates return free 400. Sorted ascending in the payment binding.
limitWith mints: 1–100, default list size. Otherwise 10–500, default 50.
sortlowest_risk|riskiest|liquidity|newest|active; default lowest_risk. safest is a deprecated alias.
max_risk / min_risk0–1.
min_liquidity_solRecorded quote-side SOL reserve. Defaults to 5 for low-risk sorts without mints; explicit 0 overrides it.
min_age_min / max_age_minAge filters, in minutes.
min_trades_5Nonnegative integer. Defaults to 3 for low-risk sorts without mints; an explicit 0 disables the activity floor.
include_crashedtrue or false. Low-risk sorts without mints default to false; true includes both trailing-hour and recorded 24-hour crashes. Watchlists receive no implicit crash exclusion.
min_confidencehigh|medium|low.

Key response fields

as_of
Completed one-minute bars up to this time (UTC epoch seconds).
issued_at
Snapshot issue time (UTC epoch seconds).
expires_at
Fresh expiry (UTC epoch seconds).
horizon_end
The forecast ends here (UTC epoch seconds); the window does not restart with your request.
rows
Current fresh ranking; no pagination or cursors.
rows[].summary
Plain-English odds, computed when answering.
rows[].already_crashed / rows[].note
Same recent-collapse context as the single-token answer.
defaults_applied
Automatic min_trades_5, min_liquidity_sol and/or include_crashed filters with their reason; empty object when none apply. Also in the 402 JSON body.
excluded_by_defaults
Rows removed only by automatic defaults, before limit; also in the 402 JSON body, never in the signed board extra.
not_delivered
With mints, every requested mint omitted from rows: mint, stage and reason. Free coverage facts, including filtered-out or limit-excluded covered mints; no uncovered odds.
rows[].confidence
Numeric filters exclude null values; confidence filters exclude missing confidence.

One current snapshot, no pagination or cursors. limit=10..500 (default 50), sort=lowest_risk|riskiest|liquidity|newest|active (default lowest_risk). safest is a deprecated alias for lowest_risk, with identical ordering. The exact requested sort name stays in the offer and must match on retry and redeem. lowest_risk — lowest predicted odds first (a ranking, not a safety claim). Ties use mint ascending. Filters: max_risk/min_risk (0–1), min_liquidity_sol, min_age_min/max_age_min, min_trades_5 (nonnegative integer), min_confidence=high|medium|low. Numeric filters exclude null values; confidence filters exclude missing confidence. Liquidity means the recorded quote-side SOL reserve, not an executable depth guarantee.

For lowest_risk and safest without an explicit mints watchlist, omitted filters default to min_trades_5=3, min_liquidity_sol=5 and include_crashed=false. Any explicit value overrides the corresponding default; use min_trades_5=0, min_liquidity_sol=0 or include_crashed=true to include those rows. The single crash filter excludes both already_crashed and crashed_within_24h. Other sorts are unchanged unless a filter is explicit. The lowest odds are usually the least-traded tokens; the default filters remove most inactive or recently collapsed tokens, not all. The 402 JSON body and answer echo defaults_applied with a one-sentence reason (empty object when none), and excluded_by_defaults counts rows removed solely by automatic defaults before the limit. Response filters contain the effective values. The offer binds only the normalized caller-supplied query; the server applies the defaults deterministically when selecting and pricing rows at the challenge and paid-request steps. These explanations never enter extra.hodloddsBoard, which keeps exactly version, query, priced_n and nonce. Price and total_matches count only matching rows.

Price is $0.005 × delivered coins: 37 matches with limit 100 costs $0.185; 500 cost $2.50. Zero matches return free HTTP 200 with empty rows and no payment headers. The paid retry returns exactly the priced count from the current fresh ranking; rows may change. Fewer matches return free 409 matches changed; request again with code: offer_changed; nothing is charged, so request a new offer. Stale data returns free 503. The normalized query and count must match the requirement, and the server rechecks before settlement.

Watch your own list

Pass mints to track a portfolio in one call. All sorts and explicit filters still apply. Watchlists are exempt from all automatic low-risk filters: no activity or liquidity floor or crash exclusion hides a named holding. Explicit min_trades_5, min_liquidity_sol and include_crashed=false still filter the list; defaults_applied is empty and excluded_by_defaults is zero. Only delivered fresh SOL rows cost $0.005 each; uncovered entries are free and are not safety signals. The offer binds query.mints_sha256 (hex SHA-256 of the sorted, comma-joined ASCII mint list) and query.mints_count, without repeating the list. The server recomputes both from the URL on retry, before settlement and on redeem. Zero deliverable rows return free 200 with rows: [] and not_delivered.

GET /v1/board?mints=CY8tK7zPvoRAL5tugvxBEgFEtrgAXiikTAKVgh61Siq5,GrbVMxp8NHbeMq4WhpD1gwBX4ftCB21ZvfdB5viZhSry

Illustrative mints only. Ten delivered coins every minute cost $0.05/min = $3.00/h.

chain and platform accept one value or a comma-separated list (1..10 distinct values). Input is case-insensitive; chain accepts aliases or CAIP-2 IDs. AND across parameters, OR within each list. Repeated keys, empty items, duplicates, contradictions and unavailable values return free 400 before payment. Valid chain values: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (aliases: solana). Valid platform values/aliases: pumpfun, pump, pump.fun. The board defaults to available markets only; explicit filters do not enable experimental rows. Risk, last and coverage use these filters as coverage assertions. See /v1/markets. Explicit filters are bound to the paid offer.

Chains and platforms

chain and platform accept one value or a comma-separated list (1..10 distinct values). Input is case-insensitive; chain accepts aliases or CAIP-2 IDs. AND across parameters, OR within each list. Repeated keys, empty items, duplicates, contradictions and unavailable values return free 400 before payment. Valid chain values: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (aliases: solana). Valid platform values/aliases: pumpfun, pump, pump.fun. The board defaults to available markets only; explicit filters do not enable experimental rows. Risk, last and coverage use these filters as coverage assertions. See /v1/markets.

Read the free /v1/markets for model IDs, hashes, routes and prices. More chains and platforms may be added; /v1/markets is authoritative. Risk, last and coverage validate chain and platform; all other query parameters are ignored. Data filters are independent of the x402 payment network.

GET /v1/board?chain=solana&platform=pumpfun
GET /v1/risk/{mint}?chain=solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp&platform=pumpfun

Payment and failure behavior

  1. Send an unpaid request
  2. Read the 402 challengePAYMENT-REQUIRED
  3. Sign locallyPin the network, USDC asset, recipient and maximum total.
  4. Retry with the signed paymentPAYMENT-SIGNATURE → 200 + PAYMENT-RESPONSE

x402 v2 uses PAYMENT-REQUIRED, PAYMENT-SIGNATURE and PAYMENT-RESPONSE. Read /v1/meta and the current challenge for accepted payment networks, recipients and native USDC assets. When enabled, Solana and Base are alternative ways to pay the same price for the same covered SOL-token answer. Payment chain never expands model coverage. Solana devnet and Base Sepolia use test funds; they are not mainnet.

A connection failure after settlement may leave the answer undelivered. Keep the transaction signature and reconcile before paying again. Never share a wallet seed phrase with this service.

Request limits per client IP: free routes default to 60 requests per minute; paid routes have their own default budget of 300 requests per minute, including unpaid probes and signed requests. At most two signed paid requests may be in flight per client IP; a third gets a free 429 with Retry-After. Do not cache last-known odds as current odds.

Status codes

CodeMeaningCharged?
400400: unknown or invalid query parameters.No settlement.
402402: an eligible covered answer requires payment, or payment failed. Inspect PAYMENT-RESPONSE before retrying.Inspect PAYMENT-RESPONSE before retrying.
404404: unknown, uncovered or experimental non-SOL mint. No settlement. Empty boards return free 200.No settlement.
409409 with code: offer_changed: the board matching set or bound query changed between the 402 offer and the paid request. Nothing is charged; request a new offer. Other 409s can mean this signed payment was already claimed. No new settlement. For a replay, reconcile its original transaction before signing again.No new settlement.
410410: last-known odds exceed the 30-minute limit. No settlement.No settlement.
422422: malformed route input.No settlement.
429429: rate limited; respect Retry-After. No settlement.No settlement.
503503: stale/down feed on fresh-only routes, no stored snapshot, beta disabled, or service unavailable. No new settlement for an unavailable answer.No new settlement for an unavailable answer.

A refusal before any settlement attempt carries HodlOdds-Charged: no and JSON "charged": false. After signing, trust this signal only when PAYMENT-RESPONSE has neither success: true nor a transaction; a conflicting or malformed receipt remains ambiguous. The SDK saves no pending payment for a confirmed free refusal, and watch skips that cycle and continues. Other failed authorized requests remain ambiguous and stop watch.

How we charge

The normal risk and board routes remain fresh or free: only fresh, covered SOL answers settle. The separate opt-in last-known route may charge for a clearly labelled stored SOL answer no older than 30 minutes. Each route checks its age and coverage limits before payment work and again before settlement. Unknown, uncovered and experimental non-SOL answers are free refusals. Filters that return no rows are also free.

Risk and last-known have fixed per-answer prices; a board costs $0.005 per delivered coin, charged as one exact total. No subscription. Read current prices in /v1/meta and approve the exact x402 requirement. An already delivered signature is rejected with 409. A reconciled undelivered payment has one explicit redeem as described below. A lost connection after settlement can leave an answer undelivered: reconcile the receipt before signing again.

Redeem an undelivered answer

If an authorized call fails, keep its original PAYMENT-SIGNATURE private and check your wallet before signing another payment. The server checks uncertain settlements on-chain in the background. A receipt saying success: false with no transaction usually means nothing was charged; if your wallet shows payment, try redemption after about two minutes. Confirmation can take longer.

The Python SDK keeps the recovery credential in HodlOddsError.payment, excluded from its printed and JSON receipt fields. Explicitly call client.redeem(payment). The CLI saves an ambiguous attempt to a private 0600 file under ~/.config/hodlodds/pending/ and prints its path; run hodlodds redeem <file>. Neither automatically retries or signs again. Never log, upload or share that file or the signed header.

A manual redeem resends the identical header to the same route; boards require the same normalized query and original priced count. Add HodlOdds-Redeem: 1 to require recovery only, so an unknown payment cannot initiate settlement. Pending verification returns free 409 with Retry-After. A final unpaid result returns free 409 saying “this payment never settled; request a new offer”.

When a payment is confirmed on-chain and the server recorded its answer as not delivered (the client connection was already closed when the answer was ready, or the settlement was confirmed later by reconciliation), it can be redeemed once within 24 hours of its original verification. An answer the server completed and sent is recorded as delivered; a redeem of it returns 409, even if it was lost between the edge network and the client. The response is the current eligible answer and includes HodlOdds-Redeemed: <transaction>, with no second settlement. Current route freshness and coverage rules still apply. An answer outside its route's freshness limit, an uncovered answer, or a board with fewer matches than the original priced count is refused free and leaves the redeem available until expiry. Already redeemed or expired payments return 409. A lost redeem response cannot be delivered again.

Reference