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.

STEP 1

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.

STEP 2

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.

STEP 3

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 usual

Standard 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 you

A 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-record

The 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 csv

Enterprise 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.