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:
- 🛒 Agentic Pay for WooCommerce — WordPress / WooCommerce Gateway (
agentic-pay-for-woocommerce) - 🛍️ Agentic Pay for Vendure — Vendure Plugin (
agentic-payment-x402-vendure) - 🌙 Agentic Pay for Lunar — Lunar Driver (
aiopsome/agentic-payment-x402-lunar)
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).
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.
npm install agentic-payment-x402-vendureimport { 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.
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.
There's no server-issued "client secret" the way Stripe works. Instead:
- Query the Shop API for payment requirements:
query { activeOrderX402PaymentRequirements { x402Version scheme network asset extra { name version } amount payTo maxTimeoutSeconds } }
- Sign a matching payment client-side with an x402-aware wallet/SDK —
@x402/evmor@x402/svmdepending on the network. (This plugin only depends on@x402/core; the buyer's client needs the chain-specific signing package, not the merchant server.) - Submit it:
mutation { addPaymentToOrder(input: { method: "x402", metadata: { paymentPayload: <the signed payload from step 2> } }) { ... } }
- Vendure calls
createPayment, which verifies the payload with the facilitator and transitions the Payment toAuthorized. No funds have moved yet.
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.settlePaymenttransitions the Payment toErrorand records the failure reason aspayment.errorMessage, visible on the order in the Admin UI. This plugin also logs the outcome via Vendure'sLogger(taggedx402). - If
settlePaymentinstead returns an error result without moving the Payment out ofAuthorized(e.g. theErrorstate transition itself is rejected), or an unexpected error is thrown (network/DB error), the Payment can remain atAuthorized. This plugin logs these cases explicitly viaLogger.error(taggedx402) 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.
- No recovery across an app restart. The event subscription is live-only: a Payment that reaches
Authorizedin the moments around a deploy, crash, or worker restart is not picked up retroactively — no event fires for it. It sits atAuthorizeduntil an admin settles it manually or it lapses pastmaxTimeoutSeconds. 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.
settlePaymentlogs the tx hash viaLogger.infothe instant the facilitator confirms settlement — before Vendure's ownPaymentService.settlePaymentpersistspayment.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 retriedsettlePaymentafter that kind of failure re-submits the same signed payload to the facilitator (the idempotency short-circuit in this handler only fires oncepayment.metadata.transactionis 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 specificerrorReasonacross 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.createRefundis intentionally omitted — per Vendure's ownPaymentMethodHandlerdocs, 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.
activeOrderX402PaymentRequirementsquotesorder.totalWithTaxminus 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'sPaymentMethodEligibilityCheckerthe way Vendure's ownaddPaymentToOrderdoes -- 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/pegCurrencyDecimalsare 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/assetVersionare required, not optional. EIP-3009transferWithAuthorizationsigning/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 withinvalid_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.
createPaymentvalidates that the submittedpaymentPayloadhas the expected shape and that itsacceptedfields (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'sverifycall, 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.
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