Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.prudra.dev/llms.txt

Use this file to discover all available pages before exploring further.

Agent payments

x402 payment flow

The x402 protocol uses ERC-3009 off-chain signatures. The agent signs but never sends an on-chain transaction — the Prudra server handles settlement.
import { ethers } from 'ethers';
import {
  signX402Payment,
  decodePaymentRequirements,
  selectPaymentOption,
} from '@prudra/payments';

async function callWithX402(
  url:    string,
  body:   unknown,
  wallet: ethers.Wallet,
) {
  // Attempt 1: no payment
  const r1 = await fetch(url, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(body),
  });

  if (r1.status !== 402) return r1;  // not a payment-gated endpoint

  // Decode challenge
  const requirements = decodePaymentRequirements(r1.headers.get('payment-required')!);
  const option = selectPaymentOption(requirements, 'USDC');

  // Sign off-chain (no gas, no tx)
  const { xPaymentHeader } = await signX402Payment(wallet, option);

  // Attempt 2: with signature
  return fetch(url, {
    method:  'POST',
    headers: {
      'Content-Type':      'application/json',
      'PAYMENT-SIGNATURE': xPaymentHeader,
    },
    body: JSON.stringify(body),
  });
}

MPP payment flow

MPP requires an actual on-chain transaction. The agent sends USDC.e to the recipient address on Tempo, then submits the txHash as proof.
import { ethers } from 'ethers';

const ERC20_ABI = [
  'function transfer(address to, uint256 amount) returns (bool)',
];

async function callWithMPP(
  url:      string,
  body:     unknown,
  signer:   ethers.Wallet,
) {
  // Attempt 1: no payment
  const r1 = await fetch(url, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(body),
  });

  if (r1.status !== 402) return r1;

  const wwwAuth = r1.headers.get('www-authenticate')!;

  // Parse challenge fields
  const field = (name: string) =>
    wwwAuth.match(new RegExp(`${name}="([^"]+)"`))?.[1];

  const challengeId = field('id')!;
  const request     = Buffer.from(field('request')!, 'base64url').toString();
  const requestObj  = JSON.parse(request);
  const expires     = field('expires')!;
  const realm       = field('realm');
  const intent      = field('intent');
  const method      = field('method');

  if (new Date() > new Date(expires)) {
    throw new Error('Challenge expired');
  }

  // Send on-chain payment
  const token = new ethers.Contract(requestObj.currency, ERC20_ABI, signer);
  const tx = await token.transfer(requestObj.recipient, BigInt(requestObj.amount));
  const receipt = await tx.wait(1);

  // Build credential
  const credential = Buffer.from(JSON.stringify({
    txHash:    receipt.hash,
    protocol:  'mpp',
    challengeId, intent, method, realm,
    request:   field('request'),
    expires,
  })).toString('base64url');

  // Attempt 2: with payment proof
  return fetch(url, {
    method:  'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Payment ${credential}`,
    },
    body: JSON.stringify(body),
  });
}

Session reuse (MPP only)

When a session payment was used on the first request, the response includes X-PRUDRA-SESSION-ID. Reuse it to avoid paying again:
let sessionId: string | null = null;

async function callWithSession(url: string, body: unknown, signer: ethers.Wallet) {
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  };

  if (sessionId) {
    headers['X-PRUDRA-SESSION-ID'] = sessionId;
  }

  const r = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });

  if (r.status === 402 || !sessionId) {
    // Need to pay — use MPP flow
    const paid = await callWithMPP(url, body, signer);
    sessionId = paid.headers.get('x-prudra-session-id');
    return paid;
  }

  return r;
}