@keelcodes/x402-gate 0.1.0 → 0.1.2
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 +49 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +20 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ export const GET = withX402(gateOptions, async () => Response.json({ price: 42 }
|
|
|
53
53
|
| `paymentsUrl` | 支付引擎地址(自建或 `https://keel.codes/v1/payment`) |
|
|
54
54
|
| `paymentsKey` | 支付引擎 Bearer key |
|
|
55
55
|
| `payTo` | 收款地址(B 端自己的钱包;资金直达,不经平台) |
|
|
56
|
-
| `amount` | 单次价格(最小单位);也可用 `amountFor(path)`
|
|
56
|
+
| `amount` | 单次价格(最小单位);也可用 `amountFor(path, req)` 动态定价(A7.9.3:`req.query` 可取 `limit` 做阶梯)。不填时从第一个带 `amount` 的 accept 取价(`0.1.1` 起;再没有就**直接报错**,不会静默免费放行) |
|
|
57
57
|
| `accepts` | 多链多资产;每项二选一:`eip3009: { name, version }` 或 `permit2: true` |
|
|
58
58
|
| `exempt` | 免费路径前缀(默认已含 `/health`、`/metrics`) |
|
|
59
59
|
| `passthroughHeaders` | 额外视为「已有平台凭证」的头(如 `x-tenant-id`) |
|
|
@@ -100,10 +100,55 @@ const gate = createX402Gate({
|
|
|
100
100
|
> `X-PAYMENT-RESPONSE`。`PAYMENT-REQUIRED` 头始终是 v2。
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
import { Keel } from '@keelcodes/sdk'
|
|
104
|
+
|
|
105
|
+
const keel = new Keel({
|
|
106
|
+
baseUrl: 'https://api.example', // 收费服务端(不是支付引擎)
|
|
107
|
+
x402: {
|
|
108
|
+
address: wallet.account.address, // viem WalletClient
|
|
109
|
+
signTypedData: (typed) => wallet.signTypedData(typed),
|
|
110
|
+
maxAmount: '2000', // 单笔上限(最小单位),超过直接拒付
|
|
111
|
+
},
|
|
105
112
|
})
|
|
106
|
-
await keel.
|
|
113
|
+
await keel.request('GET', '/quote') // 402 → 签名 → 重试 → 200
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 可运行 demo(agent 自付,零资金)
|
|
117
|
+
|
|
118
|
+
`projects/keel-sdk/examples/agent-autopay-demo.mjs` 把上面这条链路跑通,默认**全本地、离线、不上链**:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd projects/x402-gate && npm run build
|
|
122
|
+
cd ../keel-sdk && npm run build && node examples/agent-autopay-demo.mjs
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
输出依次覆盖:免费路径放行 → 未付款拿 `402`(v1 体 + v2 `PAYMENT-REQUIRED` 头 + `links`) →
|
|
126
|
+
Keel 自动签名重试 `200` → 超出 `maxAmount` 时拒付。demo 内置 mock facilitator,
|
|
127
|
+
只验 EIP-712 签名并回一个假 `transaction`,所以**不需要真钱包余额、也不需要 gas**。
|
|
128
|
+
|
|
129
|
+
接真环境时换成真收款地址/真付款私钥/真支付引擎(此时不再起本地服务):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
PAYMENTS_URL=https://keel.codes/v1/payment \
|
|
133
|
+
GATE_URL=https://your-service.example \
|
|
134
|
+
PAYER_PRIVATE_KEY=0x... PAY_TO=0x... \
|
|
135
|
+
node examples/agent-autopay-demo.mjs
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
demo 只演示 EIP-3009 轨(Base USDC);BSC USDT/USDC 走 Permit2 时,同一份代码由 SDK
|
|
139
|
+
自动改签 Permit2(`extra.assetTransferMethod=permit2`),首次使用需先按
|
|
140
|
+
「Permit2 预算」授权一次(见下)。
|
|
141
|
+
|
|
142
|
+
### Permit2 预算(授权上限)
|
|
143
|
+
|
|
144
|
+
Permit2 轨下,付款人要先给 **canonical Permit2 合约**(不是 proxy)一笔 ERC-20 allowance,
|
|
145
|
+
这笔额度就是链上硬上限;`approve` 是覆盖写,撤销 = `approve(Permit2, 0)`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
cd projects/payment/payments
|
|
149
|
+
npx tsx scripts/permit2-budget.ts --chain bsc --budget 5 # dry-run,只打印 calldata
|
|
150
|
+
npx tsx scripts/permit2-budget.ts --chain bsc --budget 5 --broadcast # 真广播
|
|
151
|
+
npx tsx scripts/permit2-budget.ts --chain bsc --revoke --broadcast # 撤销
|
|
107
152
|
```
|
|
108
153
|
|
|
109
154
|
## 状态码
|
package/dist/index.d.ts
CHANGED
|
@@ -121,8 +121,8 @@ export interface X402GateOptions {
|
|
|
121
121
|
accepts?: X402GateAccept[];
|
|
122
122
|
/** 命中这些路径前缀的请求不收费(探针/文档) */
|
|
123
123
|
exempt?: string[];
|
|
124
|
-
/** 按请求覆盖价格(最小单位);返回 null
|
|
125
|
-
amountFor?: (path: string) => string | null;
|
|
124
|
+
/** 按请求覆盖价格(最小单位);返回 null 表示不收费。A7.9.3:第二参可读 query 做 limit 阶梯 */
|
|
125
|
+
amountFor?: (path: string, req: GateRequest) => string | null;
|
|
126
126
|
/** 额外视为「已有平台凭证」的请求头(如 x-tenant-id) */
|
|
127
127
|
passthroughHeaders?: string[];
|
|
128
128
|
/** 资源元数据(写进 402 的 resource;B13.1 目录展示用) */
|
|
@@ -145,6 +145,8 @@ export interface GateRequest {
|
|
|
145
145
|
path: string;
|
|
146
146
|
url: string;
|
|
147
147
|
header: (name: string) => string | null;
|
|
148
|
+
/** 查询串(A7.9.3:`amountFor` 按 `limit` 阶梯计价用) */
|
|
149
|
+
query?: URLSearchParams;
|
|
148
150
|
/** 调用方地址(免费额度分桶用;缺省视为同一桶) */
|
|
149
151
|
ip?: string;
|
|
150
152
|
}
|
package/dist/index.js
CHANGED
|
@@ -59,10 +59,13 @@ function buildAccepts(opts, amount) {
|
|
|
59
59
|
const target = a.payTo ?? opts.payTo;
|
|
60
60
|
if (!target)
|
|
61
61
|
throw new Error('x402 gate: payTo is required (top-level or per accept)');
|
|
62
|
+
const priced = a.amount ?? amount;
|
|
63
|
+
if (!priced)
|
|
64
|
+
throw new Error('x402 gate: every accept needs an `amount` (or a top-level amount)');
|
|
62
65
|
const base = {
|
|
63
66
|
scheme: 'exact',
|
|
64
67
|
network: a.network,
|
|
65
|
-
amount:
|
|
68
|
+
amount: priced,
|
|
66
69
|
asset: a.asset,
|
|
67
70
|
payTo: target,
|
|
68
71
|
maxTimeoutSeconds: a.maxTimeoutSeconds ?? 300,
|
|
@@ -206,6 +209,15 @@ function hasPlatformCredential(req, extra) {
|
|
|
206
209
|
return typeof v === 'string' && v.trim().length > 0;
|
|
207
210
|
});
|
|
208
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* 本次调用的价格(最小单位)。
|
|
214
|
+
* 优先级:`amountFor(path, req)` > 顶层 `amount` > 第一个带价格的 accept。
|
|
215
|
+
* 只配 `accepts`(多币种)而不写顶层 `amount` 是文档里的标准用法,必须从 accept 取价,
|
|
216
|
+
* 否则 `amount` 为 undefined 会命中下面的「不收费」分支,等于整站免费。
|
|
217
|
+
*/
|
|
218
|
+
function resolveAmount(opts) {
|
|
219
|
+
return opts.amount ?? opts.accepts?.find((a) => a.amount)?.amount;
|
|
220
|
+
}
|
|
209
221
|
/**
|
|
210
222
|
* 网关核心:返回「放行」或「402 挑战」,与框架无关。
|
|
211
223
|
* 结算失败一律 402(携带原因),避免把上游错误变成 500。
|
|
@@ -216,9 +228,13 @@ async function authorizeX402(opts, req) {
|
|
|
216
228
|
return { action: 'pass' };
|
|
217
229
|
if (hasPlatformCredential(req, opts.passthroughHeaders ?? []))
|
|
218
230
|
return { action: 'pass' };
|
|
219
|
-
const amount = opts.amountFor ? opts.amountFor(req.path) : opts
|
|
220
|
-
|
|
231
|
+
const amount = opts.amountFor ? opts.amountFor(req.path, req) : resolveAmount(opts);
|
|
232
|
+
// `amountFor` 返回 null = 该路径明确免费;但「压根没配价格」必须报错,
|
|
233
|
+
// 不能静默放行(否则漏配一个 amount 就等于整站免费)。
|
|
234
|
+
if (amount === null)
|
|
221
235
|
return { action: 'pass' };
|
|
236
|
+
if (!amount)
|
|
237
|
+
throw new Error('x402 gate: no price configured (set `amount` or a priced accept)');
|
|
222
238
|
const signature = paymentSignature(req);
|
|
223
239
|
// 免费额度:只对「本来要收费、且没带付款凭证」的请求计数;已付款的正常走结算。
|
|
224
240
|
if (opts.freeQuota && !signature) {
|
|
@@ -346,6 +362,7 @@ function withX402(opts, handler) {
|
|
|
346
362
|
const result = await authorizeX402(opts, {
|
|
347
363
|
path: parsed.pathname,
|
|
348
364
|
url,
|
|
365
|
+
query: parsed.searchParams,
|
|
349
366
|
ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || undefined,
|
|
350
367
|
header: (name) => request.headers.get(name),
|
|
351
368
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keelcodes/x402-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Keel x402 v2 resource gate — Express / Fetch / Next.js adapters (no keys, no chain access)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
|
-
}
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist"
|