5-minute quickstart: decision receipts for trading systems
Send Titan one bot signal before broker submission. Use the same kind of payload your bot would send toward a broker. Get back an allow/deny diagnostic decision, gate reasons, and a receipt you can inspect later.
This quickstart uses the public API. It does not require broker credentials, and Titan's hosted service never places broker orders — it decides; your own forwarder (if you run one) executes.
Titan is diagnostics infrastructure, not a trading availability SLA. Evaluate latency, errors, and fallback behavior in your own setup before relying on it in live automation.
External bot Decision Episode
External bots do not post a pre-built Decision Episode, and there is no separate Decision Episode write endpoint. Use Titan's existing public decision-receipt flow:
POST /api/public/v1/eval?eval_mode=fullwithContent-Type: application/jsonandX-Titan-Agent-Key: tak_your_agent_key_heresubmits the decision input.GET /api/public/v1/decisions/{trace_id}with the same agent-key header retrieves Titan's recorded decision receipt and itsreceipt_authenticationartifact.POST /api/public/v1/receipts/verifywith that exact artifact as the JSON body verifies the signed receipt. The public verifier does not use the agent key.
The agent key supplies the account and agent identity. Do not send account_id, agent_id, or broker credentials in the body.
Minimum valid request shape
The public evaluation contract requires these three fields at minimum:
{
"symbol": "AAPL",
"side": "BUY",
"close": 190.50
}Copyable request and synthetic reference
Titan's standard-library reference is examples/python/external_bot_decision_episode.py, with contract notes in docs/public-api/external-bot-decision-episode-quickstart.md. It adds a fixed, validated historical simulation context, runs only against a sandbox account with full-cascade evaluation enabled, and never calls a broker, forwarder, order, or fill endpoint. From a Titan source checkout, run it with python3 examples/python/external_bot_decision_episode.py.
The same public entry point accepts this copyable minimum request:
export TITAN_URL="https://www.titandiagnostics.io"
export TITAN_AGENT_KEY="tak_your_agent_key_here"
curl -sS "$TITAN_URL/api/public/v1/eval?eval_mode=full" \
-H "Content-Type: application/json" \
-H "X-Titan-Agent-Key: $TITAN_AGENT_KEY" \
-d '{"symbol":"AAPL","side":"BUY","close":190.50}'The source-checkout example then retrieves and authenticates the resulting receipt. Its representative evidence output (identifiers and the current evaluation result can vary) is:
evaluation: status=DIAGNOSTIC allow=False decision_id: <trace_id> receipt_id: <receipt_hash> evidence_tier: simulation receipt_verification: authentic
The stages remain separate:
| Stage | What this run establishes |
|---|---|
| Intent | Titan recorded the visibly synthetic input. |
| Titan evaluation | Titan recorded an evaluation and the exact returned receipt authenticated successfully. |
| Forwarding | No forwarding call is made; other forwarding evidence is unknown. |
| Broker acceptance | Unknown without separate broker evidence. |
| Execution | Unknown without separate execution evidence. |
| Fill | Unknown without separate fill evidence. |
| Outcome | Unknown without separate outcome evidence. |
Receipt authentication does not prove the input was correct, that a broker received or accepted an order, that execution or a fill occurred, or that an outcome was profitable. Hosted Titan records and evaluates; it does not place trades.
1. Create or use an agent key
In the Titan dashboard, open /admin/agents, create an agent, and copy the raw key shown once. It starts with tak_.
The agent key is the only credential you need. It binds your account server-side — do not send account_id or agent_id in the body, and never send broker API keys (Titan rejects them with BROKER_KEYS_NOT_ALLOWED).
The canonical API host is www — https://www.titandiagnostics.io. The bare apex (titandiagnostics.io) 307-redirects to www, and curl will not replay a POST body across a redirect unless you pass -L, so always point at the www host directly.
export TITAN_URL="https://www.titandiagnostics.io" export TITAN_AGENT_KEY="tak_your_agent_key_here"
2. Run one signal through Titan
POST /api/public/v1/eval?eval_mode=full is the front door — start here. The Python example below is the most reliable path (no shell-quoting pitfalls).
Python (recommended)
Install the one third-party package used by this snippet:
python3 -m pip install requests
import os
import requests
base_url = os.environ["TITAN_URL"].rstrip("/")
headers = {"X-Titan-Agent-Key": os.environ["TITAN_AGENT_KEY"]}
signal = {
"symbol": "AAPL",
"side": "BUY",
"close": 190.50,
"atr": 2.50,
"ts": "2026-06-07T15:00:00Z",
"strategy_hint": "quickstart",
}
resp = requests.post(
f"{base_url}/api/public/v1/eval?eval_mode=full",
json=signal,
headers=headers,
timeout=10,
)
resp.raise_for_status()
decision = resp.json()
print(decision["allow"], decision["reason_code"], decision["reason_label"])
print("trace_id:", decision["trace_id"])curl (macOS / Linux)
curl -sS "$TITAN_URL/api/public/v1/eval?eval_mode=full" \
-H "Content-Type: application/json" \
-H "X-Titan-Agent-Key: $TITAN_AGENT_KEY" \
-d '{
"symbol": "AAPL",
"side": "BUY",
"close": 190.50,
"atr": 2.50,
"ts": "2026-06-07T15:00:00Z",
"strategy_hint": "quickstart"
}'curl (Windows PowerShell)
Single-quoted JSON on the PowerShell command line is not reliable, and a UTF-8 BOM file is rejected as invalid JSON. Write the body to an ASCII (no-BOM) file and post it with --data-binary:
$env:TITAN_URL = "https://www.titandiagnostics.io"
$env:TITAN_AGENT_KEY = "tak_your_agent_key_here"
Set-Content -LiteralPath signal.json -NoNewline -Encoding ASCII -Value `
'{"symbol":"AAPL","side":"BUY","close":190.50,"atr":2.50,"ts":"2026-06-07T15:00:00Z","strategy_hint":"quickstart"}'
curl.exe -sS "$env:TITAN_URL/api/public/v1/eval?eval_mode=full" `
-H "Content-Type: application/json" `
-H "X-Titan-Agent-Key: $env:TITAN_AGENT_KEY" `
--data-binary "@signal.json"Example response
{
"ok": true,
"allow": true,
"status": "SUCCESS",
"reason": "WOULD_SUBMIT_ORDER",
"reason_code": "WOULD_SUBMIT_ORDER",
"reason_label": "Signal passed Titan's decision gates",
"eval_mode": "full",
"diagnostic_only": false,
"permission_bearing": true,
"deploy_gate_compatible": true,
"limited_coverage": false,
"coverage_scope": "full_pretrade",
"coverage_note": "Full pretrade diagnostics ran. Because this was eval-only, Titan did not enforce duplicate-signal or market-hours submission checks.",
"eval_only_limitations": [
"duplicate_signal_not_enforced",
"market_hours_not_enforced"
],
"decision_summary_line": "Your BUY signal for AAPL passed Titan's diagnostic checks. This decision was committed to Titan's append-only, tamper-evident audit chain; broker action, if any, is outside hosted Titan.",
"signal_source_fingerprint": "src_3f2a9c1d7b4e5a60",
"trace_id": "b1aef5a4174e4bf49acf8d6aaebaf1d6",
"decision_summary_url": "https://www.titandiagnostics.io/analytics/autopsy/b1aef5a4174e4bf49acf8d6aaebaf1d6",
"trace_viewer_url": "https://www.titandiagnostics.io/traces/b1aef5a4174e4bf49acf8d6aaebaf1d6",
"next_action_hint": "your_bot_may_proceed",
"tags": {
"agent_id": "agt_abc123",
"agent_name": "quickstart-bot",
"symbol": "AAPL",
"side": "BUY",
"source": "agent",
"eval_mode": "full"
}
}Use allow for the first decision:
allow: trueis returned only by a permission-bearing full cascade after Confirmed-Close Reentry V2 committedCLEARorREADY.reason_code: WOULD_SUBMIT_ORDERmeans the signal passed Titan's entry authority — it does not mean hosted Titan submitted anything, and it does not guarantee broker acceptance.allow: falsemeans do not trade; readreason_code,reason_label, andnext_action_hint.- Branch on
allow, not onstatus. Fast mode, sandbox mode, and nominal full mode withEVAL_FULL_CASCADEdisabled are diagnostic only: they returnallow: false,permission_bearing: false, and no proceed hint even when their sampled gates find no blocker. limited_coverage: falsewithcoverage_scope: "full_pretrade"means the full cascade ran. Eval-only behavior is reported separately ineval_only_limitations: duplicate-signal and market-hours submission checks are not enforced on/evalbecause eval commits an evaluation receipt, never executable work, and never executes.limited_coverage: truemeans diagnostic coverage was actually limited, for example fast mode, missing broker/clock state, or a self-hosted deployment whereeval_mode=fullis running only sizing/envelope diagnostics.- Hosted Titan evaluated this decision. Broker mutation, if any, is performed by the user-controlled forwarder.
3. Request fields
| Field | Required | Notes |
|---|---|---|
symbol | yes | Ticker; an exchange prefix like NASDAQ:AAPL is stripped to AAPL. |
side | yes | Resolves to BUY or EXIT. Aliases: buy/long → BUY; sell/close/close_long/exit/exit_long → EXIT. (So "side":"SELL" is accepted and means EXIT, not an error.) An EXIT closes the entire position for that symbol — there are no partial exits, and any quantity you send is ignored. Short-entry / sell-to-open signals are not supported; "side":"short" is rejected. |
close | yes | Reference price the signal was generated at. Must be a number > 0. |
atr | no | Average True Range. Number ≥ 0 if present. |
ts | no | ISO-8601 timestamp string. |
strategy_hint | no | Free-text label, ≤ 128 chars. |
signal_id | no (/signal) | Caller-supplied durable id for idempotent duplicate detection (see §4). |
Unknown fields are ignored. Titan accepts and silently drops keys it does not model — including quantity and notional. Titan sizes orders from its own configuration, so sending notional/quantity has no effect. Do not rely on them.
4. /eval vs /signal
POST /api/public/v1/eval | POST /api/public/v1/signal | |
|---|---|---|
| Purpose | One-shot pre-trade decision check | Record a live-style signal through the ingestion path |
| Evaluates gates? | Yes (eval_mode=full for sizing/envelope diagnostics) | Yes (full live cascade) |
| Creates a trace/receipt? | Yes | Yes |
| Submits broker orders? | No (hosted Titan never does) | No (hosted Titan never does; your forwarder executes if you run one) |
| Start here? | Yes | Only once you want the ingestion/forwarder path |
Most bots only need /eval. Use /signal when you specifically want the live ingestion behavior.
eval_mode: fast vs full. /eval defaults to eval_mode=fast when the parameter is omitted, and unknown values are also treated as fast. Fast mode skips the broker-state-dependent gates and is explicitly diagnostic: it returns no permission and no proceed hint, and its receipt is not committed to the audit chain or Bitcoin-anchored. eval_mode=full can be permission-bearing only when the full cascade is enabled, the account is in enforce or observe mode, and Confirmed-Close Reentry V2 commits exact CLEAR or READY. Sandbox remains diagnostic. Eval records are never executable work and never enter /v1/forwarder/pending, even when allow: true. Self-hosted installs enable the cascade with EVAL_FULL_CASCADE=1; without it, nominal full mode returns coverage_scope: "sizing_envelope_diagnostics", limited_coverage: true, permission_bearing: false, and allow: false. Use eval_mode=full when you need an authoritative pre-trade answer; use fast only for a summary diagnostic.
curl -sS "$TITAN_URL/api/public/v1/signal" \
-H "Content-Type: application/json" \
-H "X-Titan-Agent-Key: $TITAN_AGENT_KEY" \
-d '{
"symbol": "AAPL",
"side": "BUY",
"close": 190.50,
"atr": 2.50,
"signal_id": "quickstart-001"
}'What you actually get on a fresh account depends on broker state. With no live forwarder pushing state, /signal commonly returns:
{
"ok": true,
"status": "REJECTED",
"forwarded": false,
"reason": "no_broker_state",
"reason_code": "no_broker_state",
"reason_label": "Broker state unavailable",
"trace_id": "quickstart-001",
"symbol": "AAPL",
"side": "BUY",
"blocked_by_gates": [],
"next_action_hint": null
}This is expected: drift/position gates need a recent broker-state snapshot, which only exists once your forwarder is connected and pushing. Until then, use /eval for decision checks. When state is available and the signal passes, you'll see status: "DRY_RUN" (sandbox/observe) or SUCCESS with forwarded: true and reason_label: "Signal passed Titan's decision gates".
If the market is closed, /signal may block at MARKET_CLOSED before broker-state checks. Keep /eval?eval_mode=full as the first-run path when you are just confirming that the agent key and receipt flow work.
Duplicate signal_id. If you re-send the same signal_id (or titan_signal_id), /signal returns 200 with status: "REJECTED", reason: "DUPLICATE_SIGNAL_ID", and duplicate: true. This is a deliberate duplicate-reject, not a silent success and not a replay of the original decision. The trap to know: the duplicate rejection happens before gate evaluation, so the duplicate submission produces no decision receipt of its own and is never chained or anchored — fetching a receipt for a duplicate submission returns 404. Only the ORIGINAL signal's trace_id has a fetchable receipt (§6). When Titan can resolve the original within your account, the duplicate response's original_trace_id (mirrored into trace_id) carries the original submission's trace id; when it cannot, both are null. Use a fresh signal_id per distinct signal.
Keep the returned trace_id.
5. Note: the response is the receipt's summary
The /eval and /signal responses are the decision summary. The full receipt — including the redacted signal, the gate-by-gate outcomes, the execution environment, and the proof boundary — is fetched by trace_id in the next step.
6. Retrieve the decision receipt
curl -sS "$TITAN_URL/api/public/v1/decisions/b1aef5a4174e4bf49acf8d6aaebaf1d6" \ -H "X-Titan-Agent-Key: $TITAN_AGENT_KEY"
{
"trace_id": "b1aef5a4174e4bf49acf8d6aaebaf1d6",
"decision": "SUCCESS",
"allow": true,
"decision_summary_line": "At 6:00:48 PM UTC your BUY signal for AAPL passed Titan's diagnostic checks. This decision was committed to Titan's append-only, tamper-evident audit chain; the detailed record is retained under the account's retention policy.",
"reason": "WOULD_SUBMIT_ORDER",
"reason_code": "WOULD_SUBMIT_ORDER",
"reason_label": "Signal passed Titan's decision gates",
"symbol": "AAPL",
"side": "BUY",
"evaluated_at": "2026-06-07T18:00:48.125945+00:00",
"signal": {
"raw_redacted": { "symbol": "AAPL", "side": "BUY", "close": 190.50, "atr": 2.50, "strategy_hint": "quickstart" },
"normalized": { "symbol": "AAPL", "side": "BUY", "close": 190.50, "atr": 2.50 }
},
"gates": [
{ "name": "duplicate_detection", "status": "pass", "reason": null },
{ "name": "side_valid", "status": "pass", "reason": null },
{ "name": "market_open", "status": "pass", "reason": null }
],
"gates_note": null,
"decision_signature": "12f7aa66297e43db…",
"eval_hash": "0a84a1dd4e7ad742…",
"input_hash": "65e2ce17c65a39e2…",
"config_hash": "4de62bf3a4319e8d",
"versions": { "engine": "render-paper", "strategy": "S1.0", "diagnostics": "D1.0" },
"state_source": null,
"state_age_ms": null,
"signal_source_fingerprint": "src_3f2a9c1d7b4e5a60",
"execution_environment": {
"trading_mode": "sandbox",
"dry_run": true,
"would_forward": false,
"money_at_risk": false,
"settles_via": "forwarder",
"hosted_broker_mutation": false,
"explanation": "Hosted Titan evaluated this decision. Broker mutation, if any, is performed by the user-controlled forwarder."
},
"proof_boundary": {
"proves": [
"Titan received this signal.",
"Titan evaluated it against these decision gates.",
"Titan recorded a timestamped decision commitment, anchored to Bitcoin once the daily root is built."
],
"does_not_prove": [
"That a broker accepted, filled, or even received an order.",
"That the decision was profitable or strategically sound.",
"That the submitted signal input was correct or complete.",
"Legal or compliance-grade nonrepudiation."
]
},
"audit_anchor": {
"state": "awaiting_daily_root",
"block_height": null,
"submitted_at": null,
"confirmed_at": null,
"proof_available": false,
"anchor_provider": null,
"proof_url": null,
"proof_bundle_url": "https://www.titandiagnostics.io/api/public/v1/decisions/b1aef5a4174e4bf49acf8d6aaebaf1d6/proof-bundle"
}
}Notes:
signal.raw_redactedis your inbound payload with reserved/credential and account-binding fields stripped;signal.normalizedis the canonical form the engine used. If the payload was not retained,signal.unavailableis set instead.gates[]lists each gate's outcome (pass/pass_with_warning/fail/skipped) with an operator-languagereason. A blocked decision shows the failing gate here. When no gate-level detail was recorded,gatesis empty andgates_noteexplains why.execution_environment.hosted_broker_mutationis alwaysfalse, andmoney_at_riskis only everfalseor"unknown"— the cloud can prove a decision was not against real money, but never that money moved.
7. Download proof if available
Fresh receipts usually do not have proof bytes yet. Check audit_anchor.proof_available first.
curl -fS "$TITAN_URL/api/public/v1/decisions/b1aef5a4174e4bf49acf8d6aaebaf1d6/audit-anchor.ots" \ -H "X-Titan-Agent-Key: $TITAN_AGENT_KEY" \ -o "decision.audit-anchor.ots"
If proof is not ready, the API returns 404 with:
{
"error": {
"code": "NO_PROOF_AVAILABLE",
"message": "No downloadable proof for this decision yet.",
"details": {
"anchor_state": "awaiting_daily_root"
}
}
}How eval_hash is computed (recompute it yourself)
eval_hash is not opaque. It is sha256(canonical_json(view)) over the decision's recorded envelope, where:
- view = the envelope with volatile keys removed (per-event identity such as
event_id/trace_id/created_at, replay identity, timing fields, any key startingtime_or ending_ms,build_sha,diagnostics, and the surfacedexecution_environmentblock — the committedtrading_modeis bound viaconfig_hashinstead), then post-normalized: lists whose items are all strings are sorted, and floats are rounded to 6 decimal places.NaN/Infinitybecome the strings"NaN"/"Inf"/"-Inf". - canonical_json = JSON with keys sorted and separators
(",", ":"), UTF-8 encoded.
A proof bundle — fetchable self-serve at GET /api/public/v1/decisions/{trace_id}/proof-bundle for a chained decision (also linked from the receipt's audit_anchor.proof_bundle_url), and verifiable at /verify-decision — embeds the envelope, the exact exclusion lists under eval_hash_canonicalization, and an eval_hash_recompute_match flag, so you can recompute the hash offline with no Titan access. When the account's retention policy has purged the detailed envelope, the bundle says so honestly (envelope: null + envelope_note) — the anchored commitment still verifies, but recomputation needs the envelope bytes.
8. Errors and conventions
All public API errors use one envelope: {"error": {"code": ..., "message": ...}} (validation errors add a details array). This includes oversized bodies (413) — a JSON API never returns HTML here.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_JSON_OBJECT | Body wasn't a JSON object. On Windows, this usually means a BOM or shell-quoting issue — use an ASCII no-BOM file (§2). |
| 400 | BROKER_KEYS_NOT_ALLOWED | Do not send broker API keys or secrets to hosted Titan. |
| 401 | AGENT_KEY_REQUIRED | Add the X-Titan-Agent-Key header. |
| 401 | INVALID_AGENT_KEY | The key is malformed, revoked, or does not exist. |
| 413 | REQUEST_TOO_LARGE | Body exceeds the 1 MB limit. |
| 422 | VALIDATION_ERROR | A required field is missing or has the wrong type; see details[]. |
| 404 | NOT_FOUND | The trace/receipt does not exist for this agent/account (existence is hidden cross-tenant). |
| 404 | NO_PROOF_AVAILABLE | The receipt exists but no downloadable proof is ready. |
| 429 | AGENT_RATE_LIMIT | Per-key rate limit exceeded; honor the Retry-After header and retry. |
Conventions:
- Content type: send
Content-Type: application/json. Titan currently also accepts a JSON-looking body without the header, but this tolerance is not guaranteed — set the header. - Unknown fields are ignored (§3).
- Rate limiting is per agent key and applies to authenticated requests. Repeated *invalid*-key requests fail auth (
401) before the limiter, so they will not produce a429. When you do hit the limit, the429includesRetry-After.
9. Optional: attach a forwarder-reported outcome
If your own forwarder later submits to a broker, it can attach the outcome to a trace:
curl -sS "$TITAN_URL/tv/execution_result" \
-H "Content-Type: application/json" \
-H "X-Titan-Agent-Key: $TITAN_AGENT_KEY" \
-d '{
"trace_id": "b1aef5a4174e4bf49acf8d6aaebaf1d6",
"order_id": "broker-order-123",
"symbol": "AAPL",
"action": "buy",
"status": "filled",
"submitted_price": 190.50,
"filled_price": 190.52,
"qty": 1,
"filled_at": "2026-06-07T15:00:03Z"
}'Read it back:
curl -sS "$TITAN_URL/v1/traces/b1aef5a4174e4bf49acf8d6aaebaf1d6/execution" \ -H "X-Titan-Agent-Key: $TITAN_AGENT_KEY"
Forwarder-reported broker outcomes are attached evidence, not independent broker truth.
What Titan proves and does not prove
Titan can give you:
- an allow/deny decision for the submitted signal
- gate reasons and stable reason codes
- a decision receipt you can retrieve later, subject to retention policy
- receipt commitments and proof artifacts when the audit chain has produced them
Titan does not prove:
- that the signal input was correct or complete
- that the decision was profitable or strategically sound
- that a broker accepted or filled an order
- that hosted Titan placed a trade
- that margin or order acceptance was guaranteed
- that detailed decision event rows are retained forever
Detailed decision event data follows account retention policy. Receipt commitments and proof artifacts are timestamped evidence commitments, not proof of correctness, fills, P/L, or broker execution.