Four steps: get a key, send your agent’s first decision, read the signed record it left, and verify that record yourself. About ten minutes with curl; no SDK needed.
During the design-partner phase we issue keys by hand. Email autopsy@queryfanout.dev with your team and what your agent does. You get:
Keys are tenant-scoped: you only ever see your own records. Keep the key in an environment variable or secret store, never in code or a prompt. Check it:
export QF_URL=https://queryfanout.dev
export QF_KEY=... # your writer key
curl -s "$QF_URL/auth/whoami" -H "X-API-Key: $QF_KEY"
# {"tenant_id": "your-tenant", "role": "writer", "permissions": ["audit:read", "decision:write", ...]}
Your agent already found some sources. Send the question and those sources to POST /policy/evaluate. Query Fanout runs no search; it scores what you sent, decides answer or escalate, and records the decision.
curl -s -X POST "$QF_URL/policy/evaluate" \
-H "X-API-Key: $QF_KEY" -H "Content-Type: application/json" \
-d '{
"query": "What is Yuma Consensus in Bittensor?",
"policy": "support",
"agent_id": "subnet-agent",
"run_id": "quickstart-1",
"sources": [
{"title": "Yuma Consensus | Bittensor docs",
"url": "https://docs.learnbittensor.org/learn/yuma-consensus",
"snippet": "Yuma Consensus is the algorithm that turns the weights that validators set into emissions for miners and validators on each subnet."},
{"title": "opentensor/subtensor",
"url": "https://github.com/opentensor/subtensor",
"snippet": "Subtensor implements Yuma Consensus, which computes miner and validator emissions on each subnet from the weights validators set."}
]
}'
Response (trimmed):
{
"allow_answer": true,
"audit_id": "28c80253-596b-4692-...",
"overall_confidence": 0.7433,
"policy": {"profile": "support", "action": "allow", "reasons": ["All policy checks passed."],
"metrics": {"source_count": 2, "top_authority": 0.92, "scorer": "qf-score-v2"}},
"evidence": [
{"title": "Yuma Consensus | Bittensor docs", "confidence": 0.885, "authority": 0.92, "source_type": "web"},
{"title": "opentensor/subtensor", "confidence": 0.8035, "authority": 0.92, "source_type": "web"}
]
}
allow_answer: true → let the agent answer. false → hand off, and show policy.reasons (every reason explained).policy: support (default) escalates on thin evidence; legal needs 3 sources and 75% authority and blocks; research and calibrated are more permissive. See presets.audit_id — it is how you or a reviewer finds this decision later. Use one run_id per agent run to group its decisions."source_type": "agent", "producer" and "cites" to those sources, and send the pages they cite too. Agent-only input with no verified citation escalates (agent_only_uncited). How agent sources are scored →curl -s "$QF_URL/audit/AUDIT_ID/replay" -H "X-API-Key: $QF_KEY"
The replay shows the question, every source the agent sent with its score breakdown (scoring: relevance, authority and the rule that set it, agreement, and for agent sources their citations and trust weight), the exact policy definition used, the outcome, and:
verification — this record’s hash, Ed25519 signature, chain links to its neighbours, and Merkle inclusion (pending until the next signed checkpoint covers it);reevaluation — the recorded policy re-run on the recorded evidence, and whether it reproduces the recorded decision.Find decisions without an id: GET /audit?run_id=quickstart-1, or filter by agent_id, allow_answer, policy_action, time range or text. Reviewers can do the same in the browser in the auditor workspace with a read-only key.
Don’t take our word for it. Download a signed evidence pack and check it offline against public keys you saved earlier:
curl -s "$QF_URL/audit/evidence-pack?run_id=quickstart-1" -H "X-API-Key: $QF_KEY" -o pack.zip curl -s "$QF_URL/audit/keys" -o keys.json # pin once, store out of band pip install "qf-verify[tsa]" qf-verify pack.zip --trusted-keys-file keys.json # VERIFIED: 1 record(s) checked, 0 failure(s)
Change one byte of a record in the pack and qf-verify fails. Try it on the live demo without a key.
import os, requests
def gate(question, sources, run_id):
r = requests.post(
os.environ["QF_URL"] + "/policy/evaluate",
headers={"X-API-Key": os.environ["QF_KEY"]},
json={"query": question, "policy": "support", "agent_id": "subnet-agent",
"run_id": run_id, "sources": sources},
timeout=30,
)
r.raise_for_status()
return r.json()
d = gate(question, sources, run_id)
if d["allow_answer"]:
answer(question)
else:
escalate(d["audit_id"], d["policy"]["reasons"])