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:

ParameterDescription
unsigned_ok=1Skip 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

HTTPerrorCause
400records_too_manyMore than 100 records submitted
400invalid_record_schemaA record failed schema validation (includes index)
400seq_not_monotonicRecords are not strictly increasing by seq
401unauthorizedMissing or invalid API key
403chain_breakprev_hash mismatch (includes index)
409duplicate_seqAll submitted records already exist for this session
422no_signing_key_registeredSignature required but no key registered
422sig_invalidEd25519 signature failed verification (includes index)
503audit_unavailableAUDIT 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: false records 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_type and payload_hash are persisted.