@lucid-agents/payments
Bidirectional x402 payments, SIWX, policies, and atomic accounting.
The payments extension receives x402 payments, creates payment-enabled outbound Fetch clients, verifies SIWX credentials, enforces incoming and outgoing policies, and records payment history. One runtime owns this complexity; HTTP adapters do not install their own paywalls.
Installation
bun add @lucid-agents/payments @lucid-agents/core @lucid-agents/httpThe package already declares the x402 protocol dependencies it uses. Install optional pg or stripe peers only when selecting those Node-specific integrations.
Receive x402 payments
import { z } from 'zod';
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { payments, paymentsFromEnv } from '@lucid-agents/payments';
const agent = await createAgent({
name: 'merchant',
version: '1.0.0',
})
.use(payments({ config: paymentsFromEnv() }))
.use(http())
.addEntrypoint({
key: 'quote',
input: z.object({ symbol: z.string() }),
output: z.object({ price: z.number() }),
price: '0.01',
async handler({ input }) {
return {
output: { price: input.symbol === 'ETH' ? 3_000 : 0 },
};
},
})
.build();Prices are USD decimal strings without a currency symbol. A flat string applies to invoke; use { invoke, stream } when operations have different prices. An entrypoint without a price is free.
If x402 and MPP are both installed, every priced entrypoint must choose one rail with paymentProtocol: 'x402' | 'mpp'.
Configuration
payments({
config: {
payTo: '0xabc0000000000000000000000000000000000000',
facilitatorUrl: 'https://facilitator.example',
facilitatorAuth: process.env.PAYMENTS_FACILITATOR_AUTH,
network: 'eip155:84532',
storage: { type: 'in-memory' },
},
});Supported aliases normalize to CAIP-2 identifiers:
| Alias | Canonical network |
|---|---|
base | eip155:8453 |
base-sepolia | eip155:84532 |
ethereum | eip155:1 |
sepolia | eip155:11155111 |
solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
solana-devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
paymentsFromEnv(overrides?, env?) reads:
PAYMENTS_RECEIVABLE_ADDRESS;FACILITATOR_URLorPAYMENTS_FACILITATOR_URL;NETWORKorPAYMENTS_NETWORK;FACILITATOR_AUTHorPAYMENTS_FACILITATOR_AUTH;PAYMENTS_DESTINATION=stripeandSTRIPE_SECRET_KEYfor Stripe mode.
Pass an explicit environment record in runtimes that do not expose process.env. Configuration is validated when a priced or SIWX entrypoint activates the payments runtime.
const config = paymentsFromEnv(
{ network: 'eip155:84532' },
platformEnvironment
);payments({ config: false }) explicitly disables the capability. An absent environment configuration produces no active payments runtime, allowing one build artifact to support free and paid deployments.
Authorization lifecycle
The HTTP extension owns one authorization transaction for invoke, stream, and task creation:
- Payments verifies x402 or SIWX and returns a stable subject without reserving or settling.
- An invoke wins its idempotency claim.
admit()evaluates incoming policies and atomically reserves capacity.- The entrypoint executes, or asynchronous stream/task work is accepted.
finalize()first moves accounting into a durable, non-expiring staged batch, then settles and commits it;abort()releases a pre-settlement reservation.
Invalid input, failed handlers/output validation, failed admission, and failed settlement release provisional or staged capacity. If final accounting fails after an irreversible settlement, the staged batch remains counted until reconciliation instead of expiring open. A stream or task finalizes when its asynchronous work is successfully admitted because its HTTP operation is already live or accepted.
SIWX entitlements are checked before either x402 or MPP challenges, allowing both rails to reuse the same paid entitlement. If settlement becomes irreversible and later response persistence fails, HTTP retains the idempotency claim so a retry cannot execute or charge twice.
Storage boundaries
The root package is portable and defaults to isolated in-memory payment and SIWX storage. Durable backends are explicit subpaths. Merely selecting sqlite or postgres in configuration without passing its factory fails closed.
In memory
payments({ config: { ...config } });
// Equivalent: storage: { type: 'in-memory' }Use this for tests, edge-style runtimes, or intentionally ephemeral processes.
SQLite
import {
sqlitePaymentStorageFactory,
sqliteSIWxStorageFactory,
} from '@lucid-agents/payments/storage/sqlite';
payments({
config: {
...config,
storage: {
type: 'sqlite',
sqlite: { dbPath: '.data/payments.db' },
},
siwx: {
enabled: true,
storage: {
type: 'sqlite',
sqlite: { dbPath: '.data/siwx.db' },
},
},
},
storageFactory: sqlitePaymentStorageFactory,
siwxStorageFactory: sqliteSIWxStorageFactory,
});SQLite uses Bun's SQLite runtime and is intended for one local process.
Postgres
import {
postgresPaymentStorageFactory,
postgresSIWxStorageFactory,
} from '@lucid-agents/payments/storage/postgres';
payments({
agentId: 'merchant-production',
config: {
...config,
storage: {
type: 'postgres',
postgres: { connectionString: process.env.DATABASE_URL! },
},
},
storageFactory: postgresPaymentStorageFactory,
siwxStorageFactory: postgresSIWxStorageFactory,
});Use a stable agentId to isolate agents sharing one database. Postgres transactions and advisory locking enforce the same reservation contract across processes.
All backends provide atomic total/rate reservations and durable staged settlement batches. Before payment, every applicable limit, rate, and history record moves into one non-expiring batch; after payment, that batch commits to history atomically. A post-settlement storage error leaves the staged amount counted instead of failing open after the reservation TTL. agent.close() releases storage resources.
Payment policies
Policy groups are conjunctive: every configured group must allow the payment.
payments({
config: {
...config,
policyGroups: [
{
name: 'daily-budget',
outgoingLimits: {
global: {
maxPaymentUsd: 5,
maxTotalUsd: 50,
windowMs: 86_400_000,
},
},
incomingLimits: {
global: { maxPaymentUsd: 10, maxTotalUsd: 500 },
perSender: {
'0x1234567890123456789012345678901234567890': {
maxTotalUsd: 25,
},
},
},
allowedRecipients: ['trusted.example'],
blockedSenders: ['0xbad0000000000000000000000000000000000000'],
rateLimits: {
maxPayments: 100,
windowMs: 3_600_000,
},
},
],
},
});Scopes resolve from most specific to least specific: endpoint, target/sender, then global. Incoming sender rules use only a cryptographically verified payer address from x402 or MPP; caller-controlled headers are never sender identity. Outgoing recipient rules use the destination URL.
MPP payments enter the same incoming policy and accounting transaction as x402. A policy that requires sender or USD amount data fails closed when the verified rail does not provide usable values.
Rate enforcement has one source of truth: configured PaymentStorage. createRateLimiter() remains a standalone process-local utility for custom integrations; the runtime does not maintain a duplicate rate counter.
Node applications can load policy JSON through the Node entrypoint:
import { policiesFromConfig } from '@lucid-agents/payments/node';SIWX
SIWX protects a free auth-only route or lets a wallet reuse a paid entitlement:
const agent = await createAgent({
name: 'members',
version: '1.0.0',
})
.use(
payments({
config: {
...config,
siwx: {
enabled: true,
defaultStatement: 'Sign in to Members',
expirationSeconds: 300,
storage: { type: 'in-memory' },
},
},
})
)
.use(http())
.addEntrypoint({
key: 'profile',
siwx: { authOnly: true },
async handler({ auth }) {
return { output: { address: auth?.address } };
},
})
.addEntrypoint({
key: 'report',
price: '0.05',
siwx: { enabled: true },
async handler({ auth }) {
return { output: { address: auth?.address } };
},
})
.build();Nonces are consumed atomically. Replays, malformed signatures, expired payloads, and resource/domain mismatches are rejected. Never enable skipSignatureVerification outside tests.
Make paid outbound calls
When wallets() is installed, obtain a payment-aware Fetch function from the bound runtime:
const paidFetch = await agent.payments?.getFetchWithPayment(agent);
const response = await paidFetch?.(
'https://seller.example/entrypoints/data/invoke',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: { symbol: 'ETH' } }),
}
);createRuntimePaymentContext() and createX402Fetch() support lower-level construction. Never place a server private key in an edge or client bundle.
Stripe destination mode
Stripe mode resolves a Base crypto deposit address for each challenge. Install the optional stripe peer and provide stripe instead of payTo:
payments({
config: {
stripe: { secretKey: process.env.STRIPE_SECRET_KEY! },
facilitatorUrl: 'https://facilitator.example',
network: 'eip155:8453',
},
});Stripe utilities live at @lucid-agents/payments/providers/stripe and load dynamically only in destination mode.
PaymentsRuntime
The extension exposes one complete runtime directly:
type PaymentsRuntime = {
readonly config: PaymentsConfig;
readonly isActive: boolean;
requirements(entrypoint, kind): RuntimePaymentRequirement;
activate(entrypoint): void;
resolvePrice(entrypoint, kind): string | null;
authorize(
request,
entrypoint,
kind,
verifiedPayment?
): Promise<IncomingPaymentAuthorization>;
authorizeSIWx(
request,
entrypoint,
kind
): Promise<IncomingPaymentAuthorization | undefined>;
getFetchWithPayment(runtime, network?): Promise<FetchFunction | null>;
readonly paymentTracker?: PaymentTracker;
readonly policyGroups?: PaymentPolicyGroup[];
close(): Promise<void>;
};The runtime is passed through without adapter/core wrappers. Shared configuration and runtime contracts are defined in @lucid-agents/types/payments and @lucid-agents/types/siwx.
Exports and runtime portability
The portable root exports the extension, in-memory implementations, incoming authorization, validation, pricing, x402 clients, SIWX helpers, policies, and tracker utilities.
Node-only functionality is isolated:
| Subpath | Purpose |
|---|---|
@lucid-agents/payments/node | File/config policy loading. |
@lucid-agents/payments/storage/sqlite | Bun SQLite payment and SIWX factories. |
@lucid-agents/payments/storage/postgres | Postgres payment and SIWX factories. |
@lucid-agents/payments/providers/stripe | Stripe destination provider. |
Domain types should be imported from @lucid-agents/types/payments or @lucid-agents/types/siwx; package-owned implementation types such as PaymentStorage and factory types remain available from @lucid-agents/payments.