Skip to content

Repository files navigation

Agentic Pay for Vendure

npm version npm downloads npm total downloads License: MIT Vendure Protocol Network

Package: agentic-payment-x402-vendure (npm) · Ecosystem: AIOpsome Agentic Commerce

An x402 stablecoin payment method for Vendure — lets AI shopping agents (and any x402-aware client) pay for Vendure orders directly, without a card or a traditional checkout UI.

Part of the Agentic Pay suite by AIOpsome:

Note on previous package name: Renamed from vendure-payment-x402. The old npm package is deprecated and points here (npm install agentic-payment-x402-vendure).

How it maps onto Vendure

x402's protocol has a natural two-step shape — verify (check a signed payment is well-formed, no funds move) then settle (broadcast it) — which lines up cleanly with Vendure's Authorized → Settled payment states:

Vendure x402
createPayment (→ Authorized) facilitator verify
settlePayment (→ Settled) facilitator settle
cancelPayment no-op (nothing settled yet, so nothing to undo)
createRefund omitted — see Known limitations

createPayment only verifies (no funds move) because the x402 authorization the buyer signed carries a short validity window (maxTimeoutSeconds, default 300s — for the exact EVM scheme this is the EIP-3009 validBefore baked into the signature itself). Rather than requiring an admin to click "Settle" within that window, this plugin auto-settles: see Auto-settle.

Setup

npm install agentic-payment-x402-vendure
import { X402Plugin } from 'agentic-payment-x402-vendure';

plugins: [
  X402Plugin.init(),
  // ...
]

Then create a PaymentMethod in the Admin UI using the x402 handler and configure:

Arg Example Meaning
payToAddress 0xYourMerchantWallet Where settled funds land
network eip155:8453 CAIP-2 network id (e.g. Base)
asset 0x833589...2913 Stablecoin contract/mint address
assetDecimals 6 Decimals of that asset (6 for USDC)
assetName USDC The asset contract's EIP-712 domain name — required to sign/verify the EIP-3009 transferWithAuthorization typed data
assetVersion 2 The asset contract's EIP-712 domain version (2 for USDC)
pegCurrencyCode USD ISO 4217 currency the asset is assumed 1:1 pegged to
pegCurrencyDecimals 2 Decimals Vendure stores that currency in
facilitatorUrl (optional) Defaults to the public x402.org facilitator (testnet-only — use a production facilitator, or run your own, for mainnet)

Orders in any currency other than pegCurrencyCode are rejected by this payment method at both the quoting query and createPayment — there's no FX conversion, only a 1:1 peg assumption.

Rate limiting

Both activeOrderX402PaymentRequirements and createPayment are reachable by anonymous Shop API sessions. Without a local limit, a session could spam locally-valid-looking payloads that each still round-trip to the facilitator — a cost/rate-limit amplification vector against the facilitator relationship. This plugin applies a fixed-window rate limit, keyed by session token (falling back to request IP, then a shared bucket) before any facilitator call, with sane built-in defaults:

X402Plugin.init({
  rateLimit: {
    createPaymentMax: 10,   // default: 10 attempts per window
    requirementsMax: 30,    // default: 30 requests per window
    windowMs: 60_000,       // default: 60s window
  },
}),

The limiter is in-memory and per-process (this plugin has no Redis/cache dependency) — it doesn't coordinate across multiple app instances behind a load balancer, and it fails open on its own internal errors: a broken rate limiter blocking all checkout traffic is worse than temporarily unlimited traffic.

Storefront / agent flow

There's no server-issued "client secret" the way Stripe works. Instead:

  1. Query the Shop API for payment requirements:
    query {
      activeOrderX402PaymentRequirements {
        x402Version
        scheme
        network
        asset
        extra { name version }
        amount
        payTo
        maxTimeoutSeconds
      }
    }
  2. Sign a matching payment client-side with an x402-aware wallet/SDK — @x402/evm or @x402/svm depending on the network. (This plugin only depends on @x402/core; the buyer's client needs the chain-specific signing package, not the merchant server.)
  3. Submit it:
    mutation {
      addPaymentToOrder(input: {
        method: "x402",
        metadata: { paymentPayload: <the signed payload from step 2> }
      }) { ... }
    }
  4. Vendure calls createPayment, which verifies the payload with the facilitator and transitions the Payment to Authorized. No funds have moved yet.

Auto-settle (Authorized → Settled)

Vendure itself has no built-in path from Authorized to Settled — settlePayment is normally only invoked by an admin via the Admin API. Left alone, that's a problem for x402: the authorization's validity window (maxTimeoutSeconds) can lapse before a human gets to it, and the facilitator will then reject the settlement as expired even though the buyer's agent already believes it paid.

To close that gap, this plugin subscribes to Vendure's PaymentStateTransitionEvent and, the moment a Payment backed by the x402 handler reaches Authorized, calls settlePayment itself — well inside maxTimeoutSeconds, with no admin action required. This keeps the two-step verify/settle model (an admin can still manually settle or retry through the Admin API if needed) while making the common case fully automatic. Ownership is determined by resolving the Payment's PaymentMethod entity and checking its handler code, not the PaymentMethod's own (merchant-configurable) code — so this works no matter what you name the PaymentMethod in the Admin UI, and won't misfire for an unrelated PaymentMethod that happens to share a code with this handler.

If the auto-settle attempt fails, this plugin makes the failure visible — but the Payment is not guaranteed to leave Authorized in every case.

  • If the facilitator rejects the settlement, Vendure's own PaymentService.settlePayment transitions the Payment to Error and records the failure reason as payment.errorMessage, visible on the order in the Admin UI. This plugin also logs the outcome via Vendure's Logger (tagged x402).
  • If settlePayment instead returns an error result without moving the Payment out of Authorized (e.g. the Error state transition itself is rejected), or an unexpected error is thrown (network/DB error), the Payment can remain at Authorized. This plugin logs these cases explicitly via Logger.error (tagged x402) so they show up in server logs, but resolving them still requires an admin to intervene manually.

The plugin also guards against the state-transition event firing more than once for the same Payment (the event bus can in principle redeliver): it tracks in-flight settlement attempts per Payment ID and re-checks the Payment's current state immediately before calling settlePayment, so a duplicate event can't trigger a second settlement attempt once the first has started or finished.

Known limitations

  • No recovery across an app restart. The event subscription is live-only: a Payment that reaches Authorized in the moments around a deploy, crash, or worker restart is not picked up retroactively — no event fires for it. It sits at Authorized until an admin settles it manually or it lapses past maxTimeoutSeconds. A startup reconciliation sweep would close this gap but is out of scope for this release.
  • A DB write failure right after settlement can produce an incorrect decline, not just a lost record. settlePayment logs the tx hash via Logger.info the instant the facilitator confirms settlement — before Vendure's own PaymentService.settlePayment persists payment.metadata/state, which is the write that can actually fail — so a failure there is at least reconcilable from server logs instead of leaving zero trace. But a retried settlePayment after that kind of failure re-submits the same signed payload to the facilitator (the idempotency short-circuit in this handler only fires once payment.metadata.transaction is itself persisted, which is exactly what didn't happen). What the facilitator reports back for an already-settled/nonce-reused payload isn't something this plugin can verify without a live facilitator and isn't guaranteed to be a specific errorReason across facilitators, so that retry is currently treated as a generic settlement failure — an honest "funds already moved, this is not a real decline" outcome would need pattern-matching a specific facilitator's response that hasn't been tested end-to-end. Check server logs for the original tx hash before trusting an Error state on a retried settlement.
  • No automated refunds. x402 exact-scheme settlements are on-chain token transfers; the protocol has no facilitator-side reversal endpoint, and this plugin never holds merchant private keys to construct one itself. createRefund is intentionally omitted — per Vendure's own PaymentMethodHandler docs, omitting it means refunds are settled manually by an administrator, which is correct here, not a missing feature.
  • Requirements query quotes the outstanding balance, but skips the payment method eligibility check. activeOrderX402PaymentRequirements quotes order.totalWithTax minus what's already covered by other payments (so a split/partial payment across multiple methods is accounted for), but it doesn't run the x402 payment method's PaymentMethodEligibilityChecker the way Vendure's own addPaymentToOrder does -- it's possible to quote requirements for an order the method will actually reject at payment time if an eligibility checker is configured.
  • 1:1 peg assumption, no FX. pegCurrencyCode/pegCurrencyDecimals are explicit configuration because Vendure doesn't expose a public per-currency decimals table to plugins — there's no automatic ISO 4217 lookup, and no price-oracle conversion for non-pegged assets.
  • assetName/assetVersion are required, not optional. EIP-3009 transferWithAuthorization signing/verification needs the asset contract's own EIP-712 domain to reconstruct the signed typed-data hash. Omitting these makes the facilitator reject every payment with invalid_exact_evm_missing_eip712_domain, even when the buyer signed correctly — this was caught by e2e testing against a live facilitator, not by the unit tests, since the mocked facilitator doesn't validate signatures.
  • Amount-correctness ultimately depends on the configured facilitator. createPayment validates that the submitted paymentPayload has the expected shape and that its accepted fields (scheme/network/asset/payTo/amount) match the server-built requirements before forwarding anything, which rejects a malformed or amount-mismatched payload outright. But the cryptographic guarantee that the signature itself actually authorizes that amount is enforced by the facilitator's verify call, not by this plugin — a facilitator that doesn't correctly implement exact-scheme verification could still authorize a payload this plugin considers well-formed. Use a facilitator you trust to enforce this.

E2e-verified against a real Vendure server + Postgres + the public x402.org facilitator on Base Sepolia testnet: a signed EIP-3009 USDC payment cleared verify and settle, the Order reached PaymentSettled, and the transfer landed on-chain.

Development

npm install
npm run typecheck
npm run lint
npm run test
npm run build   # tsc, not esbuild/tsup — NestJS's emitDecoratorMetadata
                # needs the real TS compiler; esbuild silently drops it

About

x402 stablecoin payment method for Vendure — lets AI shopping agents pay for orders via the x402 protocol

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages