---
title: Integrate your agent framework with taleseal
description: Point your framework's OpenTelemetry exporter at taleseal and every agent run becomes a draft tale — title, beats, tool receipts and token counts, composed from the trace.
canonical_url: https://taleseal.com/integrate
last_updated: 2026-07-13
---

# Integrate your agent framework with taleseal

taleseal is an OTLP/HTTP sink. If your agent framework emits OpenTelemetry GenAI spans —
Vercel AI SDK 7, Mastra, Pydantic AI and OpenLLMetry all do — you point the exporter at
`https://taleseal.com/v1/traces` with an API key, run your agent, and about two minutes
after the run goes quiet a **draft tale** appears in your dashboard: title, beats, tool
receipts, token counts and the span timeline, composed deterministically from the trace.

Both OTLP/HTTP encodings are accepted (`application/x-protobuf` and `application/json`),
gzip request bodies work, and `/v1/metrics` + `/v1/logs` are accept-and-drop stubs, so an
auto-instrumented process pointing all three signals at us never sees 404s. gRPC is not
supported.

## Pick your framework

Each guide has pinned, tested versions, a copy-paste config, a test run that needs **no
model API key**, and a verification step you can check from the terminal.

| Framework | Packages | Guide |
| :--- | :--- | :--- |
| Vercel AI SDK 7 | `ai`, `@ai-sdk/otel` | [taleseal.com/integrate/vercel-ai](https://taleseal.com/integrate/vercel-ai) |
| Mastra | `@mastra/core`, `@mastra/otel-exporter` | [taleseal.com/integrate/mastra](https://taleseal.com/integrate/mastra) |
| Pydantic AI | `pydantic-ai`, plain OTel SDK | [taleseal.com/integrate/pydantic-ai](https://taleseal.com/integrate/pydantic-ai) |
| OpenLLMetry | `traceloop-sdk` | [taleseal.com/integrate/openllmetry](https://taleseal.com/integrate/openllmetry) |

Any other framework that exports OTLP/HTTP GenAI spans works the same way: exporter →
`https://taleseal.com/v1/traces`, `x-api-key` header, then verify as below.

## Drafts by default — why your run does not get a public URL

Telemetry is ambient: once content capture is on (it is on by default in every framework
above), spans carry your full prompts, tool arguments and tool results, unredacted. Nobody
chose to publish any particular run — so OTLP-derived tales are **drafts**: visible only to
you, in your dashboard, with **no public URL**. Publishing a draft is the same deliberate
act it has always been, done from the dashboard. Retraction works too: the trace id is the
tale's `runId`, so `DELETE /v1/tales?runId=<trace id hex>` covers the panic path.

## Authentication

Use your `tk_…` API key ([dashboard → API keys](https://taleseal.com/dashboard); sign up at
[taleseal.com/signup](https://taleseal.com/signup)). Set it once as an environment variable —
it is the only value you need to change in any snippet:

```sh
export TALESEAL_API_KEY=tk_your_key_here
```

Two header forms are accepted on the wire:

```
x-api-key: tk_…
Authorization: Bearer tk_…
```

Prefer `x-api-key` in `OTEL_EXPORTER_OTLP_HEADERS`: that variable is comma-and-equals
delimited, and some SDKs mangle values containing spaces — `Bearer tk_…` has one,
`x-api-key=tk_…` does not.

The generic endpoint variable gets `/v1/traces` appended by the SDK; the traces-specific
one is used verbatim. Set one, never both:

```sh
OTEL_EXPORTER_OTLP_ENDPOINT=https://taleseal.com
# or
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://taleseal.com/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=$TALESEAL_API_KEY
```

## Verify an integration

After a test run, ask taleseal what it received:

```sh
curl -s https://taleseal.com/v1/otlp/status -H "Authorization: Bearer $TALESEAL_API_KEY"
```

The response lists your recent runs, newest first:

```json
{
  "runs": [
    {
      "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
      "firstSeen": "2026-07-13T14:02:01Z",
      "lastSeen": "2026-07-13T14:02:05Z",
      "spans": 6,
      "inputTokens": 40,
      "outputTokens": 20,
      "errored": false,
      "state": "collecting",
      "title": null,
      "draftUrl": null
    }
  ],
  "generatedAt": "2026-07-13T14:02:11Z"
}
```

**Success = a run with your spans counted and `errored: false`, within seconds of the
flush.** `state: "collecting"` means spans are being received and counted — the integration
works; you do not need to wait for anything else. After the run has been idle for about two
minutes (plus up to a minute of sweep interval), `state` becomes `"finalised"`, `title` is
set from the composed draft, and `draftUrl` points at the draft in your dashboard.
`"published"` means you have since published that draft — deliberately, from the dashboard.

## What lands in the draft

- **Title and outcome** — from the root span and the run's error latch.
- **Beats** — one per agent/workflow and tool span; tool spans carry a receipt with the
  call's arguments and result, verbatim, when the framework captured content.
- **Metrics** — duration, model, token sums counted from inference spans only (agent-span
  aggregates are deliberately ignored: they duplicate their children).
- **A token chart** — output tokens per model call, when the run made three or more calls
  with token data. Deterministic: every point is read straight off an inference span.
- **Proof layer** — every span, as the tale page's timeline.

This is a faithful skeleton, not a narrative — the deterministic composer never invents
prose or infers what a number means. To get a richer tale (narrative beat titles,
stat-tile and chart widgets, a summary written for a reader), the **agent composes it**
with the SDK during the run — see below. taleseal does not run a model over your run on
its own; the narration is yours to author, where the data and the intent already are.

A run finalises when no new span has arrived for the idle gap (default 120 s). Late spans
re-open an unpublished draft and it recomposes on the next sweep; published tales are
sealed and never change.

## Composing a tale yourself — POST /v1/tales

If you skip OTLP and publish a composed tale directly (the `@taleseal/sdk` `openTale()`
builder or a raw `POST /v1/tales`), you are the narrator. The schema will accept a lazy
tale; readers will not. Rules that make the page worth opening:

- **Prose narrates; payloads are evidence.** Never paste raw JSON into `prose` or
  `summary`. Put the payload in the beat's `evidence` array (`kind: "command"`, the lookup
  as `command`, the payload as `output`) — it renders as a collapsible receipt.
- **Titles are narrative, not tool ids.** `Checked their Stripe history`, not
  `stripe_history`.
- **`summary` renders as text with light formatting** — `*bold*`, `` `code` ``, bare URLs
  and `<url|label>` links, `•`/`-` bullets. Do not paste a message formatted for another
  destination and stop there; write the verdict for a reader on this page.
- **Numbers want widgets.** Counts, money and time series belong in `widgets`
  (`stat_tiles`, `table`, `line_chart`) on the beat, not in a sentence of digits.
- **Timestamps make it a story.** Set `trigger.at`, per-beat `at` and `outcome.at` from
  the run, not from publish time.
- **Anyone holding the URL can read it.** Strip test identifiers and customer PII before
  publishing; there is no edit, only retraction.

One beat, done well:

```ts
draft
  .beat({
    at: "2026-07-13T15:02:41Z",
    title: "Checked their Stripe history",
    prose: "First-ever subscription: one active plan at £104.97/month, one paid invoice.",
    evidence: [
      {
        kind: "command",
        command: "stripe_history(cus_UsVwC0…)",
        output: JSON.stringify(history, null, 2),
      },
    ],
    widgets: [
      {
        kind: "stat_tiles",
        tiles: [
          { label: "Plan", value: "£104.97/mo" },
          { label: "Paid invoices", value: "1" },
          { label: "Customer since", value: "today", tone: "good" },
        ],
      },
    ],
  })
```

## Machine-readable resources

- Append `.md` to any page under `/integrate` for raw markdown, e.g.
  [taleseal.com/integrate/mastra.md](https://taleseal.com/integrate/mastra.md).
- The same URLs answer `Accept: text/markdown` with raw markdown.
- [taleseal.com/llms.txt](https://taleseal.com/llms.txt) indexes everything on this page.

---

[Integration overview](https://taleseal.com/integrate.md) · [llms.txt](https://taleseal.com/llms.txt) ·
panic path: `DELETE /v1/tales?runId=<trace id hex>`
