This page assumes you don’t trust Regent’s database. Everything below uses standard Solana tooling and public on-chain data only.
Verify an agent
You need:
- An
agent_id(from Regent’s API or the dashboard) - The
AgentRegistryprogram ID:5jBmqyeo1vUAjHbEFuY59NMGTQR8cEe9Jvz2uCwCjp3L
import { Connection, PublicKey } from "@solana/web3.js";
import { sha256 } from "js-sha256";
const conn = new Connection("https://api.devnet.solana.com");
const programId = new PublicKey("5jBmqyeo1vUAjHbEFuY59NMGTQR8cEe9Jvz2uCwCjp3L");
const agentId = "agent_b1c59d23...";
const agentIdHash = Buffer.from(sha256(agentId), "hex");
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("agent"), agentIdHash],
programId,
);
const info = await conn.getAccountInfo(pda);
if (!info) {
console.log("Agent not anchored on-chain — do not trust");
process.exit(1);
}
// Parse with the Anchor IDL (program.account.agent.fetch) or borsh-deserialize manually.
// Fields: agent_id, did_hash, responsible_party, registered_at, revoked, revoked_at
// responsible_party = sha256(owner account id) = seed of the owner's UserAccount:
// [pda] = findProgramAddressSync([Buffer.from("user"), responsible_party], programId)
// → kyc_verified, kyc_country, kyc_attestation_hash, did_hash (agents registered before 2026-10-04 hold the authority key here)If the account exists and revoked == false, the agent is verifiably present and not revoked. If the account is missing, the agent claim is forged.
Verify a mandate
The chain answers two questions about a mandate: does it exist, and has it been revoked. UUID-shaped ids are stored as their 16 raw bytes left-padded to 32.
const uuidToBytes32 = (u: string) =>
Buffer.concat([Buffer.alloc(16), Buffer.from(u.replace(/-/g, ""), "hex")]);
const programId = new PublicKey("8HAzw3UFGmabsHJkAsuGLfBZG8djYQ3J1FRNUVjkseMr");
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("mandate"), uuidToBytes32("cab07ae3-e0e3-48d7-9e41-dc10512c4329")],
programId,
);
const account = await program.account.mandate.fetch(pda);
console.log("Registered:", new Date(account.registeredAt * 1000));
console.log("Revoked:", account.revoked, account.revokedAt);Verify the terms a decision was made against
The ceilings are deliberately not on the chain. What proves them is the decision’s own evidence:
- The allow token and the completion receipt name
mandate_id,mandate_versionandmandate_hash— a salted SHA-256 commitment of the canonical terms in force at that moment. Check the receipt’s signature offline withnpx -p @regent-protocol/receipt-verify regent-verify <receipt> --jwks jwks.json(orpip install regent-receipt-verify); the keys come fromhttps://control-api.regentprotocol.org/v1/control/.well-known/jwks.json. - The audit event for the decision carries the same version and hash, and its batch is anchored by
AuditAnchor— the recipe below proves the event is in the batch. - The owner reveals that version to you with
GET /v1/organizations/{org}/mandates/{id}/versions/{version}— the snapshot of the terms plus the salt. Recompute the hash and match it to the receipt. The gate’s receipt verify page does the recomputation in the browser.
A version is immutable; widening a limit creates a new version with a new hash, so a receipt can never be re-pointed at looser terms.
Verify an audit event
This is the most involved recipe and the most valuable: it proves that a specific event was in a specific batch, that the batch is in the chain in a specific position, and that the whole thing was fixed in time — without trusting Regent.
Everything you need comes from one endpoint:
curl -s https://api.regentprotocol.org/v1/audit/events/<event_id>/proof{
"event_id": "ctrl-dec_8c810131572d",
"payload_hash": "0x15d5690b…",
"merkle_index": 0,
"merkle_proof": ["0xabc…", "…"],
"batch_id": "78422d51-…",
"batch_root": "11999505bde71ec4…",
"chain": { "seq": 170, "chain_tx": "5N3oA…", "chain_head": "0xef40d6…" },
"epoch": { "epoch_index": 1, "first_seq": 1, "last_seq": 169,
"epoch_leaf_index": 169, "epoch_proof": ["…"],
"epoch_root": "7c1f…", "epoch_tx": "2ScWP…", "ots_status": "complete" },
"programs": { "audit_anchor": "8N1PpbJZKmvJjG86XWpP82XrWzp8HY5FHZuzyQTgjJas" },
"cluster": "devnet"
}The one-command version
python3 ops/audit/verify_event.py <event_id>It uses only the Python standard library, talks only to the public API and a public Solana
RPC, and derives the program-derived addresses itself rather than taking them from the
response, so a wrong address cannot be slipped past it. Every line must read PASS.
The hashing rules
Three rules, and they are the easy things to get wrong:
- A Merkle parent is
sha256(left_string ‖ right_string)over the hex strings, rendered as bare lowercase hex. An odd node at any level is paired with itself. - Event payload hashes are stored with a
0xprefix, so a batch of exactly one event has a prefixed root while a batch of two or more has a bare one. Compare batch roots ignoring an optional0x. - An epoch leaf is a batch root as bare 64-hex with any
0xstripped, inseqorder (leaf i=first_seq + i). The epoch root is bare, and that is also its form on chain and as the OpenTimestamps digest.
Step by step
import { createHash } from "crypto";
const bare = (h: string) => (h.startsWith("0x") ? h.slice(2) : h).toLowerCase();
const sha = (s: string) => createHash("sha256").update(s).digest("hex");
function fold(leaf: string, proof: string[], index: number): string {
let cur = leaf, idx = index;
for (const sib of proof) { cur = idx % 2 === 0 ? sha(cur + sib) : sha(sib + cur); idx >>= 1; }
return cur;
}
// 1. the event is in its batch
const batchRoot = fold(b.payload_hash, b.merkle_proof, b.merkle_index);
console.assert(bare(batchRoot) === bare(b.batch_root));
// 2. the batch is in its epoch
const epochRoot = fold(bare(b.batch_root), b.epoch.epoch_proof, b.epoch.epoch_leaf_index);
console.assert(bare(epochRoot) === bare(b.epoch.epoch_root));// 3. the epoch account on Solana carries that root, over a range containing this batch
const programId = new PublicKey(b.programs.audit_anchor);
const idx = Buffer.alloc(8); idx.writeBigUInt64LE(BigInt(b.epoch.epoch_index));
const [epochPda] = PublicKey.findProgramAddressSync([Buffer.from("epoch"), idx], programId);
const epoch = await program.account.epoch.fetch(epochPda);
console.assert(Buffer.from(epoch.merkleRoot).toString("hex") === bare(b.epoch.epoch_root));
console.assert(epoch.firstSeq <= b.chain.seq && b.chain.seq <= epoch.lastSeq);
// 4. the chain has reached this batch, so it is inside committed, ordered history
const [chainPda] = PublicKey.findProgramAddressSync([Buffer.from("chain")], programId);
const chain = await program.account.auditChain.fetch(chainPda);
console.assert(chain.seq >= b.chain.seq);If 1–4 hold, the event was in that batch, the batch is at that position in a sequence the program will not let anyone renumber, and the range is committed to a single on-chain root. No trust in Regent is required at any step.
The Bitcoin timestamp
Each epoch root is also submitted to the OpenTimestamps calendars, so an independent clock dates the evidence without Solana and without us:
curl -s -o epoch-1.ots https://api.regentprotocol.org/v1/audit/epochs/1/ots
ots verify -d <epoch_root_hex> epoch-1.otsots_status on the epoch tells you where it is: pending until Bitcoin confirms (usually
within a day), then complete.
Receipts verify offline today
The decision evidence has a packaged verifier — no Regent call, no chain call:
npx -p @regent-protocol/receipt-verify regent-verify <receipt.jwt> --jwks jwks.json
# or: pip install regent-receipt-verify && regent-receipt-verify <receipt.jwt> --jwks jwks.jsonIt checks the RS256 signature against the published keys, the expiry and the claim set, and prints the mandate and policy references. The on-chain recipes on this page remain manual snippets.
What attacks this prevents
| Attack | Prevented? | How |
|---|---|---|
| Forging a non-existent agent | Yes | No on-chain account → verification fails |
| Re-activating a revoked agent in the DB | Yes | On-chain revoked is still true |
| Widening a mandate’s limits retroactively | Yes | Every allow token, receipt and audit event names the mandate version and a salted hash of its terms; versions are immutable and the audit events are Merkle-anchored. The on-chain mandate account holds no limits by design |
| Inserting a fake past event into the audit log | Yes | Merkle root would change → mismatch with on-chain |
| Deleting an inconvenient event | Yes | Same — leaf removal changes the root |
| Modifying an event’s payload | Yes | Hash changes → mismatch |
What this does NOT prevent
- A forged audit event that was never ingested into the protocol won’t have an on-chain proof — but it also won’t carry the
payload_hashandmerkle_prooffields, so it’s trivially identifiable as not-from-Regent - Real-time censorship (the protocol could choose not to ingest an event in the first place — but the responsible party sees all events from their agent via the dashboard, so this is detectable)