@mingderwang/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/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @mingderwang/x402
2
+
3
+ `HTTP 402 Payment Required` helpers for the [x402](https://github.com/x402-foundation/specs) standard — payment requirements, EIP-3009 signing, `X-PAYMENT`, verification, and on-chain settlement.
4
+
5
+ Built on ethers v6 + `@mingderwang/wallet`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @mingderwang/x402
11
+ ```
12
+
13
+ ## Client side — detect a paywall and pay
14
+
15
+ ```ts
16
+ import { detectPaymentRequired, makePaymentPayload, makePaymentRequired } from '@mingderwang/x402';
17
+
18
+ const res = await fetch('https://api.example.com/resource', { headers: { authorization: 'Bearer ...' } });
19
+ if (res.status === 402) {
20
+ const reqs = detectPaymentRequired({ status: res.status, headers: res.headers, text: await res.text() });
21
+ const payload = await makePaymentPayload({ wallet, requirement: reqs.accepts[0] });
22
+ const retry = await fetch('https://api.example.com/resource', {
23
+ headers: { 'x-payment': Buffer.from(JSON.stringify(payload)).toString('base64') },
24
+ });
25
+ }
26
+ ```
27
+
28
+ ## Server side — issue requirements, verify, settle
29
+
30
+ ```ts
31
+ import { makePaymentRequired, verifyPaymentPayload, settlePayment } from '@mingderwang/x402';
32
+
33
+ const requirement = makePaymentRequired({
34
+ resource: '/api/resource', description: 'Pay per use',
35
+ network: 'ethereum-sepolia', amount: '500000', /* 0.5 USDC (6 decimals) */
36
+ asset: usdcAddress, payTo: sellerAddress,
37
+ }).accepts[0];
38
+
39
+ const check = verifyPaymentPayload(parsedXPayment, requirement); // { isValid, payer }
40
+ if (!check.isValid) return new Response(JSON.stringify({ x402Version: 1, error: check.invalidReason }), { status: 402 });
41
+
42
+ const settled = await settlePayment(parsedXPayment, requirement, {}); // broadcasts transferWithAuthorization (needs X402_GAS_PRIVATE_KEY)
43
+ ```
44
+
45
+ - Scheme: `exact`, network/asset/timeouts configurable.
46
+ - Replay protection: keep signed nonces; reject repeats with `invalid_transaction_state`.
47
+ - Errors follow the spec: `insufficient_funds`, `invalid_exact_evm_payload_*`, etc.
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1 @@
1
+ export * from '../src/index';
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // src/index.ts
2
+ import { randomBytesHex, NETWORKS } from "@mingderwang/wallet";
3
+ import { verifyTypedData, Contract, JsonRpcProvider, Wallet } from "ethers";
4
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
5
+ var PAYMENT_SIGNATURE_HEADER = "x-payment";
6
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
7
+ var ERROR = {
8
+ insufficientFunds: "insufficient_funds",
9
+ invalidValidAfter: "invalid_exact_evm_payload_authorization_valid_after",
10
+ invalidValidBefore: "invalid_exact_evm_payload_authorization_valid_before",
11
+ invalidValue: "invalid_exact_evm_payload_authorization_value",
12
+ invalidSignature: "invalid_exact_evm_payload_signature",
13
+ recipientMismatch: "invalid_exact_evm_payload_recipient_mismatch",
14
+ invalidNetwork: "invalid_network",
15
+ invalidPayload: "invalid_payload",
16
+ invalidPaymentRequirements: "invalid_payment_requirements",
17
+ invalidScheme: "invalid_scheme",
18
+ invalidVersion: "invalid_x402_version",
19
+ invalidTxState: "invalid_transaction_state"
20
+ };
21
+ function encodeJson(obj) {
22
+ return Buffer.from(JSON.stringify(obj)).toString("base64");
23
+ }
24
+ function decodeJson(b64) {
25
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
26
+ }
27
+ function makePaymentRequired(opts) {
28
+ return {
29
+ x402Version: 1,
30
+ error: `${PAYMENT_SIGNATURE_HEADER.toUpperCase()} header is required`,
31
+ accepts: [
32
+ {
33
+ scheme: "exact",
34
+ network: opts.network,
35
+ maxAmountRequired: opts.amount,
36
+ asset: opts.asset.toLowerCase(),
37
+ payTo: opts.payTo.toLowerCase(),
38
+ resource: opts.resource,
39
+ description: opts.description,
40
+ mimeType: opts.mimeType ?? "application/json",
41
+ outputSchema: null,
42
+ maxTimeoutSeconds: opts.maxTimeoutSeconds ?? 300,
43
+ extra: opts.extra ?? { name: "USDC", version: "2" }
44
+ }
45
+ ]
46
+ };
47
+ }
48
+ function detectPaymentRequired(response) {
49
+ if (response.status !== 402)
50
+ return null;
51
+ const header = response.headers?.get?.(PAYMENT_REQUIRED_HEADER);
52
+ if (header) {
53
+ try {
54
+ return decodeJson(header);
55
+ } catch {}
56
+ }
57
+ if (response.text) {
58
+ try {
59
+ const parsed = JSON.parse(response.text);
60
+ if (parsed && Array.isArray(parsed.accepts))
61
+ return parsed;
62
+ } catch {}
63
+ }
64
+ return null;
65
+ }
66
+ async function makePaymentPayload({ wallet, requirement, nowSeconds }) {
67
+ const { NETWORKS: _net } = { NETWORKS };
68
+ const chainId = chainIdForNetwork(requirement.network);
69
+ const now = nowSeconds ?? Math.floor(Date.now() / 1000);
70
+ const value = BigInt(requirement.maxAmountRequired);
71
+ const domain = {
72
+ name: "USD Coin",
73
+ version: "2",
74
+ chainId,
75
+ verifyingContract: requirement.asset
76
+ };
77
+ const types = {
78
+ TransferWithAuthorization: [
79
+ { name: "from", type: "address" },
80
+ { name: "to", type: "address" },
81
+ { name: "value", type: "uint256" },
82
+ { name: "validAfter", type: "uint256" },
83
+ { name: "validBefore", type: "uint256" },
84
+ { name: "nonce", type: "bytes32" }
85
+ ]
86
+ };
87
+ const authorization = {
88
+ from: wallet.address,
89
+ to: requirement.payTo,
90
+ value: value.toString(),
91
+ validAfter: (now - 60).toString(),
92
+ validBefore: (now + requirement.maxTimeoutSeconds).toString(),
93
+ nonce: randomBytesHex(32)
94
+ };
95
+ const signature = await wallet.signTypedData(domain, types, { from: authorization.from, to: authorization.to, value: authorization.value, validAfter: authorization.validAfter, validBefore: authorization.validBefore, nonce: authorization.nonce });
96
+ return {
97
+ x402Version: 1,
98
+ scheme: "exact",
99
+ network: requirement.network,
100
+ payload: { signature, authorization }
101
+ };
102
+ }
103
+ function chainIdForNetwork(network) {
104
+ for (const n of Object.values(NETWORKS)) {
105
+ if (n.networkId === network)
106
+ return n.chainId;
107
+ }
108
+ const m = /-(\d+)$/.exec(network);
109
+ if (m)
110
+ return Number(m[1]);
111
+ throw new Error(`Unknown network ${network}`);
112
+ }
113
+ function xPaymentHeader(payload) {
114
+ return encodeJson(payload);
115
+ }
116
+ var TRANSFER_WITH_AUTH_ABI = [
117
+ "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce)",
118
+ "function balanceOf(address) view returns (uint256)"
119
+ ];
120
+ function verifyPaymentPayload(payload, requirement) {
121
+ if (!payload || typeof payload !== "object") {
122
+ return { isValid: false, invalidReason: ERROR.invalidPayload };
123
+ }
124
+ const p = payload;
125
+ if (p.x402Version !== 1)
126
+ return { isValid: false, invalidReason: ERROR.invalidVersion };
127
+ if (p.scheme !== "exact")
128
+ return { isValid: false, invalidReason: ERROR.invalidScheme };
129
+ if (p.network !== requirement.network)
130
+ return { isValid: false, invalidReason: ERROR.invalidNetwork };
131
+ const a = p.payload?.authorization;
132
+ const sig = p.payload?.signature;
133
+ if (!a || !sig)
134
+ return { isValid: false, invalidReason: ERROR.invalidPayload };
135
+ const now = Math.floor(Date.now() / 1000);
136
+ const validAfter = Number(a.validAfter);
137
+ const validBefore = Number(a.validBefore);
138
+ if (now < validAfter)
139
+ return { isValid: false, invalidReason: ERROR.invalidValidAfter };
140
+ if (now > validBefore)
141
+ return { isValid: false, invalidReason: ERROR.invalidValidBefore };
142
+ if (BigInt(a.value) < BigInt(requirement.maxAmountRequired)) {
143
+ return { isValid: false, invalidReason: ERROR.invalidValue };
144
+ }
145
+ if (a.to.toLowerCase() !== requirement.payTo.toLowerCase()) {
146
+ return { isValid: false, invalidReason: ERROR.recipientMismatch };
147
+ }
148
+ if (!/^0x[a-fA-F0-9]{40}$/.test(a.from) || !/^0x[a-fA-F0-9]{64}$/.test(a.nonce)) {
149
+ return { isValid: false, invalidReason: ERROR.invalidPayload };
150
+ }
151
+ const chainId = chainIdForNetwork(requirement.network);
152
+ const domain = { name: "USD Coin", version: "2", chainId, verifyingContract: requirement.asset };
153
+ const types = {
154
+ TransferWithAuthorization: [
155
+ { name: "from", type: "address" },
156
+ { name: "to", type: "address" },
157
+ { name: "value", type: "uint256" },
158
+ { name: "validAfter", type: "uint256" },
159
+ { name: "validBefore", type: "uint256" },
160
+ { name: "nonce", type: "bytes32" }
161
+ ]
162
+ };
163
+ const message = { from: a.from, to: a.to, value: a.value, validAfter: a.validAfter, validBefore: a.validBefore, nonce: a.nonce };
164
+ try {
165
+ const signer = verifyTypedData(domain, types, message, sig);
166
+ if (signer.toLowerCase() !== a.from.toLowerCase()) {
167
+ return { isValid: false, invalidReason: ERROR.invalidSignature };
168
+ }
169
+ } catch {
170
+ return { isValid: false, invalidReason: ERROR.invalidSignature };
171
+ }
172
+ return { isValid: true, payer: a.from };
173
+ }
174
+ async function settlePayment(payload, requirement, opts) {
175
+ const a = payload.payload.authorization;
176
+ const network = requirement.network;
177
+ const rpc = opts.rpcUrl ?? process.env.X402_RPC_URL;
178
+ const provider = new JsonRpcProvider(rpc);
179
+ const signer = opts.signer ?? (process.env.X402_GAS_PRIVATE_KEY ? new Wallet(process.env.X402_GAS_PRIVATE_KEY, provider) : undefined);
180
+ if (!signer) {
181
+ return { success: false, errorReason: "server has no gas signer configured", transaction: "", network, payer: a.from };
182
+ }
183
+ const contract = new Contract(requirement.asset, TRANSFER_WITH_AUTH_ABI, signer);
184
+ const erc20 = contract;
185
+ try {
186
+ const balance = await erc20.balanceOf(a.from);
187
+ if (balance < BigInt(a.value)) {
188
+ return { success: false, errorReason: ERROR.insufficientFunds, transaction: "", network, payer: a.from };
189
+ }
190
+ const tx = await erc20.transferWithAuthorization(a.from, a.to, a.value, a.validAfter, a.validBefore, a.nonce);
191
+ await tx.wait(opts.confirmations ?? 1);
192
+ return { success: true, transaction: tx.hash, network, payer: a.from };
193
+ } catch (e) {
194
+ return {
195
+ success: false,
196
+ errorReason: ERROR.invalidTxState,
197
+ transaction: "",
198
+ network,
199
+ payer: a.from,
200
+ ...e?.shortMessage ? { errorReason: e.shortMessage } : {}
201
+ };
202
+ }
203
+ }
204
+ export {
205
+ xPaymentHeader,
206
+ verifyPaymentPayload,
207
+ settlePayment,
208
+ makePaymentRequired,
209
+ makePaymentPayload,
210
+ encodeJson,
211
+ detectPaymentRequired,
212
+ decodeJson,
213
+ chainIdForNetwork,
214
+ PAYMENT_SIGNATURE_HEADER,
215
+ PAYMENT_RESPONSE_HEADER,
216
+ PAYMENT_REQUIRED_HEADER,
217
+ ERROR
218
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mingderwang/x402",
3
+ "version": "0.1.0",
4
+ "description": "x402 (HTTP 402 Payment Required) client + server helpers: payment requirements, EIP-3009 signing, X-PAYMENT, verification and settlement.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "node ./scripts/build.mjs"
21
+ },
22
+ "dependencies": {
23
+ "@mingderwang/wallet": "^0.1.0",
24
+ "ethers": "^6.13.4"
25
+ },
26
+ "keywords": [
27
+ "x402",
28
+ "http402",
29
+ "payment",
30
+ "eip-3009",
31
+ "usdc",
32
+ "paywall"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/mingderwang/network-vulnerability-scanner.git"
37
+ }
38
+ }