@lucid-agents/payments
x402 payment protocol for monetizing agent capabilities.
The payments extension adds x402 payment protocol support, enabling agents to accept payments in USDC on EVM chains (Base, Ethereum) or Solana.
Installation
bun add @lucid-agents/payments @x402/fetch @x402/evmBasic usage
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { payments, paymentsFromEnv } from '@lucid-agents/payments';
const agent = await createAgent({
name: 'my-agent',
version: '1.0.0',
})
.use(http())
.use(payments({ config: paymentsFromEnv() }))
.build();Configuration
Environment variables
# Required in all payment modes
FACILITATOR_URL=https://facilitator.x402.org
NETWORK=base
# Static destination mode (default)
PAYMENTS_RECEIVABLE_ADDRESS=0x... # EVM or Solana address
# Stripe destination mode (dynamic payTo)
PAYMENTS_DESTINATION=stripe
STRIPE_SECRET_KEY=sk_live_...Supported aliases:
PAYMENTS_FACILITATOR_URLcan be used instead ofFACILITATOR_URLPAYMENTS_NETWORKcan be used instead ofNETWORK
paymentsFromEnv()
Loads configuration from environment variables:
import { paymentsFromEnv } from '@lucid-agents/payments';
const config = paymentsFromEnv();
// Returns PaymentsConfig from env + optional overridesPaymentsConfig
type PaymentsConfig = {
facilitatorUrl: string;
facilitatorAuth?: string;
network: Network;
policyGroups?: PaymentPolicyGroup[];
storage?: PaymentStorageConfig;
} & (
| {
// Static destination mode
payTo: `0x${string}` | SolanaAddress;
stripe?: never;
}
| {
// Stripe destination mode
stripe: {
secretKey: string;
apiBaseUrl?: string;
apiVersion?: string;
};
payTo?: never;
}
);Stripe destination mode (dynamic payTo)
Use Stripe destination mode when you want payTo to be resolved per request.
How it works:
- Runtime first tries to read destination
tofrom the incoming payment header. - If no destination is present, it creates a Stripe PaymentIntent and uses Stripe’s Base deposit address.
- The Agent Card advertises this as dynamic payee resolution (
extensions.x402.payeeMode = "dynamic").
Important behavior:
- Stripe mode is enabled when
payments.stripeis configured orPAYMENTS_DESTINATION=stripe. STRIPE_SECRET_KEYis required in Stripe mode.- Missing Stripe secret fails fast in
paymentsFromEnv()with:Missing Stripe secret: set STRIPE_SECRET_KEY or override - Stripe destination mode currently supports only Base mainnet (
base/eip155:8453).
# Stripe destination mode setup
FACILITATOR_URL=https://facilitator.x402.org
NETWORK=base
PAYMENTS_DESTINATION=stripe
STRIPE_SECRET_KEY=sk_live_...import { paymentsFromEnv } from '@lucid-agents/payments';
const config = paymentsFromEnv({
// Optional explicit overrides:
network: 'base',
// Optional Stripe overrides:
// stripe: { secretKey: process.env.STRIPE_SECRET_KEY!, apiVersion: '2024-06-20' },
});API reference
payments(options)
Creates the payments extension.
function payments(options: {
config: PaymentsConfig;
}): Extension<PaymentsExtensionContext>;Pricing entrypoints
Add pricing to entrypoints:
addEntrypoint({
key: 'premium-analysis',
description: 'Advanced AI analysis',
input: z.object({ text: z.string() }),
output: z.object({ analysis: z.string() }),
price: {
invoke: '$0.01', // Price per invocation
},
async handler({ input }) {
return { output: { analysis: 'detailed analysis...' } };
},
});For streaming entrypoints:
addEntrypoint({
key: 'chat',
streaming: true,
price: {
stream: '$0.005', // Price per stream request
},
async stream(ctx, emit) {
// ...
},
});Price formats
Prices can be specified as:
// Flat price
price: '0.01'
// Per-mode price
price: { invoke: '0.01', stream: '0.005' }In Stripe destination mode, string values used for dynamic amount resolution are interpreted as USD decimals.
PaymentsRuntime
When payments are configured, agent.payments provides:
type PaymentsRuntime = {
config: PaymentsConfig;
resolvePrice: (entrypoint: EntrypointDef) => EntrypointPrice | undefined;
evaluateRequirement: (
entrypoint: EntrypointDef,
request: Request
) => PaymentRequirement | null;
};Network support
EVM networks
base- Base mainnetbase-sepolia- Base Sepolia testnetethereum- Ethereum mainnetsepolia- Ethereum Sepolia testnet
Solana networks
solana- Solana mainnetsolana-devnet- Solana devnet
The network is auto-detected from the address format:
0x...- EVM address- Base58 string - Solana address
x402 protocol
The x402 protocol enables HTTP-native payments:
- Client sends request without payment
- Server returns
402 Payment Requiredwith payment details - Client creates payment and retries with
X-Paymentheader - Server verifies payment and processes request
Payment flow
Client Server
│ │
│ POST /entrypoints/foo/invoke │
│─────────────────────────────▶│
│ │
│ 402 Payment Required │
│ X-Payment-Details: {...} │
│◀─────────────────────────────│
│ │
│ POST /entrypoints/foo/invoke │
│ X-Payment: {payment_proof} │
│─────────────────────────────▶│
│ │
│ 200 OK │
│ {output: ...} │
│◀─────────────────────────────│Client-side payments
To call paid agent endpoints, you need a wallet that can sign x402 payment transactions.
Using helper functions
The simplest approach uses the createX402Fetch and accountFromPrivateKey helpers:
import { createX402Fetch, accountFromPrivateKey } from '@lucid-agents/payments';
// Create account from private key
const account = accountFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`);
// Create payment-enabled fetch
const x402Fetch = createX402Fetch({ account });
// Automatically handles 402 responses and payments
const response = await x402Fetch(
'https://agent.example.com/entrypoints/chat/invoke',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: { message: 'Hello' } }),
}
);Using raw @x402 packages
For advanced use cases, you can use the @x402/* packages directly:
import { wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { ExactEvmScheme, toClientEvmSigner } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const signer = toClientEvmSigner(account);
// Create x402 client and register networks
const client = new x402Client()
.register('eip155:8453', new ExactEvmScheme(signer)) // Base mainnet
.register('eip155:84532', new ExactEvmScheme(signer)) // Base Sepolia
.register('eip155:1', new ExactEvmScheme(signer)) // Ethereum mainnet
.register('eip155:11155111', new ExactEvmScheme(signer)); // Ethereum Sepolia
const x402Fetch = wrapFetchWithPayment(fetch, client);
const response = await x402Fetch(
'https://agent.example.com/entrypoints/chat/invoke',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: { message: 'Hello' } }),
}
);Using with API SDK
Combine x402 payments with the type-safe SDK client:
import { createClient, createConfig } from '@lucid-agents/api-sdk/client';
import { createX402Fetch, accountFromPrivateKey } from '@lucid-agents/payments';
const x402Fetch = createX402Fetch({
account: accountFromPrivateKey(process.env.PRIVATE_KEY as `0x${string}`),
});
const client = createClient(
createConfig({
baseUrl: 'https://api-lucid-dev.daydreams.systems',
fetch: x402Fetch,
})
);
// Invoke paid endpoint with full type safety
const { data, error } = await client.POST(
'/agents/{agentId}/entrypoints/{key}/invoke',
{
params: { path: { agentId: 'agent-123', key: 'premium' } },
body: { input: { query: 'Analyze this data' } },
}
);See Calling Paid Endpoints for a complete example with error handling.
Sign-In With X (SIWX)
SIWX lets returning wallets skip payment for previously-paid resources and protects auth-only routes with wallet signatures.
Configuration
.use(payments({
config: {
...paymentsFromEnv(),
siwx: {
enabled: true,
defaultStatement: 'Sign in to reuse access.',
expirationSeconds: 3600,
storage: { type: 'in-memory' },
},
},
}))Auth-only route
Routes that require wallet authentication but no payment:
addEntrypoint({
key: 'profile',
siwx: { authOnly: true },
async handler({ auth }) {
// auth.address - wallet address
// auth.chainId - chain ID
// auth.grantedBy - 'auth-only'
return { output: { address: auth?.address } };
},
});Challenge format
SIWX challenges are communicated through response extensions:
- 402 Payment Required -- Response body includes
extensions.siwxand theX-SIWX-EXTENSIONheader is set. The client signs the challenge and retries. - 401 Unauthorized (auth-only) -- Response body includes
error.siwxand theX-SIWX-EXTENSIONheader is set.
The client-side wrapFetchWithSIWx helper handles both cases automatically. See the payments package README for full configuration and client-side setup.
Manifest integration
Payment information is included in the Agent Card:
{
"name": "my-agent",
"payments": [
{
"method": "x402",
"payee": "0x...",
"network": "ethereum"
}
],
"skills": [
{
"id": "chat",
"pricing": {
"stream": "$0.005"
}
}
]
}In Stripe destination mode, the card omits static payee and marks dynamic payee resolution:
{
"payments": [
{
"method": "x402",
"network": "base",
"endpoint": "https://facilitator.x402.org",
"extensions": {
"x402": {
"facilitatorUrl": "https://facilitator.x402.org",
"payeeMode": "dynamic"
}
}
}
]
}See Stripe Destination Mode Example for a full setup and invocation flow.
Exports
// Extension
export { payments } from '@lucid-agents/payments';
// Configuration
export { paymentsFromEnv } from '@lucid-agents/payments';
// Utilities
export {
resolvePrice,
evaluatePaymentRequirement,
} from '@lucid-agents/payments';
// x402 client utilities
export { createX402Fetch } from '@lucid-agents/payments';
export {
createPaymentTracker,
createRateLimiter,
} from '@lucid-agents/payments';
// Types
export type {
PaymentsConfig,
PaymentsRuntime,
EntrypointPrice,
PaymentRequirement,
} from '@lucid-agents/payments';