Quickstart · developers

Verified Evidence Quickstart

Turn agent traces into signed evidence for a Verified Observed Choir Receipt. OpenAI Responses, LangGraph and CrewAI all produce the same signed bundle format.

The five-step flow

From run trace to Verified Receipt

  1. 01
    Create a Score

    A Score declares Voices, expectations and dimensions. It is the unit of assurance — every Receipt is attached to one Score.

  2. 02
    Generate an evidence signing secret

    From your Score (owner only) create a signing secret. It is shown once; the server stores only what it needs to verify. You also get the per-Score ingest URL.

  3. 03
    Convert your trace using an adapter

    Run your agent system, capture the run trace JSON, and pass it through openAIResponsesToEvidence, langGraphStreamToEvidence or crewAITraceToEvidence to produce an ObservedEvidenceBundle.

  4. 04
    Sign and POST the evidence bundle

    Canonicalise the bundle, HMAC-SHA256 it with the signing secret, and POST to /api/public/evidence/$ingest with the three Agentic Choir headers.

  5. 05
    Generate a Verified Observed Choir Receipt

    Return to your Score and generate a new Receipt using the accepted evidence. The Receipt is labelled Verified Observed Choir Receipt and the public verify page shows both Receipt-hash verification and evidence-bundle verification.

What you need

Prerequisites

Adapter examples

Three frameworks, one evidence bundle

openai.responses.v1

OpenAI Responses

openAIResponsesToEvidence from src/lib/adapters/openai-responses.ts

Maps
  • function_call / tool_call / file_search_call / web_search_call / computer_call / code_interpreter_call → tool_call
  • refusal / output_refusal content → warning
  • file_citation / url_citation / container_file_citation / file_path annotations → evidence_cited
  • reasoning items are ignored by design
  • output_text without citations → no event
Refuses to infer
  • Missed Entry
  • Authority Breach
  • Unsupported Claim
  • False Harmony
  • role failure
import { openAIResponsesToEvidence } from "@/lib/adapters/openai-responses";

const { bundle, adapter_warnings, unknown_voices } =
  openAIResponsesToEvidence(
    {
      run_label: "Refund bot · run 7",
      default_voice_name: "Assistant",
      trace: responsesTraceJson, // the raw Responses object
    },
    scoreVoiceNames, // optional: cross-check against your Score
  );

// bundle is ready to canonicalise + sign.

The returned bundle must be signed before POSTing — see the signing section below.

langgraph.stream.v1

LangGraph

langGraphStreamToEvidence from src/lib/adapters/langgraph.ts

Maps
  • node returning tool_calls (directly or inside a message) → tool_call
  • guardrail-named node + flagged/blocked/refused output → warning
  • retrieval-named node + documents/citations/sources/context → one evidence_cited per item
  • escalation/handoff/interrupt node fired → escalation
  • critic/reviewer node + disagree/objection/rejected → dissent
  • plain assistant message alone → no event
Refuses to infer
  • Missed Entry
  • Authority Breach
  • Unsupported Claim
  • False Harmony
  • role failure
import { langGraphStreamToEvidence } from "@/lib/adapters/langgraph";

const { bundle } = langGraphStreamToEvidence({
  run_label: "Triage graph · run 12",
  default_voice_name: "Assistant",
  trace: langGraphStreamItems, // updates-mode or astream_events array
});

The returned bundle must be signed before POSTing — see the signing section below.

crewai.kickoff.v1

CrewAI

crewAITraceToEvidence from src/lib/adapters/crewai.ts

Maps
  • tool_call / tool_use / tool_execution record → tool_call
  • delegation / handoff / manager allocation (including "Delegate work to coworker" tool) → escalation
  • task output with citations/sources/knowledge/references → one evidence_cited per item
  • safety/guardrail/refusal/blocked/unsafe/policy event → warning
  • critic/reviewer record with objection/disagreement/rejection → dissent (approved reviewer is not dissent)
  • plain final-answer string → no event
Refuses to infer
  • Missed Entry
  • Authority Breach
  • Unsupported Claim
  • False Harmony
  • role failure
import { crewAITraceToEvidence } from "@/lib/adapters/crewai";

const { bundle } = crewAITraceToEvidence({
  run_label: "Refund crew · run 3",
  default_voice_name: "Assistant",
  trace: crewKickoffJson, // kickoff result, events, or tasks_output
});

The returned bundle must be signed before POSTing — see the signing section below.

Combine multiple runs with mergeBundles

Deterministic, preserves event order, collapses only exact structural duplicates. The merged result is still a valid ObservedEvidenceBundle and can be signed and submitted in one POST.

import { mergeBundles } from "@/lib/adapters/types";

// Combine output from multiple frameworks or run segments into one
// signed-evidence submission. Deterministic, preserves event order,
// collapses only exact structural duplicates.
const merged = mergeBundles(openAiBundle, langGraphBundle, crewAiBundle);
Signing

Node 20+ HMAC-SHA256 signer

The canonical signing string is `${timestamp}.${canonical_json_payload}` — keys sorted recursively, no whitespace. The signature is lowercase hex HMAC-SHA256. Placeholders below: ACH_INGEST_URL, ACH_EVIDENCE_SECRET, ACH_SECRET_PREFIX. Never commit the raw secret to source control.

// Node 20+ — no external dependencies.
import { createHmac } from "node:crypto";

// 1. Canonical JSON: keys sorted recursively, no whitespace. Match the
//    canonicaliser the server uses (src/lib/choir/hash.ts).
function canonicalStringify(value) {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return "[" + value.map(canonicalStringify).join(",") + "]";
  const keys = Object.keys(value).sort();
  return "{" + keys.map(k => JSON.stringify(k) + ":" + canonicalStringify(value[k])).join(",") + "}";
}

const ACH_INGEST_URL    = process.env.ACH_INGEST_URL;     // https://agenticchoir.com/api/public/evidence/<your-ingest-slug>
const ACH_EVIDENCE_SECRET = process.env.ACH_EVIDENCE_SECRET; // shown ONCE at creation; never log
const ACH_SECRET_PREFIX = process.env.ACH_SECRET_PREFIX;  // first segment of the secret, displayed by the panel

const payload = { run_label: bundle.run_label, events: bundle.events };
const timestamp = Math.floor(Date.now() / 1000);
const signingString = `${timestamp}.${canonicalStringify(payload)}`;
const signature = createHmac("sha256", ACH_EVIDENCE_SECRET)
  .update(signingString)
  .digest("hex");

const res = await fetch(ACH_INGEST_URL, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Agentic-Choir-Secret-Prefix": ACH_SECRET_PREFIX,
    "X-Agentic-Choir-Timestamp": String(timestamp),
    "X-Agentic-Choir-Signature": signature,
  },
  body: JSON.stringify(payload),
});

const json = await res.json();
// { accepted: true, duplicate: false, evidence_slug: "..." }
// On duplicate within 24h: { accepted: true, duplicate: true, evidence_slug: "..." (same slug) }
Submit

POST to the ingest endpoint

POST the exact bytes you signed to /api/public/evidence/$ingest. The endpoint is per-Score; the slug is the same one the Evidence Signing panel showed you.

curl -X POST "$ACH_INGEST_URL" \
  -H "Content-Type: application/json" \
  -H "X-Agentic-Choir-Secret-Prefix: $ACH_SECRET_PREFIX" \
  -H "X-Agentic-Choir-Timestamp: $TIMESTAMP" \
  -H "X-Agentic-Choir-Signature: $SIGNATURE" \
  --data-binary @payload.canonical.json
Valid signed payload

Accepted. Stored with verification_status = signature_valid. The next Receipt generated against this Score that includes the bundle is labelled Verified Observed Choir Receipt.

Duplicate within 24h

Same canonical payload + same Score: returns { accepted: true, duplicate: true } and the original evidence_slug. Safe to retry on network errors.

Invalid signature

Rejected. Common causes: wrong secret, payload was modified after signing, or the canonical form differs from the server's.

Expired or future timestamp

Rejected. Tolerance is ±600 seconds. Clock skew on the signer is the usual culprit.

Payload too large

Rejected. Limits: 64 KiB body, max 200 events per bundle. Split into multiple signed submissions or mergeBundles before signing.

Receipt

Generate the Verified Observed Choir Receipt

After the signed evidence is accepted, return to your Score and generate a new Receipt using that evidence. The Receipt is labelled Verified Observed Choir Receipt. The public verify page shows both Receipt-hash verification and evidence-bundle verification side by side, so any third party can confirm payload integrity without holding the secret.

What verification proves

Honest limits

Signed evidence proves
  • Payload integrity at submission time
  • Possession of the Score's signing secret
  • Timestamp within the accepted tolerance window
  • The stored Receipt payload still matches its recorded hash
Signed evidence does not prove
  • That the underlying agent run actually occurred
  • That the events are a complete transcript
  • That the underlying model is safe
  • Compliance certification of any kind
  • Absence of false harmony in the ensemble