Solana ProgramsVerifying on-chain

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 AgentRegistry program 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:

  1. The allow token and the completion receipt name mandate_id, mandate_version and mandate_hash — a salted SHA-256 commitment of the canonical terms in force at that moment. Check the receipt’s signature offline with npx -p @regent-protocol/receipt-verify regent-verify <receipt> --jwks jwks.json (or pip install regent-receipt-verify); the keys come from https://control-api.regentprotocol.org/v1/control/.well-known/jwks.json.
  2. 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.
  3. 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:

  1. 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.
  2. Event payload hashes are stored with a 0x prefix, 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 optional 0x.
  3. An epoch leaf is a batch root as bare 64-hex with any 0x stripped, in seq order (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.ots

ots_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.json

It 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

AttackPrevented?How
Forging a non-existent agentYesNo on-chain account → verification fails
Re-activating a revoked agent in the DBYesOn-chain revoked is still true
Widening a mandate’s limits retroactivelyYesEvery 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 logYesMerkle root would change → mismatch with on-chain
Deleting an inconvenient eventYesSame — leaf removal changes the root
Modifying an event’s payloadYesHash 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_hash and merkle_proof fields, 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)