Tollbridge Docs
API

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 replay

Python

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 True

MCP

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/open
import 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.

  • The 16-field record anatomy and calibration honesty: Decisions.

On this page