powertools-x402 0.1.1 → 0.2.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/README.md +76 -54
- package/dist/chunk-4ZP7AJJC.js +231 -0
- package/dist/chunk-FLK2T35N.js +7 -0
- package/dist/coinbase.cjs +31 -0
- package/dist/coinbase.d.cts +9 -0
- package/dist/coinbase.d.ts +9 -0
- package/dist/coinbase.js +6 -0
- package/dist/index.cjs +21 -1
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +4 -204
- package/dist/stripe.cjs +336 -0
- package/dist/stripe.d.cts +38 -0
- package/dist/stripe.d.ts +38 -0
- package/dist/stripe.js +80 -0
- package/dist/testing.cjs +84 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +56 -0
- package/package.json +46 -2
package/README.md
CHANGED
|
@@ -16,9 +16,11 @@ Requires Node 18+. `@aws-lambda-powertools/event-handler` is a peer dependency,
|
|
|
16
16
|
|
|
17
17
|
- No payment attached? The caller gets a 402 with signed payment requirements
|
|
18
18
|
- Payment attached? It gets verified with a facilitator before your handler runs
|
|
19
|
-
-
|
|
19
|
+
- With the default exact payment flow, settlement happens after your handler succeeds. If the handler throws or returns an error status, settlement does not occur. A `billable` predicate can also decline to charge for a response that succeeded
|
|
20
20
|
- Verified payment details (payer, amount, network) are available in the request store
|
|
21
21
|
|
|
22
|
+
One contract to understand before you ship: in the default exact flow, verify happens before your handler and settlement happens after. That's the right fit for work that can safely run before the money moves, like inference, generation, and data retrieval. It is not a transaction around your business logic. If your handler performs an irreversible side effect and settlement fails afterward, the work already happened. Keep that kind of work reversible or reconcilable, or handle it with your own orchestration.
|
|
23
|
+
|
|
22
24
|
## Usage
|
|
23
25
|
|
|
24
26
|
### Charge for a route
|
|
@@ -74,68 +76,51 @@ const x402 = createX402({
|
|
|
74
76
|
});
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
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.
|
|
79
|
+
This emits `PaymentRequired`, `PaymentRejected`, `PaymentVerified`, `PaymentSettled`, `PaymentCancelled`, `PaymentNotBillable`, 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.
|
|
80
|
+
|
|
81
|
+
### Settle on mainnet with the Coinbase facilitator
|
|
82
|
+
|
|
83
|
+
The free `x402.org` facilitator is testnet only. Mainnet payments settle through the [Coinbase Developer Platform](https://portal.cdp.coinbase.com/) facilitator:
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
```bash
|
|
86
|
+
npm install @coinbase/x402
|
|
87
|
+
```
|
|
80
88
|
|
|
81
89
|
```ts
|
|
90
|
+
import { coinbaseFacilitator } from 'powertools-x402/coinbase';
|
|
91
|
+
|
|
82
92
|
const x402 = createX402({
|
|
83
|
-
facilitator:
|
|
84
|
-
url: 'https://facilitator.example.com',
|
|
85
|
-
createAuthHeaders: async () => {
|
|
86
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
87
|
-
return { verify: headers, settle: headers, supported: headers };
|
|
88
|
-
},
|
|
89
|
-
},
|
|
93
|
+
facilitator: coinbaseFacilitator(), // reads CDP_API_KEY_ID and CDP_API_KEY_SECRET
|
|
90
94
|
network: 'eip155:8453', // Base mainnet
|
|
91
95
|
payTo: process.env.PAY_TO!,
|
|
92
96
|
});
|
|
93
97
|
```
|
|
94
98
|
|
|
99
|
+
Using a different facilitator? Pass `{ url, createAuthHeaders }` straight through the `facilitator` option.
|
|
100
|
+
|
|
95
101
|
### Settle into your Stripe balance
|
|
96
102
|
|
|
97
|
-
Stripe supports x402 through [machine payments](https://docs.stripe.com/payments/machine/x402).
|
|
103
|
+
Stripe supports x402 through [machine payments](https://docs.stripe.com/payments/machine/x402). Payments settle through the Coinbase facilitator into a Stripe crypto deposit address, and each settled payment is recorded as a Stripe PaymentIntent. Funds land in your Stripe balance next to your card payments.
|
|
98
104
|
|
|
99
105
|
```bash
|
|
100
106
|
npm install stripe @coinbase/x402
|
|
101
107
|
```
|
|
102
108
|
|
|
103
109
|
```ts
|
|
104
|
-
import {
|
|
105
|
-
import Stripe from 'stripe';
|
|
110
|
+
import { createStripeX402 } from 'powertools-x402/stripe';
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
112
|
+
// Reads STRIPE_SECRET_KEY, STRIPE_DEPOSIT_ADDRESS, CDP_API_KEY_ID, and CDP_API_KEY_SECRET
|
|
113
|
+
const x402 = createStripeX402();
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
facilitator: createFacilitatorConfig(process.env.CDP_API_KEY_ID!, process.env.CDP_API_KEY_SECRET!),
|
|
113
|
-
network: 'eip155:8453', // Base mainnet
|
|
114
|
-
payTo: process.env.STRIPE_DEPOSIT_ADDRESS!, // POST /v1/crypto/deposit_addresses, one time
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
x402.resourceServer.onAfterSettle(async ({ result, requirements }) => {
|
|
118
|
-
if (!result.success || !result.transaction) return;
|
|
119
|
-
await stripe.paymentIntents.create(
|
|
120
|
-
{
|
|
121
|
-
amount: Math.round(Number(requirements.amount) / 10_000), // atomic USDC to cents
|
|
122
|
-
currency: 'usd',
|
|
123
|
-
confirm: true,
|
|
124
|
-
payment_method_data: { type: 'crypto' },
|
|
125
|
-
payment_method_types: ['crypto'],
|
|
126
|
-
payment_method_options: {
|
|
127
|
-
crypto: {
|
|
128
|
-
mode: 'transaction_verification',
|
|
129
|
-
transaction_verification_options: { network: 'base', transaction_hash: result.transaction },
|
|
130
|
-
} as Stripe.PaymentIntentCreateParams.PaymentMethodOptions.Crypto,
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
{ idempotencyKey: result.transaction }
|
|
134
|
-
);
|
|
135
|
-
});
|
|
115
|
+
app.post('/paid', [x402.paid({ price: '$0.01' })], async () => ({ foo: 'bar' }));
|
|
136
116
|
```
|
|
137
117
|
|
|
138
|
-
|
|
118
|
+
Prefer explicit config? Pass `depositAddress`, `stripeSecretKey` (or your own `stripe` client), and `cdpApiKeyId`/`cdpApiKeySecret` directly. Every other `createX402` option works here too.
|
|
119
|
+
|
|
120
|
+
A couple things to know:
|
|
121
|
+
|
|
122
|
+
- Create your deposit address once with `POST /v1/crypto/deposit_addresses` and store it. Keep that call off your request path.
|
|
123
|
+
- This uses Stripe's `2026-05-27.preview` API version, and you'll need the Stablecoins and Crypto payment method approved on your Stripe account.
|
|
139
124
|
|
|
140
125
|
### Accept multiple payment options on one route
|
|
141
126
|
|
|
@@ -177,6 +162,41 @@ x402.paid({
|
|
|
177
162
|
});
|
|
178
163
|
```
|
|
179
164
|
|
|
165
|
+
### Decline to charge for a successful response
|
|
166
|
+
|
|
167
|
+
Sometimes the work succeeds, the caller should get the payload, and you still don't want the money. A `billable` predicate runs after your handler on the path that would otherwise settle, and returning `false` skips settlement:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
app.post(
|
|
171
|
+
'/extract',
|
|
172
|
+
[
|
|
173
|
+
x402.paid({
|
|
174
|
+
price: '$0.05',
|
|
175
|
+
billable: (response) =>
|
|
176
|
+
response
|
|
177
|
+
.json()
|
|
178
|
+
.then(({ fields }: { fields: Record<string, string> }) => Object.keys(fields).length > 0),
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
async (reqCtx) => {
|
|
182
|
+
const { document } = (await reqCtx.req.json()) as { document: string };
|
|
183
|
+
return { fields: await extractFields(document) };
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The caller gets their 200 and the empty result either way; they just aren't charged for it. The same shape covers an LLM route that returned a refusal, or a search that legitimately found nothing.
|
|
189
|
+
|
|
190
|
+
It's all or nothing: `billable` decides whether to charge the full price, not how much to charge. If what you want is to bill less for cheaper work, that's partial settlement, which the `exact` scheme doesn't do.
|
|
191
|
+
|
|
192
|
+
The predicate receives a clone of the handler's response, so reading its body is safe, plus the request context, so `reqCtx.get('payment')` is available. It may be async. Skipping settlement leaves the response exactly as your handler produced it, with no `PAYMENT-RESPONSE` receipt header.
|
|
193
|
+
|
|
194
|
+
A skip counts a `PaymentNotBillable` metric, deliberately separate from `PaymentCancelled`, which stays reserved for handlers that threw or failed. An alarm on cancellations won't fire on a route declining to bill. A predicate that throws is treated as not billable and logged at error, so a broken predicate never turns a successful response into a 500.
|
|
195
|
+
|
|
196
|
+
Note the direction: `billable` can only subtract a settlement, never add one. It isn't consulted at all when the handler throws or returns an error status, since those already skip settlement.
|
|
197
|
+
|
|
198
|
+
It also only prevents a charge when the selected payment flow settles *after* the handler. The default `exact` authorization flow does, and that's what you get unless you ask for something else. But `exact` also supports the `upfront` flow, chosen with `extra: { paymentFlow: 'upfront' }` on an `accepts` entry, and `upfront` and `escrow` both settle before your handler runs. By the time `billable` returns `false` there, the money has already moved and nothing can unmake the charge. Rather than report a refund that never happened, the middleware ignores the `false`, settles as normal so the caller still gets the receipt they paid for, and logs at error.
|
|
199
|
+
|
|
180
200
|
### Customize the 402 response
|
|
181
201
|
|
|
182
202
|
```ts
|
|
@@ -192,23 +212,25 @@ x402.paid({
|
|
|
192
212
|
|
|
193
213
|
### Test your routes without a network
|
|
194
214
|
|
|
195
|
-
|
|
215
|
+
Import from `powertools-x402/testing` and your tests never touch the network. `stubFacilitator()` approves every payment, and `testPayer()` signs real payments with a throwaway account so you can test the paid path end to end:
|
|
196
216
|
|
|
197
217
|
```ts
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
};
|
|
218
|
+
import { stubFacilitator, testPayer } from 'powertools-x402/testing';
|
|
219
|
+
|
|
220
|
+
const facilitator = stubFacilitator(); // defaults to Base Sepolia
|
|
221
|
+
const x402 = createX402({
|
|
222
|
+
facilitator,
|
|
223
|
+
network: 'eip155:84532',
|
|
224
|
+
payTo: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
|
|
225
|
+
});
|
|
207
226
|
|
|
208
|
-
|
|
227
|
+
// 402 challenge, then pay it and assert on your handler's behavior
|
|
228
|
+
const payer = testPayer();
|
|
229
|
+
const challenge = await app.resolve(event('POST', '/paid'), context);
|
|
230
|
+
const paid = await app.resolve(event('POST', '/paid', await payer.payFor(challenge)), context);
|
|
209
231
|
```
|
|
210
232
|
|
|
211
|
-
|
|
233
|
+
Spy on `facilitator.verify` or `facilitator.settle` to assert calls or force failures. `payFor` also accepts fetch `Response` objects. And if you'd rather roll your own facilitator stub, it's any object with `verify`, `settle`, and `getSupported`.
|
|
212
234
|
|
|
213
235
|
### Pay for a request (client side)
|
|
214
236
|
|
|
@@ -0,0 +1,231 @@
|
|
|
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 isBillable = async (predicate, reqCtx, path) => {
|
|
96
|
+
try {
|
|
97
|
+
return await predicate(reqCtx.res.clone(), reqCtx);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger?.error("x402 billable predicate threw", { path, error });
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const payers = /* @__PURE__ */ new WeakMap();
|
|
104
|
+
resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
|
|
105
|
+
if (result.payer) payers.set(paymentPayload, result.payer);
|
|
106
|
+
});
|
|
107
|
+
function paid(route) {
|
|
108
|
+
const { price, accepts, onProtectedRequest, billable, ...routeConfig } = route;
|
|
109
|
+
if (accepts === void 0 && price === void 0) {
|
|
110
|
+
throw new Error("paid() requires a price or an accepts configuration");
|
|
111
|
+
}
|
|
112
|
+
const httpServer = new x402HTTPResourceServer(resourceServer, {
|
|
113
|
+
...routeConfig,
|
|
114
|
+
mimeType: routeConfig.mimeType ?? "application/json",
|
|
115
|
+
accepts: accepts ?? {
|
|
116
|
+
scheme: "exact",
|
|
117
|
+
network: options.network,
|
|
118
|
+
payTo: options.payTo,
|
|
119
|
+
price
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (onProtectedRequest) httpServer.onProtectedRequest(onProtectedRequest);
|
|
123
|
+
let ready;
|
|
124
|
+
const initialize = () => ready ??= httpServer.initialize().catch((error) => {
|
|
125
|
+
ready = void 0;
|
|
126
|
+
throw error;
|
|
127
|
+
});
|
|
128
|
+
return async ({ reqCtx, next }) => {
|
|
129
|
+
await initialize();
|
|
130
|
+
const adapter = new PowertoolsAdapter(reqCtx.req);
|
|
131
|
+
const context = {
|
|
132
|
+
adapter,
|
|
133
|
+
path: adapter.getPath(),
|
|
134
|
+
method: adapter.getMethod()
|
|
135
|
+
};
|
|
136
|
+
const result = await httpServer.processHTTPRequest(context, options.paywall);
|
|
137
|
+
if (result.type === "no-payment-required") {
|
|
138
|
+
await next();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (result.type === "payment-error") {
|
|
142
|
+
count(adapter.getHeader("payment-signature") ? "PaymentRejected" : "PaymentRequired");
|
|
143
|
+
logger?.debug("x402 payment not accepted", { status: result.response.status });
|
|
144
|
+
reqCtx.res = toResponse(result.response);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const {
|
|
148
|
+
cancellationDispatcher,
|
|
149
|
+
beforeHandlerSettlement,
|
|
150
|
+
paymentPayload,
|
|
151
|
+
paymentRequirements,
|
|
152
|
+
declaredExtensions
|
|
153
|
+
} = result;
|
|
154
|
+
reqCtx.set("payment", {
|
|
155
|
+
network: paymentRequirements.network,
|
|
156
|
+
scheme: paymentRequirements.scheme,
|
|
157
|
+
asset: paymentRequirements.asset,
|
|
158
|
+
amount: paymentRequirements.amount,
|
|
159
|
+
payer: payers.get(paymentPayload)
|
|
160
|
+
});
|
|
161
|
+
count("PaymentVerified");
|
|
162
|
+
try {
|
|
163
|
+
await next();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
await cancellationDispatcher.cancel({ reason: "handler_threw", error });
|
|
166
|
+
count("PaymentCancelled");
|
|
167
|
+
logger?.warn("x402 payment cancelled: handler threw", { path: context.path });
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (reqCtx.res.status >= 400) {
|
|
171
|
+
await cancellationDispatcher.cancel({
|
|
172
|
+
reason: "handler_failed",
|
|
173
|
+
responseStatus: reqCtx.res.status
|
|
174
|
+
});
|
|
175
|
+
count("PaymentCancelled");
|
|
176
|
+
logger?.warn("x402 payment cancelled: handler failed", {
|
|
177
|
+
path: context.path,
|
|
178
|
+
status: reqCtx.res.status
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (billable && !await isBillable(billable, reqCtx, context.path)) {
|
|
183
|
+
if (beforeHandlerSettlement) {
|
|
184
|
+
logger?.error("x402 billable ignored: flow settles before the handler", {
|
|
185
|
+
path: context.path
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
await cancellationDispatcher.cancel({ reason: "after_verify_aborted" });
|
|
189
|
+
count("PaymentNotBillable");
|
|
190
|
+
logger?.warn("x402 payment not billed: route declined", { path: context.path });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const settlement = await httpServer.processSettlement(
|
|
195
|
+
paymentPayload,
|
|
196
|
+
paymentRequirements,
|
|
197
|
+
declaredExtensions,
|
|
198
|
+
{
|
|
199
|
+
request: context,
|
|
200
|
+
responseBody: Buffer.from(await reqCtx.res.clone().arrayBuffer()),
|
|
201
|
+
responseHeaders: Object.fromEntries(reqCtx.res.headers.entries())
|
|
202
|
+
},
|
|
203
|
+
void 0,
|
|
204
|
+
beforeHandlerSettlement
|
|
205
|
+
);
|
|
206
|
+
if (!settlement.success) {
|
|
207
|
+
count("SettlementFailed");
|
|
208
|
+
logger?.error("x402 settlement failed", {
|
|
209
|
+
path: context.path,
|
|
210
|
+
errorReason: settlement.errorReason
|
|
211
|
+
});
|
|
212
|
+
reqCtx.res = toResponse(settlement.response);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
count("PaymentSettled");
|
|
216
|
+
logger?.debug("x402 payment settled", { path: context.path, transaction: settlement.transaction });
|
|
217
|
+
for (const [name, value] of Object.entries(settlement.headers)) {
|
|
218
|
+
reqCtx.res.headers.set(name, value);
|
|
219
|
+
}
|
|
220
|
+
const cacheControl = reqCtx.res.headers.get("cache-control");
|
|
221
|
+
reqCtx.res.headers.set("cache-control", cacheControl ? `${cacheControl}, private` : "private");
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { paid, resourceServer };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export {
|
|
228
|
+
PowertoolsAdapter,
|
|
229
|
+
CachingFacilitatorClient,
|
|
230
|
+
createX402
|
|
231
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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/coinbase.ts
|
|
21
|
+
var coinbase_exports = {};
|
|
22
|
+
__export(coinbase_exports, {
|
|
23
|
+
coinbaseFacilitator: () => coinbaseFacilitator
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(coinbase_exports);
|
|
26
|
+
var import_x402 = require("@coinbase/x402");
|
|
27
|
+
var coinbaseFacilitator = (apiKeyId, apiKeySecret) => (0, import_x402.createFacilitatorConfig)(apiKeyId, apiKeySecret);
|
|
28
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
29
|
+
0 && (module.exports = {
|
|
30
|
+
coinbaseFacilitator
|
|
31
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FacilitatorConfig } from '@x402/core/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coinbase Developer Platform facilitator for mainnet settlement.
|
|
5
|
+
* Falls back to CDP_API_KEY_ID and CDP_API_KEY_SECRET when keys are omitted.
|
|
6
|
+
*/
|
|
7
|
+
declare const coinbaseFacilitator: (apiKeyId?: string, apiKeySecret?: string) => FacilitatorConfig;
|
|
8
|
+
|
|
9
|
+
export { coinbaseFacilitator };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FacilitatorConfig } from '@x402/core/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coinbase Developer Platform facilitator for mainnet settlement.
|
|
5
|
+
* Falls back to CDP_API_KEY_ID and CDP_API_KEY_SECRET when keys are omitted.
|
|
6
|
+
*/
|
|
7
|
+
declare const coinbaseFacilitator: (apiKeyId?: string, apiKeySecret?: string) => FacilitatorConfig;
|
|
8
|
+
|
|
9
|
+
export { coinbaseFacilitator };
|
package/dist/coinbase.js
ADDED
package/dist/index.cjs
CHANGED
|
@@ -114,12 +114,20 @@ function createX402(options) {
|
|
|
114
114
|
const { logger } = options;
|
|
115
115
|
const metrics = options.enableMetrics ? options.metrics ?? new import_metrics.Metrics({ namespace: "x402" }) : void 0;
|
|
116
116
|
const count = (name) => metrics && (metrics.singleMetric?.() ?? metrics).addMetric(name, "Count", 1);
|
|
117
|
+
const isBillable = async (predicate, reqCtx, path) => {
|
|
118
|
+
try {
|
|
119
|
+
return await predicate(reqCtx.res.clone(), reqCtx);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger?.error("x402 billable predicate threw", { path, error });
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
117
125
|
const payers = /* @__PURE__ */ new WeakMap();
|
|
118
126
|
resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
|
|
119
127
|
if (result.payer) payers.set(paymentPayload, result.payer);
|
|
120
128
|
});
|
|
121
129
|
function paid(route) {
|
|
122
|
-
const { price, accepts, onProtectedRequest, ...routeConfig } = route;
|
|
130
|
+
const { price, accepts, onProtectedRequest, billable, ...routeConfig } = route;
|
|
123
131
|
if (accepts === void 0 && price === void 0) {
|
|
124
132
|
throw new Error("paid() requires a price or an accepts configuration");
|
|
125
133
|
}
|
|
@@ -193,6 +201,18 @@ function createX402(options) {
|
|
|
193
201
|
});
|
|
194
202
|
return;
|
|
195
203
|
}
|
|
204
|
+
if (billable && !await isBillable(billable, reqCtx, context.path)) {
|
|
205
|
+
if (beforeHandlerSettlement) {
|
|
206
|
+
logger?.error("x402 billable ignored: flow settles before the handler", {
|
|
207
|
+
path: context.path
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
await cancellationDispatcher.cancel({ reason: "after_verify_aborted" });
|
|
211
|
+
count("PaymentNotBillable");
|
|
212
|
+
logger?.warn("x402 payment not billed: route declined", { path: context.path });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
196
216
|
const settlement = await httpServer.processSettlement(
|
|
197
217
|
paymentPayload,
|
|
198
218
|
paymentRequirements,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
1
|
+
import { RequestContext, Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
2
2
|
import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
|
|
3
3
|
import { PaymentOption } from '@x402/core/http';
|
|
4
4
|
import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
|
|
@@ -54,10 +54,19 @@ interface CreateX402Options {
|
|
|
54
54
|
enableMetrics?: boolean;
|
|
55
55
|
metrics?: X402Metrics;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Decides whether a successful response should be charged for. Receives a
|
|
59
|
+
* clone of the handler's response, so reading its body is safe. Returning
|
|
60
|
+
* false skips settlement. Only prevents a charge when the selected payment
|
|
61
|
+
* flow settles after the handler, as the default exact authorization flow
|
|
62
|
+
* does; see the README for flows that settle before it.
|
|
63
|
+
*/
|
|
64
|
+
type BillablePredicate = (response: Response, reqCtx: RequestContext<X402Environment>) => boolean | Promise<boolean>;
|
|
57
65
|
interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
|
|
58
66
|
price?: Price;
|
|
59
67
|
accepts?: PaymentOption | PaymentOption[];
|
|
60
68
|
onProtectedRequest?: ProtectedRequestHook;
|
|
69
|
+
billable?: BillablePredicate;
|
|
61
70
|
}
|
|
62
71
|
type PaymentInfo = {
|
|
63
72
|
network: Network;
|
|
@@ -78,4 +87,4 @@ declare function createX402(options: CreateX402Options): {
|
|
|
78
87
|
resourceServer: x402ResourceServer;
|
|
79
88
|
};
|
|
80
89
|
|
|
81
|
-
export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|
|
90
|
+
export { type BillablePredicate, CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
1
|
+
import { RequestContext, Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
2
2
|
import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
|
|
3
3
|
import { PaymentOption } from '@x402/core/http';
|
|
4
4
|
import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
|
|
@@ -54,10 +54,19 @@ interface CreateX402Options {
|
|
|
54
54
|
enableMetrics?: boolean;
|
|
55
55
|
metrics?: X402Metrics;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Decides whether a successful response should be charged for. Receives a
|
|
59
|
+
* clone of the handler's response, so reading its body is safe. Returning
|
|
60
|
+
* false skips settlement. Only prevents a charge when the selected payment
|
|
61
|
+
* flow settles after the handler, as the default exact authorization flow
|
|
62
|
+
* does; see the README for flows that settle before it.
|
|
63
|
+
*/
|
|
64
|
+
type BillablePredicate = (response: Response, reqCtx: RequestContext<X402Environment>) => boolean | Promise<boolean>;
|
|
57
65
|
interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
|
|
58
66
|
price?: Price;
|
|
59
67
|
accepts?: PaymentOption | PaymentOption[];
|
|
60
68
|
onProtectedRequest?: ProtectedRequestHook;
|
|
69
|
+
billable?: BillablePredicate;
|
|
61
70
|
}
|
|
62
71
|
type PaymentInfo = {
|
|
63
72
|
network: Network;
|
|
@@ -78,4 +87,4 @@ declare function createX402(options: CreateX402Options): {
|
|
|
78
87
|
resourceServer: x402ResourceServer;
|
|
79
88
|
};
|
|
80
89
|
|
|
81
|
-
export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|
|
90
|
+
export { type BillablePredicate, CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|