@lucid-agents/payments
v2.5.0
Published
Payment logic for bidirectional agent-to-agent payments
Readme
@lucid-agents/payments
Bi-directional payment tracking with persistent storage and policy enforcement for AI agents.
Overview
The @lucid-agents/payments package provides:
- Bi-directional payment tracking - Track both outgoing payments (agent pays) and incoming payments (agent receives)
- Zero-value transaction tracking - Track free services and zero-cost transactions to enable policy enforcement
- Persistent storage - Multiple storage backends (SQLite, In-Memory, Postgres) for different deployment scenarios
- Payment policies - Enforce limits and controls on both outgoing and incoming payments
- x402 integration - Seamless integration with the x402 micropayment protocol
- Policy enforcement - Automatic policy checking before payments are made or accepted
Quick Start
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { payments, paymentsFromEnv } from '@lucid-agents/payments';
import { createAgentApp } from '@lucid-agents/hono';
import { z } from 'zod';
const agent = await createAgent({
name: 'my-agent',
version: '1.0.0',
description: 'My agent with payment tracking',
})
.use(http())
.use(payments({ config: paymentsFromEnv() }))
.build();
const { app, addEntrypoint } = await createAgentApp(agent);
addEntrypoint({
key: 'process',
description: 'Process data',
input: z.object({ data: z.string() }),
output: z.object({ result: z.string() }),
price: '0.01',
async handler({ input }) {
return {
output: { result: `Processed: ${input.data}` },
};
},
});Storage Options
The payments package supports three storage backends, each optimized for different deployment scenarios:
SQLite (Default)
Best for: Traditional servers, VMs, local development
- Zero configuration - Automatically creates
.data/payments.db - Persistent - Data survives agent restarts
- File-based - Uses
better-sqlite3for local SQLite database - Auto-creates directories - Creates
.data/directory if it doesn't exist
Configuration:
import { payments } from '@lucid-agents/payments';
const agent = await createAgent({ ... })
.use(payments({
config: paymentsFromEnv(),
// SQLite is the default - no configuration needed
// Optional: specify custom path
storage: {
type: 'sqlite',
sqlite: { dbPath: '.data/payments.db' } // optional
}
}))
.build();When to use:
- Traditional server deployments
- VMs with persistent disk
- Local development
- Single-instance deployments
In-Memory
Best for: Serverless without file access, testing
- No file system required - Pure in-memory storage using JavaScript
Map - Ephemeral - Data is lost when the process restarts or invocation ends
- Zero overhead - Fastest option with no I/O
- Testing friendly - Perfect for unit tests
Configuration:
import { payments } from '@lucid-agents/payments';
const agent = await createAgent({ ... })
.use(payments({
config: paymentsFromEnv(),
storage: {
type: 'in-memory'
}
}))
.build();When to use:
- Serverless functions without file system access (e.g., AWS Lambda with read-only filesystem)
- Unit tests and integration tests
- Temporary tracking that doesn't need persistence
- Development environments where data loss is acceptable
Postgres
Best for: Serverless with persistence needs, multi-agent deployments
- Remote database - Uses PostgreSQL via
pgclient - Shared state - Multiple agent instances can share the same database
- Persistent - Data survives all restarts and invocations
- Fully async - Non-blocking operations, no event loop blocking
Configuration:
import { payments } from '@lucid-agents/payments';
const agent = await createAgent({ ... })
.use(payments({
config: paymentsFromEnv(),
storage: {
type: 'postgres',
postgres: {
connectionString: process.env.DATABASE_URL // required
}
}
}))
.build();Environment variable:
DATABASE_URL=postgresql://user:password@host:5432/dbnameWhen to use:
- Serverless functions with database access (e.g., Vercel, Netlify with Postgres)
- Multi-instance deployments where agents need shared state
- Production environments requiring high availability
- When you need to query payment data from external tools
Schema: The Postgres storage automatically creates the required table and indexes on first use.
Zero-Value Transaction Tracking
The payment tracker records all transactions, including those with zero value (free services). This enables:
- Policy enforcement on free services - Block or rate-limit free endpoints using the same policy system
- Usage analytics - Track how often free services are used
- Consistent behavior - Apply the same controls regardless of whether a service is paid or free
This means even when price: "0" or when no payment is required, the transaction is still recorded in the payment storage and subject to policy checks like rate limits and recipient restrictions.
Payment Policies
Payment policies allow you to control both outgoing payments (when your agent pays others) and incoming payments (when others pay your agent).
Outgoing Payment Policies
Control how much your agent can spend:
{
name: 'Daily Spending Limit',
outgoingLimits: {
global: {
maxPaymentUsd: 10.0, // Max per payment
maxTotalUsd: 1000.0, // Max total spending
windowMs: 86400000 // 24 hour window
},
perTarget: {
'https://agent.example.com': {
maxTotalUsd: 500.0 // Per-target limit
}
},
perEndpoint: {
'https://agent.example.com/entrypoints/process/invoke': {
maxTotalUsd: 100.0 // Per-endpoint limit
}
}
},
allowedRecipients: [
'https://trusted.example.com',
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
],
blockedRecipients: [
'https://untrusted.example.com',
'0x1234567890123456789012345678901234567890'
],
rateLimits: {
maxPayments: 100,
windowMs: 3600000 // 1 hour
}
}Incoming Payment Policies
Control which payments your agent accepts:
{
name: 'Incoming Payment Controls',
incomingLimits: {
global: {
maxPaymentUsd: 100.0, // Max per incoming payment
maxTotalUsd: 5000.0, // Max total incoming
windowMs: 86400000 // 24 hour window
},
perSender: {
'0x1234567890123456789012345678901234567890': {
maxTotalUsd: 1000.0 // Per-sender limit
}
},
perEndpoint: {
'/entrypoints/process/invoke': {
maxTotalUsd: 500.0 // Per-endpoint limit
}
}
},
allowedSenders: [
'https://trusted.example.com',
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
],
blockedSenders: [
'https://untrusted.example.com',
'0x1234567890123456789012345678901234567890'
]
}Important: Due to x402 protocol limitations, wallet-based sender checks and incoming limits can only be evaluated after payment is received. Domain-based checks can block before payment.
Policy Enforcement Flow
Outgoing Payments:
- Policy check happens before payment
- If policy violation → Payment is blocked (403 Forbidden)
- If policy passes → Payment proceeds
Incoming Payments:
- Domain-based checks happen before payment (using
Origin/Refererheaders)- If blocked → Return 403 Forbidden (payment not received)
- x402 payment validation happens
- Wallet-based checks and incoming limits happen after payment
- If blocked → Return 403 Forbidden (payment already received - x402 limitation)
Loading Policies from Config
import { policiesFromConfig } from '@lucid-agents/payments';
import { join } from 'node:path';
const agent = await createAgent({ ... })
.use(payments({
config: paymentsFromEnv(),
policies: join(import.meta.dir, 'payment-policies.json')
}))
.build();See packages/examples/src/payments/payment-policies.json.example for a complete policy configuration example.
API Reference
PaymentTracker
Tracks both outgoing and incoming payments:
import { createPaymentTracker, createSQLitePaymentStorage } from '@lucid-agents/payments';
const storage = createSQLitePaymentStorage();
const tracker = createPaymentTracker(storage);
// Record outgoing payment
await tracker.recordOutgoing('group-name', 'global', 1_000_000n); // 1 USDC
// Record incoming payment
await tracker.recordIncoming('group-name', 'global', 1_000_000n); // 1 USDC
// Get totals
const outgoingTotal = await tracker.getOutgoingTotal('group-name', 'global');
const incomingTotal = await tracker.getIncomingTotal('group-name', 'global');
// Check limits
const result = await tracker.checkOutgoingLimit(
'group-name',
'global',
100.0, // maxTotalUsd
86400000, // windowMs (24 hours)
1_000_000n // requestedAmount
);
if (!result.allowed) {
console.log('Limit exceeded:', result.reason);
}Storage Implementations
import {
createSQLitePaymentStorage,
createInMemoryPaymentStorage,
createPostgresPaymentStorage,
} from '@lucid-agents/payments';
// SQLite (default)
const sqliteStorage = createSQLitePaymentStorage(); // Uses .data/payments.db
const sqliteStorageCustom = createSQLitePaymentStorage('.data/custom.db');
// In-Memory
const memoryStorage = createInMemoryPaymentStorage();
// Postgres
const postgresStorage = createPostgresPaymentStorage(
process.env.DATABASE_URL!
);Utility Functions
import {
extractSenderDomain,
extractPayerAddress,
parsePriceAmount,
} from '@lucid-agents/payments';
// Extract domain from request headers
const domain = extractSenderDomain(
req.headers.origin,
req.headers.referer
);
// Extract payer address from x402 response header
const payerAddress = extractPayerAddress(
res.headers.get('PAYMENT-RESPONSE') ?? res.headers.get('X-PAYMENT-RESPONSE')
);
// Parse price string to bigint (USDC has 6 decimals)
const amount = parsePriceAmount('1.5'); // Returns 1_500_000nServerless Considerations
AWS Lambda
Option 1: In-Memory (Ephemeral)
storage: { type: 'in-memory' }- Data lost between invocations
- Fastest option
- No file system or database required
Option 2: Postgres (Persistent)
storage: {
type: 'postgres',
postgres: { connectionString: process.env.DATABASE_URL }
}- Data persists across invocations
- Requires RDS or managed Postgres
- Shared state across Lambda instances
Vercel / Netlify
Postgres (Recommended)
storage: {
type: 'postgres',
postgres: { connectionString: process.env.DATABASE_URL }
}- Use Vercel Postgres or Netlify Postgres
- Data persists across deployments
- Shared state across serverless functions
Traditional Servers / VMs
SQLite (Recommended)
storage: { type: 'sqlite' } // or omit for default- Zero configuration
- Persistent local storage
- Best performance for single-instance deployments
Environment Variables
# Required for payments
PAYMENTS_RECEIVABLE_ADDRESS=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0
FACILITATOR_URL=https://facilitator.daydreams.systems
NETWORK=ethereum
# Optional for Postgres storage
DATABASE_URL=postgresql://user:password@host:5432/dbnameExamples
- Policy Agent -
packages/examples/src/payments/policy-agent/- Demonstrates outgoing payment policies - Receivables Policies -
packages/examples/src/payments/receivables-policies/- Demonstrates incoming payment policies - Blocked Domain -
packages/examples/src/payments/blocked-domain/- Domain-based blocking - Blocked Wallet -
packages/examples/src/payments/blocked-wallet/- Wallet-based blocking
Related Packages
@lucid-agents/analytics- Payment analytics and reporting@lucid-agents/wallet- Wallet management for making payments@lucid-agents/identity- ERC-8004 identity and trust
Type Definitions
All payment-related types are exported from @lucid-agents/types/payments:
import type {
PaymentsConfig,
PaymentPolicyGroup,
PaymentDirection,
PaymentRecord,
OutgoingLimit,
IncomingLimit,
} from '@lucid-agents/types/payments';