The Problem
AI agents today are powerful but fundamentally limited by one thing: they can't pay for themselves.
Every API call, every compute minute, every database query costs money. Currently, the only way to keep an agent running is to either:
- Pre-fund a wallet — and hope it doesn't run out at 3 AM
- Give the agent your credit card — a security nightmare
- Build your own billing system — months of work
PocketAgent solves this with a self-hosted implementation of the Nevermined x402 Delegation Extension, an open protocol that lets you delegate payment authority to an AI agent with fine-grained limits, automatic top-up, and zero direct card exposure.
What is the x402 Delegation Extension?
The x402 Delegation Extension is a specification that defines how a "delegator" (you) can grant spending authority to a "delegate" (your AI agent) through a delegation — a config object with spending limits, duration, and provider bindings.
The protocol has four phases:
- Phase 0: Card Enrollment — attach a payment method to a PSP (Stripe, Braintree)
- Phase 1: Delegation Creation — define who can spend, how much, and where
- Phase 2: Verification — validate the delegation before each spend
- Phase 3: Settlement — charge the card, credit the agent, deduct usage
What makes it powerful: the agent never sees the card number. The facilitator mediates all charges. The agent only holds a JWT with scoped claims.
Architecture Overview
PocketAgent has three components:
┌──────────────┐ create delegation ┌──────────────────┐
│ Web UI / │ ─────────────────────────> │ Mock Facilitator │
│ CLI / Demo │ │ (port 3020) │
│ │ <──── x402 access token ── │ · Stripe API │
│ │ │ · On-chain via │
│ │ ── PAYMENT-SIGNATURE ────> │ viem │
│ │ header + prompt │ · JWT sign/verify │
│ │ │ · Delegation CRUD │
│ │ <─── PAYMENT-RESPONSE ──── │ · Auto top-up │
│ │ + task result └──────────────────┘
└─────────────┘
- Facilitator (port 3020): The core — handles Stripe, on-chain operations, JWT signing, delegation lifecycle
- Agent Server (port 3010): Protected x402 endpoint + Web UI. Forwards settlement to facilitator
- Client: Web UI (MetaMask-connected), CLI (Commander-based), or automated demo script
On-chain, we use PocketAgentCredit.sol — a simple ERC20 burnable token on Base Sepolia. Credits minted from fiat top-up, burned per agent invocation.
Deep Dive: How It Works
Phase 0: Card Enrollment
The user enrolls a Stripe test card. In production, this goes through VGS Collect (PCI-compliant proxy). For the demo, we attach a Stripe test PaymentMethod directly:
// Client requests a SetupIntent
const setupRes = await fetch(`${facilitatorUrl}/payments/card/setup`, { method: 'POST' });
const { setupIntentId } = await setupRes.json();
// Facilitator creates SetupIntent + attaches test payment method
const enrollRes = await fetch(`${facilitatorUrl}/payments/card/enroll`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupIntentId }),
});
const { customerId, paymentMethodId } = await enrollRes.json();
On the facilitator side, this creates a Stripe Customer (with name and address.country for Indian export compliance) and attaches the payment method:
// mock-server.ts
const customer = await stripe.customers.create({
name: `PocketAgent User ${Date.now()}`,
address: { country: 'IN' },
payment_method: pm.id,
});
Phase 1: Delegation Creation
The user creates a delegation — think of it as a programmable spending card for your agent:
const delRes = await fetch(`${facilitatorUrl}/api/v1/delegation/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: 'stripe',
subscriberAddress: '0x...',
providerCustomerId: 'cus_...',
providerPaymentMethodId: 'pm_...',
spendingLimitCents: 10000, // $100 max
durationSecs: 604800, // 7 days
currency: 'usd',
merchantAccountId: 'acct_...', // Stripe Connect (Section 6.4)
}),
});
const { delegationId, sessionKeyHash } = await delRes.json();
DelegationConfig stores everything needed for enforcement:
interface DelegationConfig {
delegationId: string;
provider: string;
subscriberAddress: `0x${string}`;
providerCustomerId: string;
providerPaymentMethodId?: string;
spendingLimitCents: number;
spentCents: number;
currency: string;
durationSecs: number;
maxTransactions?: number;
transactionCount: number;
merchantAccountId?: string; // Stripe Connect (Section 6.4)
createdAt: number;
expiresAt: number;
status: 'Active' | 'Exhausted' | 'Expired' | 'Revoked';
sessionKeyHash: string;
}
Phase 2: Getting an Access Token
Before calling the agent, the user requests an x402 access token. The facilitator verifies the delegation exists, is active, and hasn't exceeded limits:
const permRes = await fetch(`${facilitatorUrl}/x402/permissions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resource: {
url: '/api/v1/agents/agent-1/tasks',
description: 'AI agent task execution',
mimeType: 'application/json',
},
accepted: {
scheme: 'nvm:card-delegation',
network: 'stripe',
extra: { version: '1' },
},
delegationConfig: { delegationId },
}),
});
const { accessToken } = await permRes.json();
The returned accessToken is an RS256-signed JWT with nvm-namespaced claims (per spec Section 3.3):
const token = jwt.sign(
{
sub: delegation.subscriberAddress,
iss: 'pocket-agent',
aud: delegation.provider,
exp: delegation.expiresAt,
iat: Math.floor(Date.now() / 1000),
nvm: {
delegationId: delegation.delegationId,
provider: delegation.provider,
spendingLimitCents: delegation.spendingLimitCents,
spentCents: delegation.spentCents,
},
},
rsaPrivateKey,
{ algorithm: 'RS256' },
);
Phase 2-3: Agent Invocation → Verification → Settlement
The magic happens in the x402 middleware (src/agent-server/middleware/x402.ts). Every request to the agent server goes through:
Verification (Section 5)
The middleware checks:
-
PAYMENT-SIGNATURE header present? —
INVALID_PAYLOADif missing - JWT valid? — RS256 signature match, decode claims
-
JWT expired? —
EXPIRED_TOKEN -
Delegation exists? —
DELEGATION_NOT_FOUND -
Delegation active? —
DELEGATION_INACTIVE(Revoked/Exhausted/Expired) -
Transaction count exceeded? —
TRANSACTION_LIMIT_REACHED -
Spending limit exceeded? —
BUDGET_EXCEEDED
// x402.ts middleware
const sigHeader = request.headers['payment-signature'];
if (!sigHeader) {
return reply.status(402).send({ error: { code: 'INVALID_PAYLOAD', message: 'Missing PAYMENT-SIGNATURE header' } });
}
// Verify JWT
const decoded = jwt.verify(sigHeader, publicKey);
// Verify delegation
const del = delegations.get(decoded.nvm.delegationId);
if (!del) throw new PaymentError('DELEGATION_NOT_FOUND');
if (del.status !== 'Active') throw new PaymentError('DELEGATION_INACTIVE');
if (del.expiresAt < now) throw new PaymentError('EXPIRED_TOKEN');
if (del.maxTransactions && del.transactionCount >= del.maxTransactions) {
throw new PaymentError('TRANSACTION_LIMIT_REACHED');
}
Settlement (Section 6.1)
If verification passes, the middleware forwards settlement to the facilitator. This is the dual-rail logic:
// In /settle handler on the facilitator:
async function settle(delegationId, creditsRequested) {
// 1. Check on-chain credit balance
let currentBalance = await getCreditBalance(subscriberAddress);
// 2. If insufficient, auto top-up: charge card → mint credits
if (currentBalance < creditsRequested) {
const planPriceCents = creditsRequested * 10; // 10 cents per credit
// Atomic: increment spend counter BEFORE charge (Section 6.2)
del.spentCents += planPriceCents;
// Stripe charge with Connect routing (Section 6.4)
const piParams: any = {
amount: planPriceCents,
currency: del.currency,
customer: del.providerCustomerId,
payment_method: del.providerPaymentMethodId,
off_session: true,
confirm: true,
description: 'PocketAgent AI agent task credit top-up',
};
if (del.merchantAccountId) {
piParams.transfer_data = { destination: del.merchantAccountId };
}
// Idempotent: use delegationId + transactionCount as key (Section 9.6)
const idemKey = `${del.delegationId}:${del.transactionCount}:${planPriceCents}`;
const pi = await stripe.paymentIntents.create(piParams, { idempotencyKey: idemKey });
// Mint on-chain credits
await mintCredits(subscriberAddress, planPriceCents / 10);
}
// 3. Burn credits for this invocation
const txHash = await burnCredits(subscriberAddress, creditsRequested);
del.transactionCount += 1;
// 4. Return settlement receipt
return {
success: true,
network: del.provider,
transaction: txHash,
creditsRedeemed: String(creditsRequested),
remainingBalance: String(currentBalance),
orderTx: pi?.id,
};
}
The settlement receipt is base64-encoded in the PAYMENT-RESPONSE response header, per spec Section 4.3:
reply.header(
'PAYMENT-RESPONSE',
Buffer.from(JSON.stringify(receipt)).toString('base64')
);
Error Handling (Section 7.1)
Every error returns a spec-compliant error code:
| Code | When |
|---|---|
INVALID_PAYLOAD |
Missing or malformed headers |
INVALID_TOKEN |
JWT signature or claims invalid |
EXPIRED_TOKEN |
JWT or delegation expired |
DELEGATION_NOT_FOUND |
Delegation id doesn't exist |
DELEGATION_INACTIVE |
Delegation revoked / exhausted |
BUDGET_EXCEEDED |
Spending limit hit |
TRANSACTION_LIMIT_REACHED |
Max invocations exhausted |
INSUFFICIENT_BALANCE |
Not enough on-chain credits |
CARD_DECLINED |
Stripe card declined |
PAYMENT_FAILED |
Stripe charge failed generically |
MINT_FAILED |
On-chain mint reverted |
BURN_FAILED |
On-chain burn reverted |
Each error is returned as:
{
error: {
code: 'MINT_FAILED',
message: 'Credit minting failed after successful card charge: ...'
}
}
Dual-Rail: Why Crypto + Fiat?
The dual-rail design solves a real problem:
Crypto credits (on-chain) — fast, cheap, programmable. Burn 5 credits, get 5 credits of agent work. No settlement delay.
Fiat top-up (Stripe) — replenishes crypto credits when they run low. The user's card is charged, new credits are minted on-chain.
The flow:
Balance: 10 credits
Request: 5 credits
→ Sufficient: burn 5, done
Balance: 2 credits
Request: 5 credits
→ Insufficient: charge card $5.00 → mint 50 credits (now 52) → burn 5, done
This means the agent only touches crypto (fast, cheap) while the facilitator handles the fiat bridge (slow, expensive). Best of both worlds.
Security Model
The key insight: the agent never touches the payment card.
- The user enrolls their card at the facilitator (Stripe)
- The user creates a delegation with limits ($100, 7 days, 100 transactions)
- The agent receives a JWT — not the card number
- Every agent invocation is verified and settled by the facilitator
- The facilitator enforces limits atomically (spend counter before charge, rollback on failure)
Even if the agent's JWT is compromised, the attacker can only spend within the delegation's limits. The card itself is safe.
Running It Yourself
git clone <this-repo>
npm install
cp .env.example .env
# Fill in: STRIPE_SECRET_KEY, RPC_URL, CREDIT_TOKEN_ADDRESS, FACILITATOR_PRIVATE_KEY
npm start
# Open http://localhost:3010
The Web UI walks through the full flow: connect MetaMask → enroll a test card → create delegation → invoke the agent.
Building Your Own Agent with PocketAgent
The project is designed as a template. Swap src/agent-server/mock-executor.ts with your AI logic:
// Before: mock executor
async function executeTask(prompt: string) {
return { result: `Echo: ${prompt}` };
}
// After: real AI
import OpenAI from 'openai';
const openai = new OpenAI();
async function executeTask(prompt: string) {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
});
return { result: completion.choices[0].message.content };
}
Agent ideas you can build:
| Agent Idea | How PocketAgent Helps |
|---|---|
| Research agent — fetches web data, summarizes, emails reports | Crypto credits cover per-query costs; Stripe auto-top-up keeps it running without manual refill |
| Code review bot — reviews PRs in your repo | Per-review credit cost + monthly card limit = predictable pricing for the repo owner |
| Social media scheduler — posts, monitors replies, generates content | Delegation limits prevent runaway spend; Stripe Connect routes creator revenue |
| Trading signal generator — analyzes markets, sends alerts | Dual-rail: crypto for fast micro-transactions, fiat for larger monthly settlements |
| Personal finance assistant — tracks spending, categorizes transactions | User delegates a card with their own spending limit; agent handles variable usage |
Nevermined x402 Spec Compliance
The implementation aligns with 14 of 14 key spec sections:
- ✅ Scheme
nvm:card-delegation(Section 3.1) - ✅
PaymentPayload/PaymentRequiredstructures (Section 3.2) - ✅ JWT with
nvmclaims (Section 3.3) - ✅ All 4 protocol phases (Section 4.2)
- ✅
PAYMENT-*HTTP headers (Section 4.3) - ✅ 7 verification checks with spec error codes (Section 5)
- ✅ Atomic settlement with rollback (Sections 6.1, 6.2)
- ✅ Delegation lifecycle (Section 6.3)
- ✅ Stripe Connect routing
transfer_data.destination(Section 6.4) - ✅ Settlement receipt format (Section 6.5)
- ✅ All 12 critical error codes (Section 7.1)
- ✅ Idempotency keys on charge (Section 9.6)
What's Next
This is a reference implementation — production-ready improvements would add:
- VGS Collect for PCI-compliant card data collection (Phase 0)
- Database persistence for delegations instead of in-memory Map
- Session key crypto using @noble/ed25519 for real Ed25519 keys
- Braintree + Visa TAP provider implementations
- Stripe Connect OAuth onboarding flow for merchants
- Rate limiting and request queueing for settlement
PocketAgent shows that AI agents don't need direct access to payment instruments. With the x402 Delegation Extension protocol, you can give your agents spending power that's:
- Scoped — fixed limits, time-bound, per-transaction
- Safe — the agent never touches the card
- Automatic — self-top-up when credits run low
- Transparent — every charge is a Stripe PaymentIntent, every credit burn is an on-chain transaction
The code is open-source. Fork it, swap in your agent logic, and you have a production-ready billing system for your AI agent in an afternoon.
Code & more: https://www.dailybuild.xyz/project/173-pocket-agent
Top comments (0)