TEAL Ingest
POST /v1/teal/ingest accepts Lyrie ATP v2 (TEAL) hash-chained behavioral records and stores them in AgentLair’s trust substrate. Each batch is validated for chain integrity, optionally verified against the operator’s registered Ed25519 signing key, and dual-written to teal_records and behavioral_events. The behavioral events feed directly into the trust scoring algorithm.
What it does
Each TEAL record carries a sequence number, timestamp, action type, payload hash, and a reference to the previous record’s canonical hash. The endpoint verifies that the chain is unbroken—both within the submitted batch and across previous batches for the same session. If the operator has registered an Ed25519 signing key via POST /v1/agents/signing-keys, signatures on each record are verified before acceptance. Unsigned batches are accepted with ?unsigned_ok=1, but they are marked chain_signed: false and weighted accordingly in trust scoring.
Request schema
{
"session_id": "string (required, max 256 chars)",
"records": [
{
"seq": "integer (required, >= 0, strictly increasing)",
"timestamp": "string (required, ISO 8601)",
"action_type": "string (required, max 256 chars)",
"payload_hash": "string (required, e.g. sha256:<hex>)",
"prev_hash": "string | null (null for first record in a new session)",
"agent_sig": "string (base64url Ed25519 signature, required unless unsigned_ok=1)"
}
]
}
Maximum 100 records per request.
Authentication: Authorization: Bearer al_<key> (API key) or session token.
Query parameters:
| Parameter | Description |
|---|---|
unsigned_ok=1 | Skip signature verification. Records accepted but marked chain_signed: false. |
Response
{
"ok": true,
"operator_id": "acc_...",
"session_id": "sess_...",
"records_accepted": 3,
"records_idempotent": 0,
"chain_valid": true,
"chain_signed": true,
"session_id_continued": false,
"telemetry_id_first": "be_...",
"telemetry_id_last": "be_...",
"receipt_sig": "ed25519:<hex>"
}
receipt_sig is present when AUDIT_SIGNING_KEY is configured on the server.
Error codes
| HTTP | error | Cause |
|---|---|---|
| 400 | records_too_many | More than 100 records submitted |
| 400 | invalid_record_schema | A record failed schema validation (includes index) |
| 400 | seq_not_monotonic | Records are not strictly increasing by seq |
| 401 | unauthorized | Missing or invalid API key |
| 403 | chain_break | prev_hash mismatch (includes index) |
| 409 | duplicate_seq | All submitted records already exist for this session |
| 422 | no_signing_key_registered | Signature required but no key registered |
| 422 | sig_invalid | Ed25519 signature failed verification (includes index) |
| 503 | audit_unavailable | AUDIT D1 binding not configured |
Curl examples
Submit a 3-record chain (unsigned, development)
curl -X POST https://agentlair.dev/v1/teal/ingest?unsigned_ok=1 \
-H "Authorization: Bearer al_your_key" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_abc123",
"records": [
{
"seq": 0,
"timestamp": "2026-05-15T12:00:00Z",
"action_type": "tool.invoke",
"payload_hash": "sha256:a1b2c3d4...",
"prev_hash": null
},
{
"seq": 1,
"timestamp": "2026-05-15T12:00:01Z",
"action_type": "llm.inference",
"payload_hash": "sha256:e5f6a7b8...",
"prev_hash": "sha256:<canonical_hash_of_record_0>"
}
]
}'
Submit a signed batch
# Register your signing key first:
# POST /v1/agents/signing-keys with {"public_key": "<base64url>"}
curl -X POST https://agentlair.dev/v1/teal/ingest \
-H "Authorization: Bearer al_your_key" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_signed_001",
"records": [
{
"seq": 0,
"timestamp": "2026-05-15T12:00:00Z",
"action_type": "session.start",
"payload_hash": "sha256:deadbeef...",
"prev_hash": null,
"agent_sig": "<base64url_ed25519_signature>"
}
]
}'
Continue a session across multiple POSTs
# Second batch for the same session_id — prev_hash of first record
# must match canonical hash of last record from the previous POST.
curl -X POST https://agentlair.dev/v1/teal/ingest?unsigned_ok=1 \
-H "Authorization: Bearer al_your_key" \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_abc123",
"records": [
{
"seq": 2,
"timestamp": "2026-05-15T12:00:02Z",
"action_type": "tool.result",
"payload_hash": "sha256:c9d0e1f2...",
"prev_hash": "sha256:<canonical_hash_of_seq_1>"
}
]
}'
Wire to TEAL-emitting agents
If your agent uses @lyrie/atp, configure the emitter to POST each session’s accumulated records to https://agentlair.dev/v1/teal/ingest at session close or on a rolling basis. The canonical hash for each record is:
sha256: + hex(SHA-256(JSON.stringify({seq, timestamp, action_type, payload_hash, prev_hash})))
The signature message (Ed25519 over UTF-8 bytes) is:
seq|timestamp|action_type|payload_hash|(prev_hash ?? "null")
Register your agent’s Ed25519 public key via POST /v1/agents/signing-keys before submitting signed batches.
Per-record subject_agent_id (cross-org aggregation)
By default, TEAL records attribute behavioral activity to the submitting operator — the account that POSTs to /v1/teal/ingest. That works when an operator is also the agent being observed. It breaks down when operators act as intermediaries: SaaS A submits TEAL about agent X, SaaS B submits TEAL about the same agent X. Without a shared identity, the trust substrate sees two isolated datasets instead of one cross-org picture.
The subject_agent_id field solves this. Add it to any record to identify the agent being observed, independently from the submitting operator:
{
"session_id": "sess_abc123",
"records": [
{
"seq": 0,
"timestamp": "2026-05-15T12:00:00Z",
"action_type": "tool.invoke",
"payload_hash": "sha256:a1b2c3d4...",
"prev_hash": null,
"subject_agent_id": "acc_x"
}
]
}
subject_agent_id accepts acc_<1–128 alphanumeric/dash/underscore> or a2a_<1–128 alphanumeric/dash>. When absent, the server falls back to the submitting operator’s ID — so existing integrations behave exactly as before.
Per-record granularity means a single batch can carry records about multiple subjects. A multiplexer submitting activity from agents X, Y, and Z in one call routes each record to the right behavioral history independently.
The submitting operator’s identity is always retained in metadata_json.operator_id on the behavioral event, so the audit trail shows both who submitted and who was observed.
Who submitted TEAL about a given agent
Once multiple operators have submitted TEAL about the same subject, you can see the cross-org picture with the free public endpoint:
curl https://agentlair.dev/v1/trust/acc_x/teal-sources
{
"agent_id": "acc_x",
"sources": [
{
"operator_id": "acc_a",
"record_count": 12,
"first_seen": "2026-05-10T14:00:00.000Z",
"last_seen": "2026-05-15T08:30:00.000Z",
"session_count": 2
},
{
"operator_id": "acc_b",
"record_count": 7,
"first_seen": "2026-05-12T09:15:00.000Z",
"last_seen": "2026-05-14T20:00:00.000Z",
"session_count": 1
}
],
"total_records": 19,
"total_operators": 2,
"window_days": 90
}
GET /v1/trust/:agentId/teal-sources requires no authentication and no payment. It covers a rolling 90-day window and sorts by record count descending. The response exposes which operators submitted (operator IDs and record counts) but not what they reported — payload content and action types stay private.
Limitations
- Maximum 100 records per request. For longer sessions, POST in batches.
- Partial writes are possible on D1 batch failure — D1 lacks true transactions. Resubmitting accepted records is idempotent; the duplicate count is returned in
records_idempotent. chain_signed: falserecords contribute less weight to the trust score than verified records.- Cross-session trust aggregation operates on behavioral summaries, not raw TEAL payloads. Payload content is never stored — only
action_typeandpayload_hashare persisted.