powertools-x402 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allen Helton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # powertools-x402
2
+
3
+ Paid API routes for [AWS Lambda Powertools Event Handler](https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/api-gateway/) using the [x402 payment protocol](https://x402.org).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install powertools-x402
9
+ ```
10
+
11
+ Requires Node 18+. `@aws-lambda-powertools/event-handler` is a peer dependency, so npm will grab it automatically if your app doesn't already have it.
12
+
13
+ ## What it does
14
+
15
+ `x402.paid()` is route middleware that turns any route into a paid endpoint. The route owns the price. Your handler stays pure business logic.
16
+
17
+ - No payment attached? The caller gets a 402 with signed payment requirements
18
+ - Payment attached? It gets verified with a facilitator before your handler runs
19
+ - Settlement happens after your handler succeeds. If it throws or returns an error status, the caller is never charged
20
+ - Verified payment details (payer, amount, network) are available in the request store
21
+
22
+ ## Usage
23
+
24
+ ### Charge for a route
25
+
26
+ ```ts
27
+ import { Router } from '@aws-lambda-powertools/event-handler/http';
28
+ import type { Context } from 'aws-lambda';
29
+ import { createX402, type X402Environment } from 'powertools-x402';
30
+
31
+ const app = new Router<X402Environment>();
32
+
33
+ const x402 = createX402({
34
+ facilitator: 'https://x402.org/facilitator',
35
+ network: 'eip155:84532', // Base Sepolia
36
+ payTo: process.env.PAY_TO!,
37
+ });
38
+
39
+ app.get('/health', async () => ({ status: 'ok' }));
40
+
41
+ app.post(
42
+ '/summarize',
43
+ [x402.paid({ price: '$0.01', description: 'Summarize some text' })],
44
+ async (reqCtx) => {
45
+ const { text } = (await reqCtx.req.json()) as { text: string };
46
+ return { summary: text.slice(0, 80) };
47
+ }
48
+ );
49
+
50
+ export const handler = (event: unknown, context: Context) => app.resolve(event, context);
51
+ ```
52
+
53
+ ### Read payment details in the handler
54
+
55
+ ```ts
56
+ app.post('/generate', [x402.paid({ price: '$0.05' })], async (reqCtx) => {
57
+ const payment = reqCtx.get('payment');
58
+ // { network: 'eip155:84532', scheme: 'exact', asset: '0x...', amount: '50000', payer: '0x...' }
59
+ return { result: 'expensive thing', paidBy: payment?.payer };
60
+ });
61
+ ```
62
+
63
+ ### Emit CloudWatch metrics
64
+
65
+ ```ts
66
+ const x402 = createX402({
67
+ facilitator: 'https://x402.org/facilitator',
68
+ network: 'eip155:84532',
69
+ payTo: process.env.PAY_TO!,
70
+ enableMetrics: true, // defaults to false
71
+ logger: new Logger(), // optional, any Powertools Logger works
72
+ });
73
+ ```
74
+
75
+ This emits `PaymentRequired`, `PaymentRejected`, `PaymentVerified`, `PaymentSettled`, `PaymentCancelled`, and `SettlementFailed` counts under the `x402` namespace. Metrics publish immediately, so there's no `publishStoredMetrics()` call to remember. Pass your own `metrics` instance if you want a different namespace.
76
+
77
+ ### Use an authenticated facilitator
78
+
79
+ ```ts
80
+ const x402 = createX402({
81
+ facilitator: {
82
+ url: 'https://facilitator.example.com',
83
+ createAuthHeaders: async () => {
84
+ const headers = { Authorization: `Bearer ${token}` };
85
+ return { verify: headers, settle: headers, supported: headers };
86
+ },
87
+ },
88
+ network: 'eip155:8453', // Base mainnet
89
+ payTo: process.env.PAY_TO!,
90
+ });
91
+ ```
92
+
93
+ ### Accept multiple payment options on one route
94
+
95
+ ```ts
96
+ x402.paid({
97
+ accepts: [
98
+ { scheme: 'exact', network: 'eip155:8453', payTo: evmAddress, price: '$0.05' },
99
+ { scheme: 'exact', network: 'eip155:84532', payTo: evmAddress, price: '$0.01' },
100
+ ],
101
+ });
102
+ ```
103
+
104
+ Want to take payments on non-EVM networks? Register additional schemes with the `schemes` option on `createX402`.
105
+
106
+ ### Let some callers through free
107
+
108
+ ```ts
109
+ x402.paid({
110
+ price: '$0.01',
111
+ onProtectedRequest: async (ctx) => {
112
+ if (ctx.adapter.getHeader('x-api-key') === trustedKey) return { grantAccess: true };
113
+ },
114
+ });
115
+ ```
116
+
117
+ ### Customize the 402 response
118
+
119
+ ```ts
120
+ x402.paid({
121
+ price: '$0.01',
122
+ resource: 'https://api.example.com/summarize', // canonical URL behind custom domains
123
+ unpaidResponseBody: async () => ({
124
+ contentType: 'application/json',
125
+ body: { preview: 'The first 10 words are free...' },
126
+ }),
127
+ });
128
+ ```
129
+
130
+ ### Test your routes without a network
131
+
132
+ The `facilitator` option takes any object with `verify`, `settle`, and `getSupported`, so your tests never touch the network:
133
+
134
+ ```ts
135
+ const facilitator = {
136
+ getSupported: async () => ({
137
+ kinds: [{ x402Version: 2, scheme: 'exact', network: 'eip155:84532' }],
138
+ extensions: [],
139
+ signers: {},
140
+ }),
141
+ verify: async () => ({ isValid: true, payer: '0xPayer' }),
142
+ settle: async () => ({ success: true, transaction: '0xTx', network: 'eip155:84532' }),
143
+ };
144
+
145
+ const x402 = createX402({ facilitator, network: 'eip155:84532', payTo: '0xYou' });
146
+ ```
147
+
148
+ Drive your router with API Gateway events through `app.resolve(event, context)`. Check out [test/middleware.test.ts](test/middleware.test.ts) for full round trips, including client-side payment signing.
149
+
150
+ ### Pay for a request (client side)
151
+
152
+ In the paying app, install `@x402/core`, `@x402/evm`, and `viem`:
153
+
154
+ ```ts
155
+ import { x402Client, x402HTTPClient } from '@x402/core/client';
156
+ import { registerExactEvmScheme } from '@x402/evm/exact/client';
157
+ import { privateKeyToAccount } from 'viem/accounts';
158
+
159
+ const client = new x402HTTPClient(
160
+ registerExactEvmScheme(new x402Client(), { signer: privateKeyToAccount(privateKey) })
161
+ );
162
+
163
+ // 1. Call without payment -> 402 with a PAYMENT-REQUIRED header
164
+ const challenge = await fetch(url, { method: 'POST', body });
165
+
166
+ // 2. Sign a payment for one of the offered options
167
+ const paymentRequired = client.getPaymentRequiredResponse((name) => challenge.headers.get(name));
168
+ const payment = await client.createPaymentPayload(paymentRequired);
169
+
170
+ // 3. Retry with the PAYMENT-SIGNATURE header -> 200 + settlement receipt
171
+ const paid = await fetch(url, { method: 'POST', headers: client.encodePaymentSignatureHeader(payment), body });
172
+ const receipt = client.getPaymentSettleResponse((name) => paid.headers.get(name));
173
+ ```
174
+
175
+ Need testnet USDC? Grab some from the [Circle faucet](https://faucet.circle.com/).
176
+
177
+ ## Examples
178
+
179
+ - [example/handler.ts](example/handler.ts) - lambdalith with free and paid routes
180
+ - [example/client.ts](example/client.ts) - paying client, step by step
181
+ - [example/template.yaml](example/template.yaml) - SAM deploy (esbuild, ESM, HTTP API)
182
+
183
+ ## License
184
+
185
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CachingFacilitatorClient: () => CachingFacilitatorClient,
24
+ PowertoolsAdapter: () => PowertoolsAdapter,
25
+ createX402: () => createX402
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var import_metrics = require("@aws-lambda-powertools/metrics");
29
+ var import_server = require("@x402/core/server");
30
+ var import_server2 = require("@x402/evm/exact/server");
31
+
32
+ // src/adapter.ts
33
+ var PowertoolsAdapter = class {
34
+ constructor(request) {
35
+ this.request = request;
36
+ }
37
+ request;
38
+ getHeader(name) {
39
+ return this.request.headers.get(name) ?? void 0;
40
+ }
41
+ getMethod() {
42
+ return this.request.method;
43
+ }
44
+ getPath() {
45
+ return new URL(this.request.url).pathname;
46
+ }
47
+ getUrl() {
48
+ return this.request.url;
49
+ }
50
+ getAcceptHeader() {
51
+ return this.request.headers.get("accept") ?? "";
52
+ }
53
+ getUserAgent() {
54
+ return this.request.headers.get("user-agent") ?? "";
55
+ }
56
+ getQueryParams() {
57
+ const params = new URL(this.request.url).searchParams;
58
+ const result = {};
59
+ for (const key of new Set(params.keys())) {
60
+ const values = params.getAll(key);
61
+ result[key] = values.length === 1 ? values[0] : values;
62
+ }
63
+ return result;
64
+ }
65
+ getQueryParam(name) {
66
+ const values = new URL(this.request.url).searchParams.getAll(name);
67
+ if (values.length === 0) return void 0;
68
+ return values.length === 1 ? values[0] : values;
69
+ }
70
+ async getBody() {
71
+ try {
72
+ return await this.request.clone().json();
73
+ } catch {
74
+ return void 0;
75
+ }
76
+ }
77
+ };
78
+
79
+ // src/facilitator.ts
80
+ var CachingFacilitatorClient = class {
81
+ constructor(client) {
82
+ this.client = client;
83
+ }
84
+ client;
85
+ #supported;
86
+ verify(payload, requirements) {
87
+ return this.client.verify(payload, requirements);
88
+ }
89
+ settle(payload, requirements) {
90
+ return this.client.settle(payload, requirements);
91
+ }
92
+ getSupported() {
93
+ this.#supported ??= this.client.getSupported().catch((error) => {
94
+ this.#supported = void 0;
95
+ throw error;
96
+ });
97
+ return this.#supported;
98
+ }
99
+ };
100
+
101
+ // src/index.ts
102
+ var toFacilitatorClient = (facilitator) => {
103
+ if (typeof facilitator === "string") return new import_server.HTTPFacilitatorClient({ url: facilitator });
104
+ if ("getSupported" in facilitator) return facilitator;
105
+ return new import_server.HTTPFacilitatorClient(facilitator);
106
+ };
107
+ var toResponse = ({ status, headers, body, isHtml }) => new Response(isHtml ? String(body ?? "") : JSON.stringify(body ?? {}), { status, headers });
108
+ function createX402(options) {
109
+ const facilitator = new CachingFacilitatorClient(toFacilitatorClient(options.facilitator));
110
+ const resourceServer = new import_server.x402ResourceServer(facilitator);
111
+ for (const register of options.schemes ?? [import_server2.registerExactEvmScheme]) {
112
+ register(resourceServer);
113
+ }
114
+ const { logger } = options;
115
+ const metrics = options.enableMetrics ? options.metrics ?? new import_metrics.Metrics({ namespace: "x402" }) : void 0;
116
+ const count = (name) => metrics && (metrics.singleMetric?.() ?? metrics).addMetric(name, "Count", 1);
117
+ const payers = /* @__PURE__ */ new WeakMap();
118
+ resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
119
+ if (result.payer) payers.set(paymentPayload, result.payer);
120
+ });
121
+ function paid(route) {
122
+ const { price, accepts, onProtectedRequest, ...routeConfig } = route;
123
+ if (accepts === void 0 && price === void 0) {
124
+ throw new Error("paid() requires a price or an accepts configuration");
125
+ }
126
+ const httpServer = new import_server.x402HTTPResourceServer(resourceServer, {
127
+ ...routeConfig,
128
+ mimeType: routeConfig.mimeType ?? "application/json",
129
+ accepts: accepts ?? {
130
+ scheme: "exact",
131
+ network: options.network,
132
+ payTo: options.payTo,
133
+ price
134
+ }
135
+ });
136
+ if (onProtectedRequest) httpServer.onProtectedRequest(onProtectedRequest);
137
+ let ready;
138
+ const initialize = () => ready ??= httpServer.initialize().catch((error) => {
139
+ ready = void 0;
140
+ throw error;
141
+ });
142
+ return async ({ reqCtx, next }) => {
143
+ await initialize();
144
+ const adapter = new PowertoolsAdapter(reqCtx.req);
145
+ const context = {
146
+ adapter,
147
+ path: adapter.getPath(),
148
+ method: adapter.getMethod()
149
+ };
150
+ const result = await httpServer.processHTTPRequest(context, options.paywall);
151
+ if (result.type === "no-payment-required") {
152
+ await next();
153
+ return;
154
+ }
155
+ if (result.type === "payment-error") {
156
+ count(adapter.getHeader("payment-signature") ? "PaymentRejected" : "PaymentRequired");
157
+ logger?.debug("x402 payment not accepted", { status: result.response.status });
158
+ reqCtx.res = toResponse(result.response);
159
+ return;
160
+ }
161
+ const {
162
+ cancellationDispatcher,
163
+ beforeHandlerSettlement,
164
+ paymentPayload,
165
+ paymentRequirements,
166
+ declaredExtensions
167
+ } = result;
168
+ reqCtx.set("payment", {
169
+ network: paymentRequirements.network,
170
+ scheme: paymentRequirements.scheme,
171
+ asset: paymentRequirements.asset,
172
+ amount: paymentRequirements.amount,
173
+ payer: payers.get(paymentPayload)
174
+ });
175
+ count("PaymentVerified");
176
+ try {
177
+ await next();
178
+ } catch (error) {
179
+ await cancellationDispatcher.cancel({ reason: "handler_threw", error });
180
+ count("PaymentCancelled");
181
+ logger?.warn("x402 payment cancelled: handler threw", { path: context.path });
182
+ throw error;
183
+ }
184
+ if (reqCtx.res.status >= 400) {
185
+ await cancellationDispatcher.cancel({
186
+ reason: "handler_failed",
187
+ responseStatus: reqCtx.res.status
188
+ });
189
+ count("PaymentCancelled");
190
+ logger?.warn("x402 payment cancelled: handler failed", {
191
+ path: context.path,
192
+ status: reqCtx.res.status
193
+ });
194
+ return;
195
+ }
196
+ const settlement = await httpServer.processSettlement(
197
+ paymentPayload,
198
+ paymentRequirements,
199
+ declaredExtensions,
200
+ {
201
+ request: context,
202
+ responseBody: Buffer.from(await reqCtx.res.clone().arrayBuffer()),
203
+ responseHeaders: Object.fromEntries(reqCtx.res.headers.entries())
204
+ },
205
+ void 0,
206
+ beforeHandlerSettlement
207
+ );
208
+ if (!settlement.success) {
209
+ count("SettlementFailed");
210
+ logger?.error("x402 settlement failed", {
211
+ path: context.path,
212
+ errorReason: settlement.errorReason
213
+ });
214
+ reqCtx.res = toResponse(settlement.response);
215
+ return;
216
+ }
217
+ count("PaymentSettled");
218
+ logger?.debug("x402 payment settled", { path: context.path, transaction: settlement.transaction });
219
+ for (const [name, value] of Object.entries(settlement.headers)) {
220
+ reqCtx.res.headers.set(name, value);
221
+ }
222
+ const cacheControl = reqCtx.res.headers.get("cache-control");
223
+ reqCtx.res.headers.set("cache-control", cacheControl ? `${cacheControl}, private` : "private");
224
+ };
225
+ }
226
+ return { paid, resourceServer };
227
+ }
228
+ // Annotate the CommonJS export names for ESM import in node:
229
+ 0 && (module.exports = {
230
+ CachingFacilitatorClient,
231
+ PowertoolsAdapter,
232
+ createX402
233
+ });
@@ -0,0 +1,81 @@
1
+ import { Middleware } from '@aws-lambda-powertools/event-handler/types';
2
+ import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
3
+ import { PaymentOption } from '@x402/core/http';
4
+ import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
5
+
6
+ /**
7
+ * Bridges the web-standard Request exposed by Powertools Event Handler
8
+ * to x402's framework-agnostic HTTPAdapter.
9
+ */
10
+ declare class PowertoolsAdapter implements HTTPAdapter {
11
+ private readonly request;
12
+ constructor(request: Request);
13
+ getHeader(name: string): string | undefined;
14
+ getMethod(): string;
15
+ getPath(): string;
16
+ getUrl(): string;
17
+ getAcceptHeader(): string;
18
+ getUserAgent(): string;
19
+ getQueryParams(): Record<string, string | string[]>;
20
+ getQueryParam(name: string): string | string[] | undefined;
21
+ getBody(): Promise<unknown>;
22
+ }
23
+
24
+ /**
25
+ * Memoizes getSupported() so every paid route on a cold start shares one
26
+ * facilitator round trip instead of re-fetching per route.
27
+ */
28
+ declare class CachingFacilitatorClient implements FacilitatorClient {
29
+ #private;
30
+ private readonly client;
31
+ constructor(client: FacilitatorClient);
32
+ verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise<VerifyResponse>;
33
+ settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise<SettleResponse>;
34
+ getSupported(): Promise<SupportedResponse>;
35
+ }
36
+
37
+ interface X402Logger {
38
+ debug(message: string, extra: Record<string, unknown>): void;
39
+ warn(message: string, extra: Record<string, unknown>): void;
40
+ error(message: string, extra: Record<string, unknown>): void;
41
+ }
42
+ interface X402Metrics {
43
+ addMetric(name: string, unit: 'Count', value: number): void;
44
+ singleMetric?(): Pick<X402Metrics, 'addMetric'>;
45
+ }
46
+ type SchemeRegistrar = (server: x402ResourceServer) => unknown;
47
+ interface CreateX402Options {
48
+ facilitator: string | FacilitatorConfig | FacilitatorClient;
49
+ network: Network;
50
+ payTo: string;
51
+ schemes?: SchemeRegistrar[];
52
+ paywall?: PaywallConfig;
53
+ logger?: X402Logger;
54
+ enableMetrics?: boolean;
55
+ metrics?: X402Metrics;
56
+ }
57
+ interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
58
+ price?: Price;
59
+ accepts?: PaymentOption | PaymentOption[];
60
+ onProtectedRequest?: ProtectedRequestHook;
61
+ }
62
+ type PaymentInfo = {
63
+ network: Network;
64
+ scheme: string;
65
+ asset: string;
66
+ amount: string;
67
+ payer?: string;
68
+ };
69
+ type X402Environment = {
70
+ store: {
71
+ request: {
72
+ payment?: PaymentInfo;
73
+ };
74
+ };
75
+ };
76
+ declare function createX402(options: CreateX402Options): {
77
+ paid: (route: PaidRouteOptions) => Middleware<X402Environment>;
78
+ resourceServer: x402ResourceServer;
79
+ };
80
+
81
+ export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
@@ -0,0 +1,81 @@
1
+ import { Middleware } from '@aws-lambda-powertools/event-handler/types';
2
+ import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
3
+ import { PaymentOption } from '@x402/core/http';
4
+ import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
5
+
6
+ /**
7
+ * Bridges the web-standard Request exposed by Powertools Event Handler
8
+ * to x402's framework-agnostic HTTPAdapter.
9
+ */
10
+ declare class PowertoolsAdapter implements HTTPAdapter {
11
+ private readonly request;
12
+ constructor(request: Request);
13
+ getHeader(name: string): string | undefined;
14
+ getMethod(): string;
15
+ getPath(): string;
16
+ getUrl(): string;
17
+ getAcceptHeader(): string;
18
+ getUserAgent(): string;
19
+ getQueryParams(): Record<string, string | string[]>;
20
+ getQueryParam(name: string): string | string[] | undefined;
21
+ getBody(): Promise<unknown>;
22
+ }
23
+
24
+ /**
25
+ * Memoizes getSupported() so every paid route on a cold start shares one
26
+ * facilitator round trip instead of re-fetching per route.
27
+ */
28
+ declare class CachingFacilitatorClient implements FacilitatorClient {
29
+ #private;
30
+ private readonly client;
31
+ constructor(client: FacilitatorClient);
32
+ verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise<VerifyResponse>;
33
+ settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise<SettleResponse>;
34
+ getSupported(): Promise<SupportedResponse>;
35
+ }
36
+
37
+ interface X402Logger {
38
+ debug(message: string, extra: Record<string, unknown>): void;
39
+ warn(message: string, extra: Record<string, unknown>): void;
40
+ error(message: string, extra: Record<string, unknown>): void;
41
+ }
42
+ interface X402Metrics {
43
+ addMetric(name: string, unit: 'Count', value: number): void;
44
+ singleMetric?(): Pick<X402Metrics, 'addMetric'>;
45
+ }
46
+ type SchemeRegistrar = (server: x402ResourceServer) => unknown;
47
+ interface CreateX402Options {
48
+ facilitator: string | FacilitatorConfig | FacilitatorClient;
49
+ network: Network;
50
+ payTo: string;
51
+ schemes?: SchemeRegistrar[];
52
+ paywall?: PaywallConfig;
53
+ logger?: X402Logger;
54
+ enableMetrics?: boolean;
55
+ metrics?: X402Metrics;
56
+ }
57
+ interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
58
+ price?: Price;
59
+ accepts?: PaymentOption | PaymentOption[];
60
+ onProtectedRequest?: ProtectedRequestHook;
61
+ }
62
+ type PaymentInfo = {
63
+ network: Network;
64
+ scheme: string;
65
+ asset: string;
66
+ amount: string;
67
+ payer?: string;
68
+ };
69
+ type X402Environment = {
70
+ store: {
71
+ request: {
72
+ payment?: PaymentInfo;
73
+ };
74
+ };
75
+ };
76
+ declare function createX402(options: CreateX402Options): {
77
+ paid: (route: PaidRouteOptions) => Middleware<X402Environment>;
78
+ resourceServer: x402ResourceServer;
79
+ };
80
+
81
+ export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
package/dist/index.js ADDED
@@ -0,0 +1,210 @@
1
+ // src/index.ts
2
+ import { Metrics } from "@aws-lambda-powertools/metrics";
3
+ import {
4
+ HTTPFacilitatorClient,
5
+ x402HTTPResourceServer,
6
+ x402ResourceServer
7
+ } from "@x402/core/server";
8
+ import { registerExactEvmScheme } from "@x402/evm/exact/server";
9
+
10
+ // src/adapter.ts
11
+ var PowertoolsAdapter = class {
12
+ constructor(request) {
13
+ this.request = request;
14
+ }
15
+ request;
16
+ getHeader(name) {
17
+ return this.request.headers.get(name) ?? void 0;
18
+ }
19
+ getMethod() {
20
+ return this.request.method;
21
+ }
22
+ getPath() {
23
+ return new URL(this.request.url).pathname;
24
+ }
25
+ getUrl() {
26
+ return this.request.url;
27
+ }
28
+ getAcceptHeader() {
29
+ return this.request.headers.get("accept") ?? "";
30
+ }
31
+ getUserAgent() {
32
+ return this.request.headers.get("user-agent") ?? "";
33
+ }
34
+ getQueryParams() {
35
+ const params = new URL(this.request.url).searchParams;
36
+ const result = {};
37
+ for (const key of new Set(params.keys())) {
38
+ const values = params.getAll(key);
39
+ result[key] = values.length === 1 ? values[0] : values;
40
+ }
41
+ return result;
42
+ }
43
+ getQueryParam(name) {
44
+ const values = new URL(this.request.url).searchParams.getAll(name);
45
+ if (values.length === 0) return void 0;
46
+ return values.length === 1 ? values[0] : values;
47
+ }
48
+ async getBody() {
49
+ try {
50
+ return await this.request.clone().json();
51
+ } catch {
52
+ return void 0;
53
+ }
54
+ }
55
+ };
56
+
57
+ // src/facilitator.ts
58
+ var CachingFacilitatorClient = class {
59
+ constructor(client) {
60
+ this.client = client;
61
+ }
62
+ client;
63
+ #supported;
64
+ verify(payload, requirements) {
65
+ return this.client.verify(payload, requirements);
66
+ }
67
+ settle(payload, requirements) {
68
+ return this.client.settle(payload, requirements);
69
+ }
70
+ getSupported() {
71
+ this.#supported ??= this.client.getSupported().catch((error) => {
72
+ this.#supported = void 0;
73
+ throw error;
74
+ });
75
+ return this.#supported;
76
+ }
77
+ };
78
+
79
+ // src/index.ts
80
+ var toFacilitatorClient = (facilitator) => {
81
+ if (typeof facilitator === "string") return new HTTPFacilitatorClient({ url: facilitator });
82
+ if ("getSupported" in facilitator) return facilitator;
83
+ return new HTTPFacilitatorClient(facilitator);
84
+ };
85
+ var toResponse = ({ status, headers, body, isHtml }) => new Response(isHtml ? String(body ?? "") : JSON.stringify(body ?? {}), { status, headers });
86
+ function createX402(options) {
87
+ const facilitator = new CachingFacilitatorClient(toFacilitatorClient(options.facilitator));
88
+ const resourceServer = new x402ResourceServer(facilitator);
89
+ for (const register of options.schemes ?? [registerExactEvmScheme]) {
90
+ register(resourceServer);
91
+ }
92
+ const { logger } = options;
93
+ const metrics = options.enableMetrics ? options.metrics ?? new Metrics({ namespace: "x402" }) : void 0;
94
+ const count = (name) => metrics && (metrics.singleMetric?.() ?? metrics).addMetric(name, "Count", 1);
95
+ const payers = /* @__PURE__ */ new WeakMap();
96
+ resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
97
+ if (result.payer) payers.set(paymentPayload, result.payer);
98
+ });
99
+ function paid(route) {
100
+ const { price, accepts, onProtectedRequest, ...routeConfig } = route;
101
+ if (accepts === void 0 && price === void 0) {
102
+ throw new Error("paid() requires a price or an accepts configuration");
103
+ }
104
+ const httpServer = new x402HTTPResourceServer(resourceServer, {
105
+ ...routeConfig,
106
+ mimeType: routeConfig.mimeType ?? "application/json",
107
+ accepts: accepts ?? {
108
+ scheme: "exact",
109
+ network: options.network,
110
+ payTo: options.payTo,
111
+ price
112
+ }
113
+ });
114
+ if (onProtectedRequest) httpServer.onProtectedRequest(onProtectedRequest);
115
+ let ready;
116
+ const initialize = () => ready ??= httpServer.initialize().catch((error) => {
117
+ ready = void 0;
118
+ throw error;
119
+ });
120
+ return async ({ reqCtx, next }) => {
121
+ await initialize();
122
+ const adapter = new PowertoolsAdapter(reqCtx.req);
123
+ const context = {
124
+ adapter,
125
+ path: adapter.getPath(),
126
+ method: adapter.getMethod()
127
+ };
128
+ const result = await httpServer.processHTTPRequest(context, options.paywall);
129
+ if (result.type === "no-payment-required") {
130
+ await next();
131
+ return;
132
+ }
133
+ if (result.type === "payment-error") {
134
+ count(adapter.getHeader("payment-signature") ? "PaymentRejected" : "PaymentRequired");
135
+ logger?.debug("x402 payment not accepted", { status: result.response.status });
136
+ reqCtx.res = toResponse(result.response);
137
+ return;
138
+ }
139
+ const {
140
+ cancellationDispatcher,
141
+ beforeHandlerSettlement,
142
+ paymentPayload,
143
+ paymentRequirements,
144
+ declaredExtensions
145
+ } = result;
146
+ reqCtx.set("payment", {
147
+ network: paymentRequirements.network,
148
+ scheme: paymentRequirements.scheme,
149
+ asset: paymentRequirements.asset,
150
+ amount: paymentRequirements.amount,
151
+ payer: payers.get(paymentPayload)
152
+ });
153
+ count("PaymentVerified");
154
+ try {
155
+ await next();
156
+ } catch (error) {
157
+ await cancellationDispatcher.cancel({ reason: "handler_threw", error });
158
+ count("PaymentCancelled");
159
+ logger?.warn("x402 payment cancelled: handler threw", { path: context.path });
160
+ throw error;
161
+ }
162
+ if (reqCtx.res.status >= 400) {
163
+ await cancellationDispatcher.cancel({
164
+ reason: "handler_failed",
165
+ responseStatus: reqCtx.res.status
166
+ });
167
+ count("PaymentCancelled");
168
+ logger?.warn("x402 payment cancelled: handler failed", {
169
+ path: context.path,
170
+ status: reqCtx.res.status
171
+ });
172
+ return;
173
+ }
174
+ const settlement = await httpServer.processSettlement(
175
+ paymentPayload,
176
+ paymentRequirements,
177
+ declaredExtensions,
178
+ {
179
+ request: context,
180
+ responseBody: Buffer.from(await reqCtx.res.clone().arrayBuffer()),
181
+ responseHeaders: Object.fromEntries(reqCtx.res.headers.entries())
182
+ },
183
+ void 0,
184
+ beforeHandlerSettlement
185
+ );
186
+ if (!settlement.success) {
187
+ count("SettlementFailed");
188
+ logger?.error("x402 settlement failed", {
189
+ path: context.path,
190
+ errorReason: settlement.errorReason
191
+ });
192
+ reqCtx.res = toResponse(settlement.response);
193
+ return;
194
+ }
195
+ count("PaymentSettled");
196
+ logger?.debug("x402 payment settled", { path: context.path, transaction: settlement.transaction });
197
+ for (const [name, value] of Object.entries(settlement.headers)) {
198
+ reqCtx.res.headers.set(name, value);
199
+ }
200
+ const cacheControl = reqCtx.res.headers.get("cache-control");
201
+ reqCtx.res.headers.set("cache-control", cacheControl ? `${cacheControl}, private` : "private");
202
+ };
203
+ }
204
+ return { paid, resourceServer };
205
+ }
206
+ export {
207
+ CachingFacilitatorClient,
208
+ PowertoolsAdapter,
209
+ createX402
210
+ };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "powertools-x402",
3
+ "version": "0.1.0",
4
+ "description": "x402 payment middleware for AWS Lambda Powertools Event Handler",
5
+ "license": "MIT",
6
+ "author": "Allen Helton <allenheltondev@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/allenheltondev/powertools-x402.git"
10
+ },
11
+ "keywords": [
12
+ "x402",
13
+ "payments",
14
+ "aws-lambda",
15
+ "powertools",
16
+ "middleware",
17
+ "serverless"
18
+ ],
19
+ "type": "module",
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ }
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "sideEffects": false,
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:coverage": "vitest run --coverage --coverage.include=src/**",
47
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
48
+ },
49
+ "dependencies": {
50
+ "@aws-lambda-powertools/metrics": "^2.35.0",
51
+ "@x402/core": "^2.23.0",
52
+ "@x402/evm": "^2.23.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@aws-lambda-powertools/event-handler": "^2.35.0"
56
+ },
57
+ "devDependencies": {
58
+ "@aws-lambda-powertools/event-handler": "^2.35.0",
59
+ "@aws-lambda-powertools/logger": "^2.35.0",
60
+ "@types/aws-lambda": "^8.10.152",
61
+ "@types/node": "^24.0.0",
62
+ "@vitest/coverage-v8": "^3.2.7",
63
+ "tsup": "^8.4.0",
64
+ "typescript": "^5.7.0",
65
+ "viem": "^2.48.0",
66
+ "vitest": "^3.0.0"
67
+ }
68
+ }