Policy-Bound Agent Wallets: The Missing Layer Between ERC-8004, ENSIP-25, and x402
Version 2.0 — March 2026
Compiled from deep research across ERC-8004, ENSIP-25, BitGo SDK, IPFS, x402, Helia, and viem
Key Sources: ERC-8004 · ENSIP-25 · BitGo SDK · Pinata · x402 · Helia · viem
- Executive Summary & Architecture
- CRITICAL CORRECTIONS vs Original VCR Spec
- ERC-8004 Complete Reference
- ENSIP-25 & ENS Integration
- BitGo SDK — Wallet & Policy Management
- IPFS Storage
- x402 Protocol — Payment Layer
- VCR Policy Schema (Complete)
- Tech Stack & Dependencies
- Hackathon Build Playbook (48-Hour Sprint)
- Contract Addresses Quick Reference
- API Keys & Environment Setup
VCR = Verifiable Capability Routing — a protocol layer that constrains how an autonomous agent can spend funds. ERC-8004 gives agents on-chain identity1, x402 gives them HTTP-native payment rails2, but nothing constrains what an agent is allowed to do with its wallet. VCR fills this gap.
- Policy Schema — A JSON document pinned to IPFS describing spending constraints (max per-tx, daily limits, allowed recipients, allowed tokens, time windows).
- ENS Text Records — ENSIP-25 links the agent's identity to ERC-8004, and a custom
vcr.policytext record points to the IPFS-hosted policy CID. - Verifier Library — A TypeScript function
canAgentSpend()that any service can call to check whether a proposed payment is within the agent's policy.
ERC-8004 provides agent registration and identity1. x402 provides HTTP 402-based payment2. But between identity and payment, there is no standard for expressing spending policy. An agent owner cannot say "this agent may spend up to $50/day, only on USDC, only to whitelisted services." VCR introduces this constraint layer — verifiable by any third party, stored on neutral infrastructure (IPFS + ENS).
Agent Owner → defines VCR Policy JSON → pins to IPFS → gets CID
Agent Owner → sets ENS text record vcr.policy = ipfs://<CID>
Agent Owner → registers agent on ERC-8004 IdentityRegistry
Agent Owner → links ENS name via ENSIP-25 agent-registration text record
Service (paywall) → receives x402 payment request from agent
Service → reads agent's ENS → fetches vcr.policy from IPFS → runs canAgentSpend()
canAgentSpend() checks:
✓ amount ≤ maxTransaction
✓ recipient in allowedRecipients
✓ cumulative ≤ dailyLimit
✓ token in allowedTokens
✓ chain in allowedChains
✓ time within allowedHours
If ALL pass → allow x402 payment to proceed
- ERC-8004: https://eips.ethereum.org/EIPS/eip-8004
- x402 Protocol: https://x402.org
The following corrections were identified during deep research. Each has significant impact on implementation.
| # | Component | Original Doc Says | Correct Value | Impact |
|---|---|---|---|---|
| 1 | Policy Storage Helper | AgentClient with writeFile / readFile |
Generic Pinata-backed JSON upload helper that returns cid, ipfsUri, and gatewayUrl. Requires Pinata JWT and gateway config. |
Build will fail if using old storage helper names or provider-specific fields. |
| 2 | BitGo Test OTP | 000000 (6 zeroes) |
0000000 (7 zeroes) |
Auth fails silently with wrong OTP. |
| 3 | BitGo velocityLimit | Amount implies USD | Amount is in wei (base units). 1 ETH = 1e18 wei. | Policy limits off by 1018 factor. |
| 4 | BitGo wallet version | multisigType: 'tss' |
Use v3 with multisigType: 'onchain'. TSS/v6 requires contacting BitGo support. |
Wallet creation fails for hackathon accounts. |
| 5 | ERC-8004 Testnet Addrs | Mainnet addresses used for testnet | Sepolia Identity: 0x8004A818...BD9e Reputation: 0x8004B663...8713 |
Transactions revert on wrong network. |
| 6 | ENS Public Resolver | Single address for all networks | Mainnet: 0xF291...C15 Sepolia: 0xE996...b5 |
setText calls fail on wrong resolver. |
| 7 | ENS Universal Resolver | Not specified | 0xeEeE...EeEe (same proxy across all networks) |
Needed for cross-chain resolution. |
| 8 | agentId start | Starts from 1 | Starts from 0. Uses post-increment $._lastId++ |
Off-by-one in all agent lookups. |
| 9 | JSON determinism | JSON.stringify for CID |
JSON.stringify is NOT deterministic. Use json-stringify-deterministic or @helia/json. |
CID mismatch breaks policy verification. |
| 10 | x402 V2 headers | X-PAYMENT format | PAYMENT-SIGNATURE (client→server) and PAYMENT-RESPONSE (server→client). No X- prefix. |
402 handshake fails with old headers. |
| 11 | Hoodi testnet | Holesky referenced | Chain ID 560048, replaces Holesky (shut down). Faucet: hoodi-faucet.pk910.de | Cannot get test ETH on deprecated chain. |
| 12 | BitGo 48h policy lock | Not mentioned | All wallet policies lock 48 hours after creation and become immutable forever. | Must plan policies carefully before creating. |
Sources: Pinata Docs, BitGo Developer Docs, ERC-8004 EIP, ENS Deployments, x402.org
ERC-8004 defines an on-chain identity system for autonomous agents1. It provides three registries: IdentityRegistry (registration and metadata), ReputationRegistry (feedback), and ValidationRegistry (trust scores).
| Contract | Network | Address |
|---|---|---|
| IdentityRegistry | Mainnet | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| IdentityRegistry | Sepolia | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| ReputationRegistry | Mainnet | 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 |
| ReputationRegistry | Sepolia | 0x8004B663056A597Dffe9eCcC1965A193B7388713 |
Three registration variants exist:
register()— bare registration, no URI, returns agentIdregister(string agentURI)— with URI pointing to agent metadataregister(string agentURI, bytes metadata)— with URI and raw metadata bytes
agentId assignment: Uses post-increment from 0. First agent gets id=0, second gets id=1, etc.
{
"type": "autonomous-agent",
"name": "Research Assistant v1",
"description": "Agent that fetches and summarizes research papers",
"image": "ipfs://bafkrei.../avatar.png",
"registrations": [
{ "chain": "eip155:11155111", "registry": "0x8004A818...", "agentId": 42 }
],
"services": [
{ "type": "research", "endpoint": "https://agent.example.com/api" }
],
"x402Support": {
"enabled": true,
"supportedTokens": ["USDC"],
"supportedChains": ["base", "base-sepolia"]
},
"active": true,
"supportedTrust": ["erc8004-reputation", "vcr-policy"]
}Setting an agent's wallet requires an EIP-712 typed signature with a 5-minute deadline maximum:
- Domain:
name="ERC8004IdentityRegistry",version="1" - TypeHash:
AgentWalletSet(uint256 agentId,address newWallet,address owner,uint256 deadline) - Metadata:
"agentWallet"is a reserved metadata key
import { createWalletClient, http, parseAbi } from 'viem';
import { sepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: sepolia,
transport: http('https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'),
});
const txHash = await walletClient.writeContract({
address: '0x8004A818BFB912233c491871b3d84c89A494BD9e',
abi: parseAbi([
'function register(string memory agentURI) external returns (uint256)'
]),
functionName: 'register',
args: ['ipfs://bafkrei...'],
});giveFeedback(agentId, score, comment)— score uses WAD 18-decimal mathreadFeedback(agentId, index)— returns individual feedback entrygetSummary(agentId)— returns aggregate score and count- Self-feedback is blocked (caller cannot rate own agent)
- Request→response pattern for trust validation
- Response score: 0-100 scale
- ERC-8004: https://eips.ethereum.org/EIPS/eip-8004
ENSIP-25 defines a standard for linking ENS names to agent registrations1. It uses parameterized text record keys that encode both the registry address and agent ID.
The key follows this pattern:
agent-registration[<ERC-7930 encoded registry>][<agentId>]
For mainnet IdentityRegistry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 on chain 12:
Step 1: Chain ID 1 → CAIP-2 "eip155:1"
Step 2: ERC-7930 encoding components:
- 0x00 (ERC-7930 prefix)
- 0x01 (chain type = EVM)
- 0x00000101 (chain ID = 1, compact varint)
- 0x14 (address length = 20 bytes)
- 8004a169fb4a3325136eb29fa0ceb6d2e539a432 (address bytes)
Result: 0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432
Full text record key for agent #42:
agent-registration[0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432][42]
Both registry AND ENS must match for valid agent-ENS linkage:
- Registry confirms agent ownership (agentId → owner address)
- ENS confirms text record value is "1" (active linkage)
- If either check fails, the agent-ENS link is invalid
import { createWalletClient, http, parseAbi } from 'viem';
import { namehash, normalize } from 'viem/ens';
const RESOLVER = '0xF29100983E058B709F3D539b0c765937B804AC15'; // mainnet
const resolverAbi = parseAbi([
'function setText(bytes32 node, string calldata key, string calldata value) external',
]);
const node = namehash(normalize('yourname.eth'));
const hash = await walletClient.writeContract({
address: RESOLVER,
abi: resolverAbi,
functionName: 'setText',
args: [
node,
'agent-registration[0x0001000001011480...a432][42]',
'1',
],
});
// Set VCR policy record
const policyHash = await walletClient.writeContract({
address: RESOLVER,
abi: resolverAbi,
functionName: 'setText',
args: [node, 'vcr.policy', 'ipfs://bafkrei...your-policy-cid'],
});Set multiple ENS text records in one transaction using the resolver's multicall:
const multicallAbi = parseAbi([
'function multicall(bytes[] calldata data) external returns (bytes[] memory)',
]);
// Encode both setText calls, then wrap in multicall
const encoded1 = encodeFunctionData({ abi: resolverAbi, functionName: 'setText',
args: [node, 'agent-registration[...][42]', '1'] });
const encoded2 = encodeFunctionData({ abi: resolverAbi, functionName: 'setText',
args: [node, 'vcr.policy', 'ipfs://bafkrei...'] });
await walletClient.writeContract({
address: RESOLVER, abi: multicallAbi,
functionName: 'multicall', args: [[encoded1, encoded2]],
});| Contract | Network | Address |
|---|---|---|
| ENS Registry | All | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e |
| Universal Resolver | All | 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe |
| Public Resolver | Mainnet | 0xF29100983E058B709F3D539b0c765937B804AC15 |
| Public Resolver | Sepolia | 0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5 |
| Name Wrapper | Mainnet | 0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 |
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { normalize } from 'viem/ens';
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
// Read VCR policy
const policyUri = await publicClient.getEnsText({
name: normalize('agent.eth'),
key: 'vcr.policy',
});
// Returns: "ipfs://bafkrei..."
// Read agent registration
const registration = await publicClient.getEnsText({
name: normalize('agent.eth'),
key: 'agent-registration[0x0001...a432][42]',
});
// Returns: "1" if linked
- ENSIP-25: https://docs.ens.domains/ensip/25
- ERC-7930: https://eips.ethereum.org/EIPS/eip-7930
- ENS Deployments: https://docs.ens.domains/learn/deployments
BitGo provides institutional-grade wallet infrastructure1. For VCR, BitGo policies serve as the on-chain enforcement layer, while VCR policies serve as the off-chain intent layer verifiable by third parties.
@bitgo/sdk-api— Core SDK with wallet management, policy APIs@bitgo/sdk-coin-eth— Ethereum-specific coin module
- Test environment OTP:
0000000(7 zeroes — NOT 6!) - Environment:
env: 'test' - Base URL:
https://app.bitgo-test.com
- Chain ID: 560048
- Coin:
hteth - Faucet: https://hoodi-faucet.pk910.de
import { BitGoAPI } from '@bitgo/sdk-api';
import { Eth } from '@bitgo/sdk-coin-eth';
const bitgo = new BitGoAPI({ env: 'test' });
bitgo.register('eth', Eth.createInstance);
bitgo.register('hteth', Eth.createInstance);
await bitgo.authenticateWithAccessToken({ accessToken: process.env.BITGO_ACCESS_TOKEN });
const wallet = await bitgo.coin('hteth').wallets().generateWallet({
label: 'VCR Agent Wallet',
passphrase: process.env.BITGO_WALLET_PASSPHRASE,
enterprise: process.env.BITGO_ENTERPRISE_ID,
walletVersion: 3, // MUST be v3 for hackathon
multisigType: 'onchain', // NOT 'tss' — TSS requires support contact
});
// CRITICAL: userKeychain.prv is ONLY returned once — store it securely!
console.log('Wallet ID:', wallet.wallet.id());
console.log('User Key (SAVE THIS):', wallet.userKeychain.prv);
console.log('Pending init:', wallet.wallet.coinSpecific()?.pendingChainInitialization);IMPORTANT: Enterprise gas tank must be funded before creating wallets. Without gas, wallet initialization transactions will fail.
Old wallet-level policies:
advancedWhitelist— Address whitelist (allowed recipients)velocityLimit— Spending cap in wei per time windowallocationLimit— Per-transaction maximum
New enterprise-level policies:
- Touchpoints / conditions / actions model
- More granular but requires enterprise account setup
48-HOUR POLICY LOCK WARNING: All wallet policies lock 48 hours after creation and become immutable forever. You cannot modify or remove them after the lock period. Plan policies carefully before wallet creation.
const result = await wallet.sendMany({
recipients: [{
amount: '1000000000000000', // Amount in WEI as STRING
address: '0xRecipientAddress',
}],
walletPassphrase: process.env.BITGO_WALLET_PASSPHRASE,
});
// Returns txid (if approved) or pendingApproval (if policy-triggered)
console.log('TX ID:', result.txid);
console.log('Pending:', result.pendingApproval);For walletVersion 3, forwarder contracts are used for receiving tokens. Each wallet can create multiple forwarder addresses for different use cases.
- When a transaction triggers a policy, it goes to pending approval
- Approvers can approve/reject via API or dashboard
- Webhooks notify on pendingApproval events
- transfer — Triggered on confirmed transfers
- pendingApproval — Triggered when policy blocks a transaction
- Verify with HMAC signature in webhook headers
| Endpoint | Method | Description |
|---|---|---|
/api/v2/:coin/wallet |
POST | Create wallet |
/api/v2/:coin/wallet/:id |
GET | Get wallet details |
/api/v2/:coin/wallet/:id/sendmany |
POST | Send transaction |
/api/v2/:coin/wallet/:id/policy |
GET/PUT | Get/set wallet policy |
/api/v2/:coin/wallet/:id/webhooks |
POST | Register webhook |
/api/v2/pendingapprovals/:id |
PUT | Approve/reject pending |
BitGo policy = on-chain enforcement (wallet actually blocks the transaction). VCR policy = off-chain intent layer (third-party services can verify before accepting payment). Both should mirror each other: VCR policy describes intent, BitGo enforces it.
- BitGo Developer Docs: https://developers.bitgo.com
The policy storage layer pins JSON documents to IPFS through Pinata and exposes deterministic CIDs for ENS and registry consumers.
- Class:
Agent(NOTAgentClient) - Constructor:
chain,viemAccount,pimlicoAPIKey,storageProvider - Storage provider:
PinataStorageProvider(jwt, gateway) - Setup:
setupStorage(namespace)— deploys Safe + Portal on Gnosis/Sepolia
create(content, metadata)— Create new filegetFile(fileId)— Read file by IDupdate(fileId, content, metadata)— Update existing filedelete(fileId)— Delete file
WARNING: There are NO writeFile/readFile/listFiles methods. These do not exist.
EOA → Safe Smart Account → Portal Contract → IPFS (Pinata)
- No encryption support
- Only Gnosis and Sepolia chains
- Only Pinata as storage backend
- Markdown content only
- No
listFilesmethod — must track file IDs externally
For simpler IPFS pinning (recommended for VCR policy files), use the Pinata SDK2 directly:
import { PinataSDK } from 'pinata';
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
pinataGateway: process.env.PINATA_GATEWAY,
});
// Pin JSON (e.g., VCR policy)
const result = await pinata.upload.public.json({
version: '1.0',
agentId: 'eip155:11155111:0x8004A818...BD9e:42',
constraints: { /* ... */ },
});
console.log('CID:', result.cid); // ipfs://bafkrei...
// Fetch pinned content
const data = await pinata.gateways.public.get(result.cid);
// REST alternative:
// POST https://api.pinata.cloud/pinning/pinJSONToIPFS
// Header: Authorization: Bearer <JWT>For local IPFS node operations or CID computation3:
import { createHelia } from 'helia';
import { unixfs } from '@helia/unixfs';
import { json } from '@helia/json';
const helia = await createHelia();
const fs = unixfs(helia);
const j = json(helia);
// Add JSON (deterministic CID)
const cid = await j.add({ version: '1.0', constraints: { /* ... */ } });
// Read back
const data = await j.get(cid);
// UnixFS for raw bytes
const bytes = new TextEncoder().encode('hello');
const fileCid = await fs.addBytes(bytes);
for await (const chunk of fs.cat(fileCid)) {
console.log(new TextDecoder().decode(chunk));
}- sha256 hash only matches for small files with raw codec
JSON.stringify()is NOT deterministic — key order varies across runtimes- Use
@helia/json,json-stringify-deterministic, or@ipld/dag-json - For VCR: Pin policy JSON to IPFS → get CID → store as ENS text record
vcr.policy
- Pinata IPFS docs: https://docs.pinata.cloud
- Pinata Docs: https://docs.pinata.cloud
- Helia: https://github.com/ipfs/helia
x402, developed by Coinbase1, enables HTTP 402 Payment Required flows for crypto-native paywalls. It allows agents to pay for API access using on-chain USDC payments authorized via EIP-3009 signatures.
1. Client sends GET request to protected resource
2. Server returns HTTP 402 with PAYMENT-REQUIRED header
(contains: price, token, recipient, facilitator URL, network)
3. Client parses payment requirements
4. Client creates EIP-3009 transferWithAuthorization signature
5. Client retries request with PAYMENT-SIGNATURE header
6. Server receives request, extracts PAYMENT-SIGNATURE
7. Server calls Facilitator /verify endpoint
8. Facilitator validates signature, checks balance
9. Server calls Facilitator /settle endpoint
10. Facilitator executes on-chain USDC transfer
11. Server returns HTTP 200 with content + PAYMENT-RESPONSE header
PAYMENT-REQUIRED— Server → Client (in 402 response)PAYMENT-SIGNATURE— Client → Server (in retry request)PAYMENT-RESPONSE— Server → Client (in 200 response)
No X- prefix in V2. Old X-PAYMENT format is deprecated.
- exact — One-time EIP-3009 USDC transferWithAuthorization
- streaming — Ongoing payment streams (future)
For USDC transferWithAuthorization:
// EIP-3009 typed data
{
types: {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'TransferWithAuthorization',
domain: { name: 'USD Coin', version: '2', chainId, verifyingContract: USDC_ADDRESS },
message: { from, to, value, validAfter, validBefore, nonce },
}POST /verify— Validates payment signature and checks sender balancePOST /settle— Executes the on-chain transfer- Coinbase facilitator:
https://x402.org/facilitator
- Base, Base Sepolia
- Arbitrum One
- Polygon
- Primary token: USDC
canAgentSpend() runs BEFORE the agent signs the x402 payment. The flow:
// In x402 client middleware:
// 1. Receive 402 with payment requirements
// 2. Extract amount, recipient, token, chain from PAYMENT-REQUIRED
// 3. Call canAgentSpend(ensName, { amount, recipient, token, chain })
// 4. If allowed → sign EIP-3009 and attach PAYMENT-SIGNATURE
// 5. If denied → reject payment, log reason
import { paymentRequired } from '@x402/server';
// Express middleware
app.get('/premium-content', paymentRequired({
amount: '100000', // $0.10 USDC (6 decimals)
token: 'USDC',
network: 'base',
facilitator: 'https://x402.org/facilitator',
}), (req, res) => {
res.json({ data: 'premium content here' });
});import { withPayment } from '@x402/client';
const response = await withPayment(
fetch('https://api.example.com/premium-content'),
{ wallet, chain: 'base' }
);
const data = await response.json();
- x402 Protocol: https://x402.org
{
"version": "1.0",
"agentId": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e:42",
"constraints": {
"maxTransaction": {
"amount": "1000000",
"token": "USDC",
"chain": "base"
},
"dailyLimit": {
"amount": "5000000",
"token": "USDC",
"chain": "base"
},
"allowedRecipients": [
"0xServiceA...",
"0xServiceB..."
],
"allowedTokens": ["USDC", "USDT"],
"allowedChains": ["base", "ethereum"],
"timeRestrictions": {
"timezone": "UTC",
"allowedHours": [9, 17]
}
},
"metadata": {
"createdAt": "2026-03-13T00:00:00Z",
"createdBy": "0xOwnerAddress",
"description": "Policy for research assistant agent",
"expiresAt": "2026-06-13T00:00:00Z"
}
}Types and setup:
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { normalize } from 'viem/ens';
interface SpendRequest {
amount: string; token: string; recipient: string; chain: string;
}
interface SpendResult { allowed: boolean; reason?: string; }Core verification function:
async function canAgentSpend(ensName: string, req: SpendRequest): Promise<SpendResult> {
// 1. Fetch vcr.policy from ENS text record
const policyUri = await publicClient.getEnsText({
name: normalize(ensName), key: 'vcr.policy',
});
if (!policyUri) return { allowed: false, reason: 'No VCR policy found' };
// 2. Fetch policy JSON from IPFS
const policy = await fetchFromIPFS(policyUri);
// 3. Check max transaction amount
if (BigInt(req.amount) > BigInt(policy.constraints.maxTransaction.amount))
return { allowed: false, reason: 'Exceeds max transaction' };
// 4. Check allowed recipients
if (!policy.constraints.allowedRecipients.includes(req.recipient))
return { allowed: false, reason: 'Recipient not whitelisted' };
// 5. Check allowed tokens
if (!policy.constraints.allowedTokens.includes(req.token))
return { allowed: false, reason: 'Token not allowed' };
// 6. Check allowed chains
if (!policy.constraints.allowedChains.includes(req.chain))
return { allowed: false, reason: 'Chain not allowed' };
// 7. Check time restrictions
const hour = new Date().getUTCHours();
const [start, end] = policy.constraints.timeRestrictions.allowedHours;
if (hour < start || hour >= end)
return { allowed: false, reason: 'Outside allowed hours' };
// 8. Check daily cumulative (requires tracking state)
const dailySpent = await getDailySpent(ensName, req.token);
if (BigInt(dailySpent) + BigInt(req.amount)
> BigInt(policy.constraints.dailyLimit.amount))
return { allowed: false, reason: 'Daily limit exceeded' };
return { allowed: true };
}{
"dependencies": {
"viem": "^2.x",
"@bitgo/sdk-api": "^1.63.x",
"@bitgo/sdk-coin-eth": "^25.x",
"pinata": "latest",
"pinata": "latest",
"helia": "^6.x",
"@helia/unixfs": "^5.x",
"@helia/json": "latest",
"multiformats": "latest",
"json-stringify-deterministic": "latest",
"@x402/client": "latest",
"@x402/server": "latest"
},
"devDependencies": {
"typescript": "^5.x",
"tsx": "latest",
"vitest": "latest",
"@types/node": "^20.x"
}
}| Service | Key Name | Where to Get |
|---|---|---|
| BitGo | Access Token | app.bitgo-test.com → Settings → API Tokens |
| BitGo | Enterprise ID | Dashboard → Enterprise settings |
| Pinata | JWT + Gateway URL | app.pinata.cloud → API Keys |
| Pimlico | API Key | dashboard.pimlico.io → Keys |
| Alchemy/Infura | RPC API Key | Provider dashboard → Create app → Copy key |
| ENS | ENS Name | app.ens.domains → Register on Sepolia |
- Node.js: >=20 <23
- TypeScript: 5.x with strict mode
- Package manager: pnpm recommended
- Create BitGo test account, generate access token
- Create Pinata account, generate JWT
- Create Pimlico account, get API key
- Set up Alchemy/Infura RPC for Sepolia
- Get testnet ETH from Sepolia faucet
- Register ENS name on Sepolia testnet
- Initialize project:
pnpm init && pnpm add viem @bitgo/sdk-api ...
- Create BitGo wallet (v3, onchain multisig) — fund gas tank first!
- Set wallet policies (whitelist, velocity limit) — 48h lock timer starts!
- Pin VCR policy JSON to IPFS via Pinata
- Register agent on ERC-8004 IdentityRegistry (Sepolia)
- Set ENS text records: agent-registration + vcr.policy
- Define policy JSON schema (TypeScript types)
- Implement
canAgentSpend()verifier - Implement ENS text record read/write helpers
- Implement IPFS fetch + CID verification
- Write unit tests for all constraint checks
- Build x402 server middleware with VCR check
- Build x402 client wrapper that calls
canAgentSpend()before payment - End-to-end demo flow: agent → paywall → VCR check → payment → content
- Basic UI showing policy status and transaction log
- Integration tests with live testnet
- Demo script (recorded or live)
- Presentation slides
- Documentation (README, architecture diagram)
| Risk | Impact | Mitigation |
|---|---|---|
| BitGo 48h policy lock | Cannot modify policies after lock | Set policies immediately on Day 1; over-allocate limits |
| Gas tank not funded | Wallet init transactions fail | Fund gas tank before wallet creation |
| Pinata rate limits | Policy storage throttled | Use a retry or backoff strategy |
| ENS registration delays | Cannot set text records | Register ENS name in Hour 0; use Sepolia for speed |
| Testnet faucet dry | No test ETH for transactions | Use multiple faucets; request in advance |
One consolidated table with all addresses needed for the VCR Protocol implementation:
| Contract | Network | Address |
|---|---|---|
| ERC-8004 IdentityRegistry | Mainnet | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| ERC-8004 IdentityRegistry | Sepolia | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| ERC-8004 ReputationRegistry | Mainnet | 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 |
| ERC-8004 ReputationRegistry | Sepolia | 0x8004B663056A597Dffe9eCcC1965A193B7388713 |
| ENS Registry | All | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e |
| ENS Universal Resolver | All | 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe |
| ENS Public Resolver | Mainnet | 0xF29100983E058B709F3D539b0c765937B804AC15 |
| ENS Public Resolver | Sepolia | 0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5 |
| x402 Facilitator (Coinbase) | Base | https://x402.org/facilitator |
| BitGo Test API | Test | https://app.bitgo-test.com |
Sources: ERC-8004 EIP, ENS Deployments, x402.org, BitGo Docs
Copy this template and fill in your values. Never commit this file to version control.
# ──────────────────────────────────────────────────
# BitGo
# ──────────────────────────────────────────────────
BITGO_ACCESS_TOKEN=v2x...
BITGO_ENTERPRISE_ID=...
BITGO_WALLET_ID=...
BITGO_WALLET_PASSPHRASE=...
# ──────────────────────────────────────────────────
# Pinata (IPFS)
# ──────────────────────────────────────────────────
PINATA_JWT=...
PINATA_GATEWAY=your-gateway.mypinata.cloud
# ──────────────────────────────────────────────────
# Pimlico (Account Abstraction)
# ──────────────────────────────────────────────────
PIMLICO_API_KEY=...
# ──────────────────────────────────────────────────
# IPFS Storage
# ──────────────────────────────────────────────────
CHAIN=gnosis
PRIVATE_KEY=0x...
# ──────────────────────────────────────────────────
# RPC Endpoints
# ──────────────────────────────────────────────────
ALCHEMY_API_KEY=...
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/...
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/...
# ──────────────────────────────────────────────────
# ENS
# ──────────────────────────────────────────────────
ENS_NAME=youragent.ethRun this script to verify all environment variables are configured correctly:
import 'dotenv/config';
const required = [
'BITGO_ACCESS_TOKEN', 'BITGO_ENTERPRISE_ID',
'PINATA_JWT', 'PINATA_GATEWAY',
'PIMLICO_API_KEY', 'PRIVATE_KEY',
'SEPOLIA_RPC_URL', 'ENS_NAME',
];
const missing = required.filter(k => !process.env[k]);
if (missing.length) {
console.error('Missing env vars:', missing.join(', '));
process.exit(1);
}
console.log('All required environment variables are set.');VCR Protocol — Complete Build Reference v2.0, March 2026
Compiled by Perplexity Computer from primary sources.
Key Sources: ERC-8004 · ENSIP-25 · ERC-7930 · BitGo SDK · x402 Protocol · Helia · viem · ENS Deployments · Pinata