The Audit Trail is the system of record for everything an agent does. It is append-only, cryptographically hashed, and periodically anchored to Solana — meaning records cannot be altered, deleted, or forged after the fact without detection.
Event model
Every interaction produces an audit event:
{
"event_id": "trade-2234146-d41d57ed",
"agent_id": "agent_b1c59d23...",
"event_type": "trade.executed",
"payload": {
"exchange": "binance-testnet",
"symbol": "BTCUSDT",
"side": "BUY",
"price": "80947.93",
"amount_usd": "49.38",
"btc_qty": "0.00061",
"order_id": "12345",
"authorization_jti": "848373a9-a016-46..."
}
}Standard event types include:
agent.registered,agent.revokedmandate.created,mandate.activated,mandate.revokedpayment.authorized,payment.rejectedtrade.executed,trade.rejected(or any application-specific events emitted by the agent)kyc.completed
Applications are free to emit additional event types; the protocol stores them all as part of the agent’s tamper-evident timeline.
The pipeline
Each stage updates the event’s status field:
| Status | Meaning |
|---|---|
received | Event ingested, payload hashed, stored in PostgreSQL |
batched | Event included in a sealed Merkle batch, batch ID assigned |
anchored | The batch root is on Solana — appended to the audit chain, seq assigned |
Merkle-batching is what makes anchoring economical: hundreds of events share a single on-chain write while each keeps its own proof of inclusion.
The chain is what proves nothing is missing
A batch root on its own proves “this event is in that batch”. It says nothing about whether a whole batch was quietly dropped. So every batch root is appended to one audit chain account on Solana, which keeps a running head:
head = sha256(head_previous ‖ batch_root ‖ seq)seq increases by exactly one per batch, and the program refuses anything else. Removing,
reordering or altering a batch changes every head after it, and a gap in the sequence is
rejected on chain rather than noticed later. Completeness and order become checkable facts,
not promises.
Periodically — once a day, or every 1 000 batches — an epoch records a Merkle root over a contiguous range of batch roots in its own small account, so proving one event is two short Merkle paths rather than a replay of the whole chain. Each epoch root is also submitted to OpenTimestamps, which lands it in a Bitcoin block: a second, independent clock that does not depend on Regent or on Solana.
Proof of inclusion
Once batched, every event has a Merkle proof — a small list of sibling hashes that lets anyone verify “this event is in this batch” without downloading the whole batch.
The proof is returned by the API:
{
"event_id": "trade-2234146-d41d57ed",
"payload_hash": "0x44d926713d6d15...",
"batch_id": "78422d51-6248-...",
"merkle_index": 14,
"merkle_proof": ["0xabc...", "0xdef...", "..."],
"anchored_at": "2026-05-11T14:27:05Z"
}GET /v1/audit/events/{event_id}/proof returns the whole bundle — the batch proof, the
batch’s position in the chain, and the epoch proof with its on-chain account.
To verify without trusting us:
- Hash the event payload (canonical JSON) →
payload_hash - Walk the batch proof → the batch root
- Walk the epoch proof → the epoch root
- Read the
Epochaccount from Solana and compare the root, and check that the epoch’s sequence range contains this batch - Read the audit chain account and confirm its sequence has reached this batch
There is a script that does all five against the public API and a public Solana RPC, and derives the account addresses itself:
python3 ops/audit/verify_event.py <event_id>See Verifying on-chain for the step-by-step version and the exact hashing rules.
Why this design
| Goal | How the design supports it |
|---|---|
| Immutability | Once anchored, modifying an event would change the Merkle root, which is fixed on Solana |
| Compactness | One Solana TX per batch (hundreds of events), not per event |
| Verifiability | Anyone with the batch root + proof can audit a single event without trusting Regent |
| Performance | Ingestion is async; the agent’s /audit call returns in <50ms even when the chain is slow |
What gets logged
The protocol enforces audit-event ingestion at every protocol boundary. Specifically, every:
- Agent registration and revocation
- Mandate authorize call — approved or rejected
- Mandate creation, suspension, revocation
- KYC milestone (submission, approval, DID issuance)
The agent itself is also expected to emit audit events for its own actions — trade fills, contract calls, payments, etc. The SDK’s ingest_event() method takes a single async call.