lucidAGENTS
Buy

Set policies and budgets

Gate outgoing and incoming payments by counterparty, endpoint, amount, rate, and atomic totals.

Payment policy is deterministic code between an untrusted challenge and signing authority. It limits damage; it does not decide whether an LLM's purchase idea is sensible.

Policy groups are cumulative. Every group must admit the payment, and the first violation stops it.

Outgoing buyer policy

import {
  createInMemoryPaymentStorage,
  createPaymentTracker,
  wrapBaseFetchWithPolicy,
} from '@lucid-agents/payments';
import type { PaymentPolicyGroup } from '@lucid-agents/types/payments';

const endpoint = 'https://service.example/entrypoints/search/invoke';

const policyGroups: PaymentPolicyGroup[] = [
  {
    name: 'approved-counterparties',
    allowedRecipients: ['0x1111111111111111111111111111111111111111'],
    blockedRecipients: ['0x0000000000000000000000000000000000000000'],
  },
  {
    name: 'research-budget',
    outgoingLimits: {
      global: {
        maxPaymentUsd: 0.05,
        maxTotalUsd: 2,
        windowMs: 24 * 60 * 60 * 1_000,
      },
      perEndpoint: {
        [endpoint]: {
          maxPaymentUsd: 0.02,
          maxTotalUsd: 1,
          windowMs: 24 * 60 * 60 * 1_000,
        },
      },
    },
    rateLimits: {
      maxPayments: 60,
      windowMs: 60 * 60 * 1_000,
    },
  },
];

const tracker = createPaymentTracker(createInMemoryPaymentStorage());
const policyFetch = wrapBaseFetchWithPolicy(fetch, policyGroups, tracker);

Wrap this base Fetch with the x402 payment client after policy:

application → x402 payment wrapper → policy wrapper → network Fetch

The initial unpaid request reaches the seller. On 402, the policy wrapper decodes the selected x402 v2 requirement, evaluates the target URL, requirement payee, and six-decimal amount, then atomically reserves totals/rate capacity. Only then does the outer x402 wrapper sign and retry.

Scope precedence

For outgoing limits, Lucid selects the most specific configured scope:

perEndpoint > perTarget > global

Only one outgoing limit scope per group applies to one payment. Separate policy groups are the way to enforce independent organization, project, and endpoint ceilings simultaneously.

Target matching normalizes URL casing/trailing slashes and can match a domain. For strict purchasing, prefer complete HTTPS endpoint URLs plus an explicit on-chain recipient allowlist. Domain matching alone does not protect against a compromised seller or changed receiving address.

Limit meanings

OptionEnforcement
maxPaymentUsdStateless ceiling for one selected six-decimal requirement
maxTotalUsdStateful sum within windowMs, or lifetime when no window is set
windowMsSliding query window used by the tracker; align it with business policy
rateLimits.maxPaymentsNumber of admitted payments for that group/window
allowedRecipientsAllowlisted payee address or target domain
blockedRecipientsDenylist evaluated before allowlist
perTargetTarget URL/domain total scope
perEndpointExact endpoint total scope

Policy amount accounting assumes six-decimal USD/USDC. Do not use these fields for a non-USD MPP currency unless the verifier and policy conversion are explicitly designed for it.

Reservation lifecycle

unpaid 402
  → validate requirement
  → evaluate every policy group
  → reserve totals and rate capacity
  → paid retry with matching request fingerprint
  → stage non-expiring outgoing accounting
  → receive successful response + PAYMENT-RESPONSE
  → commit staged accounting

The wrapper fingerprints method, URL, non-payment headers, and body. A paid retry without a matching active reservation returns 503. Outstanding attempts are bounded (10,000 by default) and expire after five minutes by default; configure maxOutstandingAttempts and attemptTtlMs for the expected challenge-to-signing delay.

If the paid call throws, returns a non-success response, or omits PAYMENT-RESPONSE, Lucid releases the local staged accounting. A remote system could still have an ambiguous external outcome, so reconcile before signing a new payment after a timeout.

Incoming seller policy

Configure incoming groups on the payments extension:

const config = {
  facilitatorUrl: process.env.PAYMENTS_FACILITATOR_URL!,
  network: 'eip155:84532' as const,
  payTo: process.env.PAYMENTS_RECEIVABLE_ADDRESS as `0x${string}`,
  policyGroups: [
    {
      name: 'seller-admission',
      allowedSenders: ['0x2222222222222222222222222222222222222222'],
      incomingLimits: {
        global: { maxPaymentUsd: 1, maxTotalUsd: 100 },
        perSender: {
          '0x2222222222222222222222222222222222222222': {
            maxTotalUsd: 10,
            windowMs: 24 * 60 * 60 * 1_000,
          },
        },
      },
      rateLimits: { maxPayments: 100, windowMs: 60_000 },
    },
  ],
};

Incoming sender rules use the payer recovered by facilitator/credential verification. Never scope them from an address in the request body, Origin, or Referer. Scope precedence is perEndpoint > perSender > global.

Durable and multi-instance enforcement

In-memory state gives each process its own budget. Four replicas with a $2 limit can collectively admit close to $8. For any restart or horizontally scaled deployment, inject one shared atomic payment store—normally Postgres—so check/reserve/stage/commit operations serialize correctly.

SQLite can persist one-host state but is not a generic multi-host coordinator. Use a stable agentId when several agents share the Postgres payment tables, and test cross-tenant isolation.

Human approvals

Lucid policy groups do not implement an approval queue. Add an application approval step before calling paidFetch when a payment exceeds an autonomy tier, targets a new counterparty, changes network/recipient, or buys a high-risk action. The approval artifact should bind the exact URL, payee, network, maximum amount, purpose, expiry, and operation ID.

Do not let an LLM approve its own exception or rewrite policy in response to a seller message.

Error handling

ResponseMeaningAction
403 policy_violationA configured limit, allowlist, denylist, or rate rule blocked paymentTreat as a control decision; do not bypass automatically
503 policy_storage_errorStore/evaluation failed or paid retry had no reservationStop signing; repair/retry with the same operation ID
503 reservation capacityToo many outstanding unpaid attemptsBackpressure buyers; shorten abandoned attempt lifetime carefully
Remote 402 after a paid retryCredential was not acceptedRelease local reservation; inspect version/network/asset/signature
Successful response without PAYMENT-RESPONSESettlement evidence absentLocal amount is not committed; escalate/reconcile before another charge
Recording failure after receiptExternal settlement may be irreversibleKeep staged record counted and reconcile; never reset the budget to silence the error

Verification tests

Test exact recipient and domain allow/deny behavior, boundary amounts, window rollover, concurrent calls at the remaining total, paid retry without reservation, reservation expiry, network exception, store disconnect, settlement receipt missing, and process loss after the external payment.

Next: build the budgeted buyer, handle retries, and configure durable storage.

On this page