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.
ai@ai-sdk/otelGuide →MMastraTypeScriptPoint the exporter's custom provider at taleseal and every agent.generate() run lands.@mastra/core@mastra/otel-exporterGuide →PPydantic AIPythonA plain OTel SDK plus Agent.instrument_all() sends every run — nothing framework-specific.pydantic-aiopentelemetry-sdkGuide →OOpenLLMetryPythonTraceloop.init with a custom endpoint traces every LLM and tool call it can see.traceloop-sdkGuide →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; sign up at taleseal.com/signup). Set it once as an environment variable — it is the only value you need to change in any snippet:
export TALESEAL_API_KEY=tk_your_key_hereTwo 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:
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_KEYVerify an integration
After a test run, ask taleseal what it received:
curl -s https://taleseal.com/v1/otlp/status -H "Authorization: Bearer $TALESEAL_API_KEY"The response lists your recent runs, newest first:
{
"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
proseorsummary. Put the payload in the beat'sevidencearray (kind: "command", the lookup ascommand, the payload asoutput) — it renders as a collapsible receipt. - Titles are narrative, not tool ids.
Checked their Stripe history, notstripe_history. summaryrenders 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-beatatandoutcome.atfrom 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:
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
.mdto any page under/integratefor raw markdown, e.g. taleseal.com/integrate/mastra.md. - The same URLs answer
Accept: text/markdownwith raw markdown. - taleseal.com/llms.txt indexes everything on this page.
Integration overview · llms.txt · panic path: DELETE /v1/tales?runId=<trace id hex>