5-minute quickstart: decision receipts for trading bots
Decision receipts for trading bots. Send Titan a signal. Get back an allow/deny 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.
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. Ask Titan for a full allow/deny eval
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",
"deploy_gate_compatible": true,
"limited_coverage": false,
"coverage_scope": "full_pretrade",
"coverage_note": "Full pretrade includes sizing/envelope diagnostics but still never executes broker orders.",
"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: truemeans Titan did not block this signal in the requested eval.reason_code: WOULD_SUBMIT_ORDERmeans the signal passed every gate — it does not mean hosted Titan submitted anything.allow: falsemeans do not trade; readreason_code,reason_label, andnext_action_hint.- 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/short/close/close_long/exit/exit_long → EXIT. (So "side":"SELL" is accepted and means EXIT, not an error.) |
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.
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, /signal returns 200 with status: "REJECTED" and reason: "DUPLICATE_SIGNAL_ID". This is a deliberate duplicate-reject, not a silent success and not a replay of the original decision — to fetch the original, look it up by its trace_id (§6). 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,
"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,
"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
}
}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"
}
}
}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 trace 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 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, profitability, or broker execution.