Integration guide
Fifteen minutes to a sealed ledger.
Five integration paths, all exercised against a live frontier model before this page was written. Pick the one that matches your stack; they all land in the same tamper-evident chain.
Sign up, create a project
One project per AI system. You get a project id and mint an API key with ingest scope from the dashboard - the key is shown once and stored only as a hash.
Point your traces at us
Pick a path below. The OTLP route is usually two environment variables; the SDK adds the richer compliance events when you want them.
Verify and attest
Watch sessions appear as sealed traces, re-verify the chain any time, and generate evidence packs mapped to EU AI Act articles when a buyer or auditor asks.
OpenTelemetry (any language)
You already emit OTel traces - most agent frameworks do.
# Any OpenTelemetry exporter, any language - two env vars: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://YOUR_HOST/v1/otlp/<projectId>/v1/traces OTEL_EXPORTER_OTLP_TRACES_HEADERS=authorization=Bearer atst_YOUR_KEY # Spans using the GenAI semantic conventions (gen_ai.*) map automatically: # gen_ai.operation.name = chat -> model.call # gen_ai.operation.name = execute_tool -> tool.call # gen_ai.operation.name = invoke_agent -> decision # Token usage (gen_ai.usage.*) is carried into the sealed payload.
Zero code change: set two environment variables and your existing GenAI spans become sealed ledger events.
Vercel AI SDK
TypeScript agents on the ai package.
import { generateText } from "ai";
import { createAzure } from "@ai-sdk/azure";
// Register any OTel NodeTracerProvider with an OTLP exporter pointed
// at your AttestBase project endpoint - the two environment variables
// in the OpenTelemetry section above - then switch telemetry on.
// That is the whole integration:
const result = await generateText({
model: azure("gpt-5.5"),
tools: { policyLookup },
prompt: "Review claim CLM-1003 for approval.",
experimental_telemetry: { isEnabled: true },
});
// Every step - model calls, tool calls, agent decisions - arrives
// in AttestBase sealed, with token counts.The SDK's built-in telemetry does all the span work - we tested this against live gpt-5.5 with tools and multi-step loops.
Python
Python agents - OpenAI SDK, LangChain, or hand-rolled.
# pip install opentelemetry-sdk requests
# Note: Python's stock otlp-proto-http exporter emits protobuf only;
# the ingest endpoint speaks OTLP/HTTP JSON. This small JSON exporter
# keeps the rest of the stack (TracerProvider, spans, gen_ai.*) stock:
import requests
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor, SpanExporter, SpanExportResult,
)
def _val(v):
if isinstance(v, bool): return {"boolValue": v}
if isinstance(v, int): return {"intValue": str(v)}
if isinstance(v, float): return {"doubleValue": v}
return {"stringValue": str(v)}
class JsonOtlpExporter(SpanExporter):
def __init__(self, url, headers): self.url, self.headers = url, headers
def export(self, spans):
enc = []
for s in spans:
ctx = s.get_span_context()
span = {
"traceId": format(ctx.trace_id, "032x"),
"spanId": format(ctx.span_id, "016x"),
"name": s.name,
"startTimeUnixNano": str(s.start_time),
"endTimeUnixNano": str(s.end_time),
"attributes": [{"key": k, "value": _val(v)}
for k, v in (s.attributes or {}).items()],
}
if s.parent: span["parentSpanId"] = format(s.parent.span_id, "016x")
enc.append(span)
r = requests.post(self.url, headers=self.headers, timeout=30,
json={"resourceSpans": [{"scopeSpans": [{"spans": enc}]}]})
return SpanExportResult.SUCCESS if r.ok else SpanExportResult.FAILURE
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(JsonOtlpExporter(
url="https://YOUR_HOST/v1/otlp/<projectId>/v1/traces",
headers={"authorization": "Bearer atst_YOUR_KEY"},
)))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("claims-agent")
with tracer.start_as_current_span("chat") as span:
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.request.model", "gpt-5.5")
# ... call your model as usualStandard OpenTelemetry, no AttestBase-specific package required.
LangChain.js
You want per-callback control instead of spans.
import { AttestClient } from "@attest/sdk";
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
const attest = new AttestClient({ baseUrl, apiKey, projectId });
const run = attest.session(sessionId, "claims-agent");
class AttestHandler extends BaseCallbackHandler {
name = "attest";
handleLLMEnd(output, runId) {
run.modelCall("gpt-5.5", { runId, usage: output.llmOutput?.tokenUsage });
}
handleToolEnd(output, runId) {
run.toolCall("policy_lookup", { runId, result: String(output.content ?? output) });
}
}
// pass the handler in callbacks: [...] - nothing else changes;
// session() stamps occurredAt/systemId on every event for youA callback handler bridges LangChain events to the SDK - useful when you also want human.approval and incident.flagged events.
Native SDK
Maximum evidence: oversight, disclosures, incidents.
# set once in your agent's environment:
# ATTEST_API_KEY=atst_... ATTEST_PROJECT_ID=... ATTEST_BASE_URL=https://YOUR_HOST
import { attestFromEnv } from "@attest/sdk";
const attest = attestFromEnv(); // that's the whole setup
// already on OpenTelemetry? one line instead:
// new NodeSDK({ traceExporter: new AttestSpanExporter() })
const run = attest.session(crypto.randomUUID(), "claims-agent");
run.start({ task: "review claim CLM-1003" });
run.modelCall("gpt-5.5", { usage: { input: 91, output: 24 } });
run.toolCall("policy_lookup", { policy: "P-88341" });
run.decision({ payout: 12400, currency: "EUR" });
run.humanOverride("m.reeves", { action: "approved" });
run.disclosureShown({ channel: "chat-banner" });
run.end({ outcome: "settled" });
await attest.flush();
// batched + serialized + retried; every batch carries an idempotency
// key, so a retry after a lost response can never double-recordThe typed taxonomy (17 event types) is where compliance evidence gets richer than debugging traces: disclosure.shown, human.override, incident.flagged.
Verification and evidence
The point of the ledger is that anyone can check it.
# Prove the record whenever you like:
curl -H "authorization: Bearer atst_YOUR_KEY" \
https://YOUR_HOST/v1/projects/<projectId>/verify
# -> { "valid": true, "checkedEvents": 184209, "headHash": "..." }
# Evidence pack for an audit or questionnaire:
curl -X POST -H "authorization: Bearer atst_YOUR_KEY" \
-H "content-type: application/json" \
-d '{"framework":"eu-ai-act-art12-19","periodStart":"...","periodEnd":"..."}' \
https://YOUR_HOST/v1/projects/<projectId>/packs
# Everything is exportable - stream the full ledger, hash columns included,
# for independent re-verification or your own warehouse:
curl -H "authorization: Bearer atst_YOUR_KEY" \
"https://YOUR_HOST/v1/projects/<projectId>/export?format=jsonl" # or csvEnterprise deployment
Self-serve today, EU-dedicated or self-hosted at scale.
The pilot runs against our hosted EU endpoint with your own org, keys and retention policy. Enterprise plans add dedicated EU infrastructure or a self-hosted build inside your own VPC, DPA included - the ledger design is identical either way.