Introduction#
BoundID answers a scoped question for any app that lets an AI agent move money: does this request satisfy this owner's policy, using evidence that is acceptable right now? Answers are ALLOW, DENY or INDETERMINATE, with reason codes and a receipt signed for 60 seconds.
Who controls the agent, which issuers vouch for it, and what each claim does not prove.
The owner's EIP-712 signature over asset, recipients, caps and expiry.
Enforcement at execution: nonce, budget and revocation checked on the exact call.
The public sandbox runs the full engine with real signing keys, test assets and sample operators. Signatures are scoped to Robinhood Chain (chain ID 4663), and receipts reference the live block they were evaluated against.
Quickstart#
- 1Read a passport
curl https://useboundid.com/api/v1/passports/bnd:4663:1 - 2Evaluate a payment
Run it here. Change the amount or recipient to see
AMOUNT_EXCEEDS_TX_LIMITandRECIPIENT_NOT_ALLOWED.
curl https://useboundid.com/api/v1/evaluations \ -H "content-type: application/json" \ -d '{ "agentRef": "bnd:4663:1", "request": { "to": "0xCcb26681DcF04FDF9Ab1F828781d77D85ECdB7e3", "amount": "84000000" } }'
Run the request against the live API.
Try an unlisted recipient or more than 100 USDG.
- 3Register your own agent
Open the Console, sign in with any EVM wallet or a sandbox wallet, and run the pilot: register, attest, delegate and send guarded payments, each with a real signature.
Concepts#
Agent references
Agents are addressed as bnd:{chainId}:{agentId}, for example bnd:4663:1042. The API also accepts the bare numeric ID. A reference alone is never enough to trust a credential: every claim and delegation also names the binding version it was issued for.
Claims and profiles
There is no single VERIFIED badge. A passport shows separate, dated claims, and relying apps choose which to accept.
| Profile | Required evidence | Does not establish |
|---|---|---|
| Registered | Agent identifier and declared endpoint metadata | Real-world identity, authority or safety |
| Control checked | Fresh signatures from owner and execution keys, endpoint challenge | The legal identity of the controller |
| Operator attested | Accepted issuer credential linking an operator to the current binding | Permission for any particular transaction |
| Policy eligible | Current operator claim plus an unexpired delegation matching the request | Execution, unless the receiving system enforces the policy |
Credential lifecycle
Credentials move through pending, active, suspended, revoked and expired. Suspension blocks reliance while an issue is reviewed. Revocation is permanent; remediation means a new issuance. Expiry is evaluated from the signed validity window, even if nothing updates stored state.
Any owner or execution-key change increments the binding version. Claims and delegations signed for an older binding return BINDING_STALE; a new owner never inherits old permissions.
| Window | Default | Notes |
|---|---|---|
| Operator claim | 30 days | Freshness ceiling is set by the relying app |
| Delegation | 24 hours | Up to 7 days in the sandbox |
| Decision receipt | 60 seconds | Revocation overrides every window |
Delegations
A delegation is the owner's EIP-712 signature over exactly what the agent may do: one asset, an allowlist of function selectors (the pilot allows transfer(address,uint256) only), approved recipients, a per-transaction limit and a per-UTC-day budget in base units, and a validity window. Its EIP-712 digest is the policy hash. Versions increase monotonically and the newest one is in force.
Decisions and receipts
DENY wins over INDETERMINATE, and missing or unreachable evidence is never an implicit ALLOW. Every response carries a receipt bound to the request hash, policy version, binding version, issuer-set version and the Robinhood Chain block it was evaluated at, signed by the API key published at /v1/status.
import { verifyTypedData } from "viem";
const status = await fetch("https://useboundid.com/api/v1/status").then((r) => r.json());
const valid = await verifyTypedData({
address: status.signing.receiptSigner,
domain: { name: "BoundID", version: "1", chainId: 4663 },
types: { EvaluationReceipt: [/* fields above */] },
primaryType: "EvaluationReceipt",
message: receiptMessage, // receipt fields, times in unix seconds
signature: receipt.signature,
});Authentication#
Sandbox requests work without a key at 60 per minute per IP. Create a key in the Console to raise evaluations to 1,200 per minute and tie usage to your account. Keys look like bnd_test_…; only a hash is stored.
curl https://useboundid.com/api/v1/passports/bnd:4663:1 \
-H "authorization: Bearer bnd_test_…"Mutations are authorized by signatures, not keys: registration by the owner and agent keys, delegations and revocations by the owner, executions by the agent key.
API reference#
/v1/statusService and chain status
Current Robinhood Chain block, the receipt signer address and key ID, the BoundID Verify issuer key and the issuer-set version. Pin the signer address to verify receipts offline.
{
"object": "status",
"api": "operational",
"chain": { "chainId": 4663, "blockNumber": 66712345, "blockTimeMs": 100 },
"signing": {
"domain": { "name": "BoundID", "version": "1", "chainId": 4663 },
"receiptSigner": "0x…",
"receiptKeyId": "bnd-api-…",
"issuer": { "id": "bnd-verify", "address": "0x…", "keyEpoch": 3 },
"issuerSetVersion": 7
}
}/v1/agentsStart registration
Reserves an agent reference and returns an EIP-712 BindAgent challenge. The owner and the agent's execution key must be different addresses. The challenge expires in 10 minutes.
{
"name": "Invoice Agent",
"description": "Pays approved supplier invoices",
"endpoint": "https://agents.example.com/invoice",
"owner": "0xb9159EFfD7aD1c885424aa5bAc9986Dc8cfD28d7",
"executionKey": "0xC27386A9b9683AE0A2400E4212882AEEdeEA584C"
}{
"object": "registration",
"agentRef": "bnd:4663:1042",
"status": "pending",
"bindingVersion": 1,
"challenge": "0x6c1f…9a02",
"expiresAt": 1789790400,
"sign": { "standard": "EIP-712", "signers": ["owner", "executionKey"], "typedData": { "primaryType": "BindAgent" } },
"next": "POST /api/v1/agents/bnd:4663:1042/bind"
}/v1/agents/{agentRef}/bindComplete registration
Submit both signatures over the challenge. BoundID Verify checks they recover to the owner and the execution key, activates the agent and issues a signed control credential.
{
"ownerSignature": "0x…",
"agentSignature": "0x…"
}{
"object": "binding",
"agentRef": "bnd:4663:1042",
"status": "active",
"bindingVersion": 1,
"credential": { "id": "crd_…", "claim": "control", "status": "active", "issuer": "bnd-verify", "signature": "0x…" }
}/v1/passports/{agentRef}Read a passport
Public claims and current status. Each claim names its issuer, method, validity window and what it does not establish. Identity evidence never appears here.
{
"object": "passport",
"agent": { "ref": "bnd:4663:1", "name": "Acme Invoice Agent", "bindingVersion": 3 },
"claims": [
{ "profile": "registered", "state": "verified" },
{ "profile": "control", "state": "verified", "issuer": "BoundID Verify" },
{ "profile": "operator", "state": "verified", "issuer": "BoundID Verify", "expiresAt": 1791331200000 },
{ "profile": "policy", "state": "verified", "method": "Delegation v3 · policy 0x9b1e…" }
],
"delegation": { "version": 3, "state": "active", "policyHash": "0x9b1e…c44a" },
"issuerSetVersion": 7
}/v1/verificationsRequest an operator attestation
Starts a BoundID Verify review. Reviews complete asynchronously; poll GET /v1/verifications/{id}. The claim binds to the current binding version and lasts 30 days.
{
"agentRef": "bnd:4663:1042",
"claim": "operator",
"operator": "Acme Operations",
"jurisdiction": "US-DE"
}{
"object": "verification",
"id": "crd_…",
"status": "pending",
"issuer": "bnd-verify",
"estimatedCompletion": 1789790406000
}/v1/delegationsRecord a delegation
Submit an owner-signed EIP-712 Delegation. Versions increase by one; the newest version is the one in force. The sandbox caps windows at 7 days.
{
"agentRef": "bnd:4663:1042",
"version": 1,
"nonce": "0x…32 bytes",
"policy": {
"asset": { "symbol": "USDG", "address": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "decimals": 6 },
"selectors": ["0xa9059cbb"],
"recipients": [{ "address": "0xCcb26681DcF04FDF9Ab1F828781d77D85ECdB7e3", "label": "Northwind Supply" }],
"perTxLimit": "100000000",
"dailyLimit": "500000000",
"validFrom": 1789790400,
"validUntil": 1789876800
},
"signature": "0x…"
}{
"object": "delegation",
"id": "dlg_…",
"version": 1,
"policyHash": "0x9b1e…c44a",
"state": "active"
}/v1/evaluationsEvaluate a request
Evaluates one call against current evidence and the active delegation. Without a signature it is a preflight. With the agent's EIP-712 ExecutionRequest signature and execute: true, the guard consumes the nonce and updates the UTC-day spend counter atomically.
{
"agentRef": "bnd:4663:1042",
"request": {
"to": "0xCcb26681DcF04FDF9Ab1F828781d77D85ECdB7e3",
"amount": "84000000",
"nonce": "0x…32 bytes",
"deadline": 1789790520
},
"signature": "0x…",
"execute": true
}{
"object": "evaluation",
"mode": "signed",
"decision": "ALLOW",
"reasonCodes": ["POLICY_MATCH"],
"execution": { "status": "EXECUTED", "spentToday": "84000000" },
"receipt": {
"id": "rcpt_…",
"requestHash": "0x…",
"policyVersion": 1,
"agentBindingVersion": 1,
"issuerSetVersion": 7,
"evaluatedBlockNumber": 66712345,
"evaluatedBlockHash": "0x…",
"validUntil": 1789790460000,
"signerKeyId": "bnd-api-…",
"signature": "0x…"
},
"latencyMs": 38
}/v1/revocationsRevoke a delegation
Owner-signed and permanent. Every later evaluation of that delegation returns DENY with DELEGATION_REVOKED, including requests that hold an unexpired receipt.
{
"delegationId": "dlg_…",
"reason": "Rotating vendors",
"issuedAt": 1789790600,
"signature": "0x…"
}{
"object": "revocation",
"delegationId": "dlg_…",
"version": 1,
"revokedAt": 1789790600412
}/v1/receipts/verifyVerify a receipt
Checks a receipt's EIP-712 signature against the API signer. You can do the same offline with any EIP-712 library and the signer from /v1/status.
{ "receipt": { "id": "rcpt_…", "decision": "ALLOW", "…": "…" } }{
"object": "receipt_verification",
"validSignature": true,
"signer": "0x…",
"expired": false
}/v1/registry · /v1/issuers · /v1/activityRegistry, issuers and activity
Public listings: registry agents with claim states, the issuer set with key epochs, and the recent decision stream.
{ "object": "list", "data": [ … ] }EIP-712 types#
Every signature uses the BoundID domain on chain 4663. Times inside signed messages are unix seconds.
// Domain
{ name: "BoundID", version: "1", chainId: 4663 }
BindAgent(string agentRef, address owner, address executionKey,
uint64 bindingVersion, bytes32 challenge, uint64 expiresAt)
Delegation(string agentRef, address agentKey, uint64 bindingVersion,
address asset, bytes4[] selectors, address[] recipients,
uint256 perTxLimit, uint256 dailyLimit,
uint64 validFrom, uint64 validUntil, uint32 version, bytes32 nonce)
ExecutionRequest(string agentRef, address to, address asset, uint256 amount,
bytes4 selector, bytes32 nonce, uint64 deadline)
Revocation(string delegationId, bytes32 policyHash, string reason, uint64 issuedAt)
Credential(string credentialId, string agentRef, string kind, uint64 bindingVersion,
bytes32 claimHash, uint64 issuedAt, uint64 expiresAt, uint32 keyEpoch)
EvaluationReceipt(string receiptId, string decision, string[] reasonCodes,
bytes32 requestHash, uint32 policyVersion, uint64 agentBindingVersion,
uint32 issuerSetVersion, uint64 evaluatedBlockNumber,
bytes32 evaluatedBlockHash, uint64 evaluatedAt, uint64 validUntil,
string audience, bytes32 nonce)Reason codes#
| Code | Decision | Meaning |
|---|---|---|
POLICY_MATCH | ALLOW | Every check passed against the current policy and evidence. |
AGENT_NOT_FOUND | DENY | No agent is registered under this reference. |
AGENT_SUSPENDED | DENY | The agent is suspended in the registry. |
BINDING_STALE | DENY | Owner or execution key changed since this evidence was signed. Old permissions do not carry over. |
CONTROL_UNVERIFIED | DENY | Owner and execution keys have not both proven control. |
OPERATOR_UNATTESTED | DENY | No accepted issuer has attested the operator. |
CREDENTIAL_SUSPENDED | DENY | The issuer suspended this credential while it is reviewed. |
CREDENTIAL_REVOKED | DENY | The credential was revoked. Remediation requires a new issuance. |
CREDENTIAL_EXPIRED | DENY | The credential's signed validity window has ended. |
ISSUER_NOT_ACCEPTED | DENY | The issuer is not in this relying party's accepted set. |
ISSUER_SUSPENDED | DENY | The issuer's key is suspended in IssuerRegistry. |
NO_ACTIVE_DELEGATION | DENY | The owner has not signed a delegation for this agent. |
DELEGATION_REVOKED | DENY | The owner revoked the delegation. Revocation overrides every window. |
DELEGATION_EXPIRED | DENY | The delegation's validity window has ended. |
DELEGATION_NOT_YET_VALID | DENY | The delegation's validity window has not started. |
ACTION_NOT_PERMITTED | DENY | The function selector is not in the policy allowlist. |
ASSET_NOT_PERMITTED | DENY | The policy does not cover this asset. |
RECIPIENT_NOT_ALLOWED | DENY | The recipient is not on the owner's approved list. |
AMOUNT_EXCEEDS_TX_LIMIT | DENY | The amount is above the per-transaction limit. |
DAILY_BUDGET_EXCEEDED | DENY | The amount would exceed the remaining UTC-day budget. |
REQUEST_EXPIRED | DENY | The request deadline has passed. |
NONCE_REUSED | DENY | This nonce was already consumed. Replays are rejected. |
SIGNATURE_INVALID | DENY | The execution key's signature does not match the request. |
EVIDENCE_PENDING | INDETERMINATE | An issuer check is still in progress. |
EVIDENCE_STALE | INDETERMINATE | The claim is older than this relying party's freshness ceiling. |
STATUS_UNAVAILABLE | INDETERMINATE | Current chain or registry status could not be established. The request fails closed. |
Errors#
Errors return a JSON body with a stable code. Every response carries an x-request-id header.
{ "error": { "code": "invalid_signature", "message": "Delegation must be signed by the agent's owner (EIP-712).", "docs": "https://useboundid.com/docs#errors" } }| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Malformed JSON, missing fields or out-of-range values |
| 401 | invalid_signature | A signature does not recover to the required key |
| 401 | unauthorized | Invalid or revoked API key |
| 403 | read_only | Mutating a sample agent |
| 404 | not_found | Unknown agent, verification or delegation |
| 409 | conflict | Wrong delegation version, already bound, already revoked |
| 410 | expired | Registration challenge expired |
| 429 | rate_limited | Too many requests; honor retry-after |
Contracts#
Core contracts are immutable and versioned; administrative actions use separate keys, and an emergency pause can stop new issuance or execution but never blocks revocation. Addresses and bytecode hashes are published here after deployment and independent review.
| Component | Responsibility | Release |
|---|---|---|
| IdentityAdapter | Resolve a pinned ERC-8004 registry, current owner and wallet binding | MVP |
| IssuerRegistry | Issuer keys, permitted claim types, key epochs, suspension | MVP |
| CredentialStatusRegistry | Opaque credential IDs and public status, no identity evidence | MVP |
| DelegationRegistry | Owner-authorized policy hashes, revocations, binding versions | MVP |
| ExecutionGuard | Permission checks, atomic spend and replay protection | MVP |
| ProviderBondVault | Provider collateral reserved against open obligations | Later |
| DisputeManager | Adjudicated outcomes and bounded penalties | Later |
interface IExecutionGuard {
struct Call {
address to;
address asset;
uint256 amount;
bytes4 selector;
bytes32 nonce;
uint64 deadline;
}
/// Reverts with a reason code unless the delegation allows this exact call.
function execute(bytes32 agentRef, Call calldata call, bytes calldata agentSig) external;
/// Owner-signed and permanent.
function revoke(bytes32 delegationId, bytes calldata ownerSig) external;
function spentToday(bytes32 delegationId) external view returns (uint256);
}Trust model#
BoundID starts with named, approved issuers and says so. It is not permissionless verification yet. Credentials are scoped evidence for a relying party's decision: they do not guarantee an agent's conduct, prove a model is honest, or replace human and business identity checks where those are required. Revocation does not erase public-chain history, and on-chain policy constraints are public.
Read the whitepaper for the full specification, threat model and acceptance tests.