The Agent Authorization Rail
The card that proves an agent may act.
One call, POST /v1/verify, tells any counterparty that a specific agent is authorized to take a specific action for a specific principal. Scoped grants, spend caps, instant revocation, step-up approvals, a tamper-evident hash-chained ledger. It authorizes. It never moves money.
Agents are transacting. Authority is assumed.
An AI agent arrives at your checkout holding someone's intent. You can check a card. You cannot check its authority. Was this action granted? Under what cap? Was it revoked a minute ago? Today authority is claimed, not proven: blanket API keys that express no scope, no limits, no expiry, and leave no evidence.
Vera AI is the neutral layer between principals, agents, and counterparties. The principal issues a scoped, Ed25519-signed grant. The agent presents it. You verify it against live state, at decision time.
The Rail takes no side of any transaction · it holds no funds
POST /v1/verifyThe entire demand-side surface.
Send the agent's grant token, the intended action, and the amount. The Rail evaluates against live state (revocation, remaining caps, velocity) and answers in one round trip. Every decision is HTTP 200: a refusal is a policy answer, not a transport error. Branch on the decision, never on the status code.
allowProceed. A hold reserves the amount against the grant's cap, so a second agent cannot spend it out from under you.denyRefuse. Exactly one machine-readable reason. Evaluation short-circuits at the first failure in a fixed order.step_up_requiredWait. The amount crossed the principal's threshold for unattended spending. A human approves, out of band.Nine checks. Fixed order. One reason.
Every verification runs the same nine checks in the same sequence. Evaluation short-circuits at the first failure and returns exactly one deny reason. The order is part of the protocol, not an implementation detail. A test fails if it changes.
Every decision leaves a receipt.
Allow, deny, revoke, confirm: each writes an entry to an append-only ledger, hash-chained per account. Fetch a proof and re-derive the hashes yourself, on your own machine. Alter one byte and the chain breaks. This is the record disputes and audits are built on.
Append-only is enforced below the application: the runtime database role has UPDATE, DELETE, and TRUNCATE revoked on the ledger, and the API task never receives the owner credentials. What the chain does not yet prove is us: signed checkpoints that would bind the head against the operator are on the roadmap, and SECURITY.md says so in plain terms.
GET /v1/ledger/{id}/proof · tamper-evident below the application role. Revocation wins at the next verify.
Take the card.
Drag to orbit · double-click to reset
Eight invariants, guarded by named tests in CI.
Auditable at a glance. Alongside them, a contract suite validates every documented response against the OpenAPI spec, so drift fails the build. One disclosed gap, invariant 7's current test cannot yet fail, is tracked openly in SECURITY.md, because a guarantee you cannot audit is not one.
No double-spend, ever
Holds reserve spend atomically. Two concurrent verifies cannot reserve the same dollar.
invariant-1-no-double-spend.test.tsThe evaluation order is law
Nine checks, one fixed sequence, one deny reason per failure.
invariant-2-deny-reasons.test.tsThe ledger is append-only
Hash-chained per account. Tamper-evident by construction, independently verifiable.
invariant-3-ledger-chain.test.tsIdempotency is mandatory
Every POST carries a key. A retry returns the original answer, never a second hold.
invariant-4-idempotency.test.tsSecrets never touch disk
Or logs, in plaintext. API keys are stored as SHA-256 hashes with a prefix and last four.
invariant-5-secrets.test.tsRevocation wins instantly
Effective at the next verify, by construction. Webhooks are a courtesy, not the mechanism.
invariant-6-revocation.test.tsHolds settle exactly once
Confirm finalizes spend. Cancel releases it. Silence lets it expire. Never twice.
invariant-7-hold-settlement.test.tsDenials are 200
A refusal is policy, carried in the body. Errors are deterministic.
invariant-8-errors.test.tsIntegration
Two runtime calls. An afternoon.
Register once
Receive an sk_cpt_ key, shown one time, then declare your category tags and, optionally, a minimum verification tier.
Verify at checkout
POST /v1/verify with the agent's token, the action, and the amount. Proceed on allow. Refuse on deny. Wait on step-up.
Confirm after fulfillment
POST /v1/verifications/{id}/confirm. Cancel releases the hold; silence lets it expire and release itself.
Issuing agents on the supply side? Each user maps to an account, each of their agents to an agent under it. Issue grants scoped to the task's budget and expiry on task start; revoke on task end or anomaly. Listen for verification.* and grant.threshold_reached.
POST /v1/verify HTTP/1.1
Authorization: Bearer sk_cpt_····
Idempotency-Key: chk_order_58821
Content-Type: application/json
{
"grant_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"action": "commerce:purchase",
"amount": 4599,
"currency": "USD",
"categories": ["office_supplies"],
"reference": "order_58821",
"hold_ttl_seconds": 1800
}
HTTP/1.1 200 OK
{
"decision": "allow",
"verification_id": "vrf_01JD8QZT6H",
"hold": {
"id": "hld_01JD8R02KA",
"expires_at": "2026-08-16T15:42:09Z"
},
"grant_remaining": {
"max_total": 95401,
"velocity_actions_left": 9
},
"principal": {
"display_name": "Ada L.",
"tier": "verified"
}
}
// amounts are integers in minor units: 4599 = $45.99
HTTP/1.1 200 OK // a denial is policy, not an error
{
"decision": "deny"
,
"verification_id": "vrf_01JD8R1M8V",
"deny_reasons": ["amount_exceeds_transaction_cap"
],
"grant_remaining": {
"max_total": 95401,
"velocity_actions_left": 9
}
}
// exactly one reason. evaluation short-circuits
// at the first failure in the fixed order
HTTP/1.1 200 OK
{
"decision": "step_up_required",
"verification_id": "vrf_01JD8R2P0X",
"approval": {
"id": "apr_01JD8R2Q4T",
"expires_at": "2026-08-16T15:27:09Z"
}
}
// a human approves out of band;
// one approved retry passes, once
POST /v1/grants HTTP/1.1 // principal key
Authorization: Bearer sk_acct_····
Idempotency-Key: grant_task_7731
{
"agent_id": "agt_01JD8KX2N4",
"aud": ["cpt_01JD8M9Q7C"],
"expires_in": 2592000,
"scopes": [{
"action": "commerce:purchase",
"constraints": {
"currency": "USD",
"max_per_transaction": 20000,
"max_total": 100000,
"categories": ["office_supplies"],
"velocity": { "max_actions": 10, "window": "24h" },
"step_up_over": 10000
}
}]
}
Three audiences. One console.
Own the authority
Register agents. Issue and revoke grants. Approve step-ups. Read the ledger. Freeze everything, instantly.
Trust the answer
Set minimum tier, categories, and hold TTL. Read your own verifications. Manage API keys.
Run the rail
Set verification tiers with recorded evidence. Unfreeze accounts. The billing meter. Memberships.
Sign-in is passwordless: an emailed link, single-use, valid for fifteen minutes. Approving a step-up, freezing an account, and changing a tier each require a freshly checked authenticator code. Being signed in is not enough.
Security posture
The worst outcome is not a stolen card number. It is a forged allow.
Boring cryptography
Ed25519-signed JWTs. No verifiable credentials, no DIDs. Public keys at /.well-known/jwks.json, rotated with overlap.
Fail closed
A 5xx is a deny. So is a timeout. So is an exhausted retry. An outage removes purchases, never protections.
Data minimization
Verify responses carry IDs, a display name, and a tier. Never PII, never payment credentials, never the principal's other grants.
Kill switches
Revoke-all per agent. Freeze per account, idempotent and exempt from rate limiting by design.
A posture you can read
A published security policy: response commitments, safe harbor for researchers, known limitations disclosed up front.
Agents will act. Make it provable.
Four commands, under ten minutes from a fresh clone. The demo issues a grant, allows a purchase, denies an over-cap one, walks a step-up, confirms, revokes, then fetches a ledger proof, verifies the chain independently, tampers with one byte, and watches verification fail.