DEV Community

Cover image for PocketAgent: Building a Self-Hosted Payment Delegation System for AI Agents
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

PocketAgent: Building a Self-Hosted Payment Delegation System for AI Agents

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:

  1. Pre-fund a wallet — and hope it doesn't run out at 3 AM
  2. Give the agent your credit card — a security nightmare
  3. 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          └──────────────────┘
└─────────────┘
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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' },
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. PAYMENT-SIGNATURE header present?INVALID_PAYLOAD if missing
  2. JWT valid? — RS256 signature match, decode claims
  3. JWT expired?EXPIRED_TOKEN
  4. Delegation exists?DELEGATION_NOT_FOUND
  5. Delegation active?DELEGATION_INACTIVE (Revoked/Exhausted/Expired)
  6. Transaction count exceeded?TRANSACTION_LIMIT_REACHED
  7. 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');
}
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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: ...'
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. The user enrolls their card at the facilitator (Stripe)
  2. The user creates a delegation with limits ($100, 7 days, 100 transactions)
  3. The agent receives a JWT — not the card number
  4. Every agent invocation is verified and settled by the facilitator
  5. 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
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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 / PaymentRequired structures (Section 3.2)
  • ✅ JWT with nvm claims (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)