Decisions API
The deep decision endpoint, the free ledger read and the deterministic policy replay.
POST /v1/decisions (paid)
Deep decision: run any catalog signal (S01..S11) through the engine. Bills decision.deep.
curl -H "PAYMENT-SIGNATURE: <base64 payment payload>" -H 'content-type: application/json' \
https://api.tollbridge.app/v1/decisions \
-d '{"asset":"BTC","chain":"base","question_id":"S06_funding_anomaly"}'Response data: the full public decision record (question_id, event, horizon,
conditions, probability, confidence, decision, action, input_hash, state_hash,
policy_version, jev_version, choice).
Identical inputs under the same policy version re-serve the recorded decision (idempotency, dossier §6 stage 7) — a repeat call is not charged twice as a new decision row.
GET /v1/decisions/{decision_id} (free)
Fetch a recorded decision — the append-only ledger read, free.
curl https://api.tollbridge.app/v1/decisions/<decision_id>Unknown id → 404 DECISION_NOT_FOUND.
GET /v1/decisions/latest (free)
The newest recorded decision for a (question_id, asset) pair — the blotter
lookup the Deck and the /markets tiles hydrate from. Free, no payer: the
ledger is the product memory, not session state.
curl 'https://api.tollbridge.app/v1/decisions/latest?question_id=S01_market_regime&asset=BTC'Ordering law: the NEWEST record per (question_id, asset) wins (timestamp
desc, then decision id) — gate/refusal rows included, since a refusal is the
question's last word too. Empty ledger → typed 404 DECISION_NOT_FOUND
(the true empty, never a fabricated placeholder). Unknown question/asset →
typed 400. The response carries the same stored tile body as the
by-id read plus meta.freshness / meta.horizon (the envelope law: a
consumer reading meta alone still sees the claim's true input ages and its
horizon).
POST /v1/decisions/{decision_id}/replay (free)
Deterministic policy-only replay. Request {} or {"ruleset_version": "1.0.0"}.
curl -X POST https://api.tollbridge.app/v1/decisions/<decision_id>/replay \
-H 'content-type: application/json' \
-d '{}'Response data: { decision_id, original_verdict, replayed_verdict, matched_rules, policy_version, deterministic: true }. Replay re-verifies the journaled inputs against the
ledger row's input_hash and re-evaluates under the requested sealed ruleset; unverifiable
inputs → 409 REPLAY_INPUT_LOST (never a fabricated replay). See
Replay semantics.
TypeScript
import { Tollbridge } from '@tollbridge/sdk-typescript';
import { account } from './wallet';
const paying = new Tollbridge({ baseUrl: 'https://api.tollbridge.app', x402: { signer: account } });
const free = new Tollbridge({ baseUrl: 'https://api.tollbridge.app' });
const decision = await paying.decision({ asset: 'BTC', question_id: 'S06_funding_anomaly' });
decision.data.probability;
decision.data.payment_id; // payment ledger FK
const decisionId = '<decision_id>'; // from the deep-decision response
const past = await free.decisions(decisionId); // free ledger read
const replayed = await free.replay(past.data.decision_id); // deterministic replayPython
import httpx
# Reference snippets — official Python SDK in progress.
base = "https://api.tollbridge.app"
H = {"PAYMENT-SIGNATURE": payment_payload_b64, "content-type": "application/json"}
deep = httpx.post(
f"{base}/v1/decisions",
headers=H,
json={"asset": "BTC", "chain": "base", "question_id": "S06_funding_anomaly"},
).json()["data"]
record = httpx.get(f"{base}/v1/decisions/{deep['decision_id']}").json()["data"]
replay = httpx.post(f"{base}/v1/decisions/{deep['decision_id']}/replay", json={}).json()["data"]
assert replay["deterministic"] is TrueMCP
The deep decision is the tollbridge_decision tool — any catalog signal through the engine:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "tollbridge_decision",
"arguments": { "asset": "BTC", "chain": "base", "question_id": "S06_funding_anomaly" }
}
}The ledger read and replay are also proxied 1:1 as tollbridge_get_decision /
tollbridge_get_resolution / tollbridge_calibration:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "tollbridge_get_resolution", "arguments": { "decision_id": "<uuid>" } }
}The loop: GET /v1/resolutions/{decision_id} (free)
At horizon end the resolution engine re-derives the event from the same input grammar and
writes the outcome. Three honest states: obtains, not, or unresolvable (e.g. S06 with no
funding settlement in the window — unresolvable is a legal outcome, never coerced to not).
Before the horizon ends the route answers a dated { "status": "pending", "horizon_end": … }.
curl https://api.tollbridge.app/v1/resolutions/<decision_id>import httpx
r = httpx.get(f"https://api.tollbridge.app/v1/resolutions/`{decision_id}`").json()["data"]
# {"decision_id": ..., "outcome": "obtains|not|unresolvable", "resolved_at": ..., "statistic": ...}GET /v1/lineage/{decision_id} (free)
The evidentiary bundle: for one decision, the full record plus its resolution (or an honest
pending), the payment method, the per-input tape sources, the hashes, and a recomputed
request-hash verify block. This is a ledger read — no live model call, no second market
read — so every cell carries a source. Free; an 8-or-more-character id resolves by unique
prefix.
curl https://api.tollbridge.app/v1/lineage/<decision_id>verify is true, false, or unknown. Unknown is the honest absence (a record that
predates model tracing), never true. A decision with no traceable input or state hash
reports input_ok / state_ok as unknown rather than inventing a match.
// Reference snippet — the SDK facade method is coming; this uses fetch.
const res = await fetch('https://api.tollbridge.app/v1/lineage/<decision_id>');
const { data } = (await res.json()) as {
data: {
hashes: { input_hash: string | null };
verify: { input_ok: boolean | 'unknown' };
payment: { rail: 'x402' | 'credits' | 'none' };
};
};
data.hashes.input_hash; // the recorded input hash
data.verify.input_ok; // true | false | 'unknown'
data.payment.rail; // 'x402' | 'credits' | 'none'import httpx
bundle = httpx.get("https://api.tollbridge.app/v1/lineage/<decision_id>").json()["data"]
assert bundle["verify"]["input_ok"] in (True, False, "unknown")
print(bundle["tape"]["sources"]){
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "tollbridge_lineage", "arguments": { "decision_id": "<decision_id>" } }
}GET /v1/calibration (free)
Reliability buckets per question: predicted decile bands vs realized empirical frequency,
publication-gated. Below the per-question floor the bucket reports its counts with
published: false — "n too small" is data, not an error. Query params: question_id, subject.
curl "https://api.tollbridge.app/v1/calibration?question_id=S05_volatility_expansion"import httpx
c = httpx.get("https://api.tollbridge.app/v1/calibration", params={"question_id": "S05_volatility_expansion"}).json()["data"]
# {"buckets": [...], "published": false, "n": 7, "n_floor": 30, "note": "n too small ..."}GET /v1/clocks/open (free)
The open clocks: every pending decision across the live questions and canonical
assets (BTC · ETH · SOL), soonest horizon_end first — the same pending set the
web strip renders and the pending rows of the blotter carry, in one call. The
live set is derived, never a hand list; read GET /v1/signals for the
authoritative questions. Each clock carries {question_id, asset, question_label, decision_id, predicted, horizon_end}. An empty list is data
(nothing is in flight), not an error; a degraded upstream is a typed 503, never
an invented row. This route never asks and never spends credits.
curl https://api.tollbridge.app/v1/clocks/openimport httpx
clocks = httpx.get("https://api.tollbridge.app/v1/clocks/open").json()["data"]["clocks"]
# [] when nothing is pending; else [{"question_id": ..., "asset": ..., "decision_id": ..., "horizon_end": ...}]GET /v1/feed (free)
The terminal-chip tape: the newest resolved decisions, newest first. Each chip is
{decision_id, question_id, asset, output, predicted, realized, match, observed, status, resolved_at, horizon_end}. A choice row carries the predicted and realized
labels plus whether they matched; a probability row carries observed (true/false);
an unresolvable (no-claim) row is included, honestly labelled, with the label fields
null. No wallet and no model internals ever appear. limit defaults to 50 and is
capped at 200. Lurk here for free: this is where an agent learns the product is
alive before it pays for anything. Over MCP: tollbridge_feed.
curl "https://api.tollbridge.app/v1/feed?limit=50"import httpx
chips = httpx.get("https://api.tollbridge.app/v1/feed", params={"limit": 50}).json()["data"]["chips"]POST /v1/decisions/next (paid)
Ask the next dated sentence on the same question and asset as a decision you already
bought. Send {previous_decision_id}; the server reads the previous decision's
question and asset (and its subject wallet, for the account-scoped questions S09–S11)
and runs the same paid ask at the same price. It refuses, typed, while the previous
clock is still open (clock_open) — wait for horizon_end first. It is the relapse
path: an agent that already paid stays on the loop without re-reading the catalog.
curl -X POST https://api.tollbridge.app/v1/decisions/next \
-H 'content-type: application/json' \
-d '{"previous_decision_id":"<decision_id>"}'const next = await toll.request('POST', '/v1/decisions/next', {
body: { previous_decision_id: previousId },
});POST /v1/hooks (paid)
Arm a resolution hook. Send {decision_id, url}; when the watched clock writes its
resolution, the worker POSTs the ResolutionRecord to your URL, signed with
X-Tollbridge-Signature (HMAC-SHA256, one retry). The URL is checked against
loopback, link-local and private addresses when you register and again before every
delivery. An already-terminal clock answers 409 and points you at
GET /v1/resolutions/{decision_id} — a hook that can never fire is never silently
accepted. Priced at the decision price.
curl -X POST https://api.tollbridge.app/v1/hooks \
-H 'content-type: application/json' \
-d '{"decision_id":"<decision_id>","url":"https://example.com/tollbridge-hook"}'import httpx
resp = httpx.post(
"https://api.tollbridge.app/v1/hooks",
json={"decision_id": decision_id, "url": "https://example.com/tollbridge-hook"},
)POST /v1/calls (paid) — the duel
Lock your own call on an OPEN clock before its horizon. Send
{decision_id, call, call_probability?} where call is obtains or not; the
house's frozen probability is stored alongside your call, so the duel is fair by
construction — you saw the house's number before calling. Priced at the decision
price. A call is immutable after horizon_end (409, CLOCK_CLOSED), and one
call per payer per clock (409, CALL_ALREADY_LOCKED). At resolution the same
evaluator grades both the house probability and your call. It is a calibration
game — no wagering, no payout, no odds movement.
curl -X POST https://api.tollbridge.app/v1/calls \
-H 'content-type: application/json' \
-H 'x-tollbridge-payer: 0xYourAddress' \
-d '{"decision_id":"<decision_id>","call":"obtains","call_probability":0.4}'GET /v1/calls (free) — your duel scoreboard
Your calls graded against the house on identical events: calls, graded,
open, player_accuracy, house_accuracy, and both Brier scores. Every number
is a count of real graded calls; an empty record reports null accuracies — never
a fabricated zero. Requires the payer query parameter (your 20-byte address).
curl "https://api.tollbridge.app/v1/calls?payer=0xYourAddress"GET /v1/calls/scoreboard (free) — the aggregate board
The graded-call records across opted-in players, ranked by accuracy over a
minimum of 10 graded calls to appear. Strictly personal identifiers and counts —
no balance, spend or payment field is ever returned, so the board can never be
bought. Requires the payer query parameter.
curl "https://api.tollbridge.app/v1/calls/scoreboard?payer=0xYourAddress"GET /v1/scoreboard (free) — the public opt-in leaderboard
The PUBLIC leaderboard over graded player calls (?window=all|30d|7d). One row per opt-in
payer: graded accuracy (correct ÷ graded), calls graded, calls correct, and a derived,
abbreviated handle. Ranking is earned on the record only — no pay-to-rank: no billing,
balance, credit, payment or plan table is joined anywhere in the ranking query, so no spend
can move a rank. You appear only when you have opted in (see POST /v1/calls/opt-in).
curl "https://api.tollbridge.app/v1/scoreboard?window=all"A payer appears on the board only after 10 graded calls. Zero qualified players is an honest empty
board served as 200 with entries: [] — empty is data, not an error. The personal board
stays at GET /v1/calls/scoreboard; this is the public aggregate.
POST /v1/calls/opt-in (free) — record consent
Records your consent (default false) for a future public view of your scoreboard. No public board is served from this flag today — personal boards only; the flag is stored so the choice is yours and never assumed.
curl -X POST "https://api.tollbridge.app/v1/calls/opt-in" \
-H 'content-type: application/json' \
-d '{"payer":"0xYourAddress","opt_in":true}'Billing
Scout credits — the free taste. A payer address gets three free entry-priced questions (S01 Market regime, S05 Volatility expansion, S06 Funding anomaly), one time; credit balances and packs are separate — see Billing for the full credit surface.
Related
- The 16-field record anatomy and calibration honesty: Decisions.