@a3stack/payments 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/package.json +33 -0
- package/src/client.ts +209 -0
- package/src/constants.ts +73 -0
- package/src/index.ts +26 -0
- package/src/server.ts +225 -0
- package/src/types.ts +94 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a3stack/payments",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "x402 payment flows for AI agents — client (paying) and server (receiving)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --outDir dist",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@x402/fetch": "^2.4.0",
|
|
22
|
+
"@x402/evm": "^2.4.0",
|
|
23
|
+
"viem": "^2.39.3",
|
|
24
|
+
"zod": "^3.24.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "^5.4.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Client — wraps x402/fetch for easy agent-to-agent payments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createPublicClient, createWalletClient, http, formatUnits } from "viem";
|
|
6
|
+
import type { Account } from "viem";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MAX_AMOUNT,
|
|
9
|
+
NETWORK_USDC,
|
|
10
|
+
DEFAULT_NETWORK,
|
|
11
|
+
ERC20_ABI,
|
|
12
|
+
NETWORK_RPC,
|
|
13
|
+
BASE_RPC,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import type {
|
|
16
|
+
PaymentClientConfig,
|
|
17
|
+
PaymentBalance,
|
|
18
|
+
PaymentDetails,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
export class PaymentClient {
|
|
22
|
+
private config: PaymentClientConfig;
|
|
23
|
+
private _paidFetch?: typeof fetch;
|
|
24
|
+
|
|
25
|
+
constructor(config: PaymentClientConfig) {
|
|
26
|
+
this.config = {
|
|
27
|
+
...config,
|
|
28
|
+
chains: config.chains ?? ["eip155:*"],
|
|
29
|
+
maxAmountPerRequest: config.maxAmountPerRequest ?? DEFAULT_MAX_AMOUNT,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize the payment-wrapped fetch (lazy — only creates when first used)
|
|
35
|
+
*/
|
|
36
|
+
private async getPaidFetch(): Promise<typeof fetch> {
|
|
37
|
+
if (this._paidFetch) return this._paidFetch;
|
|
38
|
+
|
|
39
|
+
// Dynamic import to avoid bundling issues
|
|
40
|
+
const [{ wrapFetchWithPaymentFromConfig }, { ExactEvmScheme, toClientEvmSigner }] = await Promise.all([
|
|
41
|
+
import("@x402/fetch"),
|
|
42
|
+
import("@x402/evm"),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const account = this.config.account as Account;
|
|
46
|
+
const chains = this.config.chains!;
|
|
47
|
+
|
|
48
|
+
// ExactEvmScheme requires a ClientEvmSigner with address + signTypedData + readContract
|
|
49
|
+
// We build a minimal signer from the viem account + a public client for readContract
|
|
50
|
+
const rpcUrl = BASE_RPC;
|
|
51
|
+
const publicClient = createPublicClient({ transport: http(rpcUrl) });
|
|
52
|
+
|
|
53
|
+
// Build a ClientEvmSigner manually
|
|
54
|
+
const signer = toClientEvmSigner({
|
|
55
|
+
address: account.address,
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
signTypedData: (params: any) => account.signTypedData?.(params) ?? Promise.reject(new Error("signTypedData not available on this account")),
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
readContract: (params: any) => publicClient.readContract(params),
|
|
60
|
+
} as Parameters<typeof toClientEvmSigner>[0]);
|
|
61
|
+
|
|
62
|
+
// Build scheme registrations
|
|
63
|
+
const schemes = chains.map((network) => ({
|
|
64
|
+
network: network as `${string}:${string}`,
|
|
65
|
+
client: new ExactEvmScheme(signer),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
this._paidFetch = wrapFetchWithPaymentFromConfig(fetch, { schemes });
|
|
69
|
+
return this._paidFetch;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A fetch function that automatically handles x402 payment challenges
|
|
74
|
+
*/
|
|
75
|
+
get fetch(): (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response> {
|
|
76
|
+
return async (input, init) => {
|
|
77
|
+
const paidFetch = await this.getPaidFetch();
|
|
78
|
+
return paidFetch(input, init);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a payment-capable fetch pre-configured for a specific agent.
|
|
84
|
+
* The wallet is resolved automatically from the 402 payment requirements.
|
|
85
|
+
*/
|
|
86
|
+
fetchForWallet(
|
|
87
|
+
_paymentWallet: `0x${string}`
|
|
88
|
+
): (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response> {
|
|
89
|
+
// The x402 protocol handles payment wallet resolution automatically via 402 response
|
|
90
|
+
// This just returns the standard paid fetch — the wallet in payment requirements
|
|
91
|
+
// is provided by the server's 402 response
|
|
92
|
+
return this.fetch;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check your USDC balance on a network
|
|
97
|
+
*/
|
|
98
|
+
async getBalance(
|
|
99
|
+
network = DEFAULT_NETWORK,
|
|
100
|
+
rpc?: string
|
|
101
|
+
): Promise<PaymentBalance> {
|
|
102
|
+
const usdcAddress = NETWORK_USDC[network];
|
|
103
|
+
if (!usdcAddress) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`No USDC address configured for network "${network}". ` +
|
|
106
|
+
`Supported networks: ${Object.keys(NETWORK_USDC).join(", ")}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rpcUrl = rpc ?? NETWORK_RPC[network] ?? BASE_RPC;
|
|
111
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
112
|
+
|
|
113
|
+
const [rawBalance, decimals, symbol] = await Promise.all([
|
|
114
|
+
client.readContract({
|
|
115
|
+
address: usdcAddress,
|
|
116
|
+
abi: ERC20_ABI,
|
|
117
|
+
functionName: "balanceOf",
|
|
118
|
+
args: [this.config.account.address],
|
|
119
|
+
}) as Promise<bigint>,
|
|
120
|
+
client.readContract({
|
|
121
|
+
address: usdcAddress,
|
|
122
|
+
abi: ERC20_ABI,
|
|
123
|
+
functionName: "decimals",
|
|
124
|
+
}) as Promise<number>,
|
|
125
|
+
client.readContract({
|
|
126
|
+
address: usdcAddress,
|
|
127
|
+
abi: ERC20_ABI,
|
|
128
|
+
functionName: "symbol",
|
|
129
|
+
}) as Promise<string>,
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
amount: rawBalance,
|
|
134
|
+
formatted: formatUnits(rawBalance, decimals),
|
|
135
|
+
symbol,
|
|
136
|
+
decimals,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decode payment receipt from a successful x402 response
|
|
142
|
+
* Returns null if no payment was made (wasn't a 402 endpoint)
|
|
143
|
+
*/
|
|
144
|
+
decodeReceipt(response: Response): PaymentDetails | null {
|
|
145
|
+
const paymentResponseHeader = response.headers.get("x-payment-response") ??
|
|
146
|
+
response.headers.get("PAYMENT-RESPONSE");
|
|
147
|
+
if (!paymentResponseHeader) return null;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// The x402 response header contains base64-encoded payment details
|
|
151
|
+
const decoded = JSON.parse(
|
|
152
|
+
Buffer.from(paymentResponseHeader, "base64").toString("utf8")
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
from: decoded.sender ?? decoded.from,
|
|
156
|
+
to: decoded.recipient ?? decoded.to ?? decoded.payTo,
|
|
157
|
+
amount: decoded.amount ?? decoded.value,
|
|
158
|
+
asset: decoded.asset ?? decoded.token,
|
|
159
|
+
network: decoded.network ?? decoded.chain ?? DEFAULT_NETWORK,
|
|
160
|
+
txHash: decoded.txHash ?? decoded.hash,
|
|
161
|
+
timestamp: decoded.timestamp ?? Date.now(),
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if an endpoint requires payment (without making the actual request)
|
|
170
|
+
*/
|
|
171
|
+
async checkPaymentRequirements(url: string): Promise<{
|
|
172
|
+
requiresPayment: boolean;
|
|
173
|
+
amount?: string;
|
|
174
|
+
asset?: string;
|
|
175
|
+
network?: string;
|
|
176
|
+
payTo?: string;
|
|
177
|
+
}> {
|
|
178
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
179
|
+
|
|
180
|
+
if (response.status !== 402) {
|
|
181
|
+
return { requiresPayment: false };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const reqHeader = response.headers.get("x-payment-required") ??
|
|
185
|
+
response.headers.get("X-PAYMENT-REQUIRED");
|
|
186
|
+
if (!reqHeader) return { requiresPayment: true };
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const reqs = JSON.parse(Buffer.from(reqHeader, "base64").toString("utf8"));
|
|
190
|
+
const first = Array.isArray(reqs.accepts) ? reqs.accepts[0] : reqs;
|
|
191
|
+
return {
|
|
192
|
+
requiresPayment: true,
|
|
193
|
+
amount: first.maxAmountRequired,
|
|
194
|
+
asset: first.asset,
|
|
195
|
+
network: first.network,
|
|
196
|
+
payTo: first.payTo,
|
|
197
|
+
};
|
|
198
|
+
} catch {
|
|
199
|
+
return { requiresPayment: true };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a payment client
|
|
206
|
+
*/
|
|
207
|
+
export function createPaymentClient(config: PaymentClientConfig): PaymentClient {
|
|
208
|
+
return new PaymentClient(config);
|
|
209
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** USDC on Base mainnet */
|
|
6
|
+
export const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
|
|
7
|
+
|
|
8
|
+
/** USDC on Ethereum mainnet */
|
|
9
|
+
export const USDC_ETH = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;
|
|
10
|
+
|
|
11
|
+
/** USDC on Polygon */
|
|
12
|
+
export const USDC_POLYGON = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" as const;
|
|
13
|
+
|
|
14
|
+
/** USDC on Arbitrum */
|
|
15
|
+
export const USDC_ARBITRUM = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" as const;
|
|
16
|
+
|
|
17
|
+
/** USDC on Optimism */
|
|
18
|
+
export const USDC_OPTIMISM = "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" as const;
|
|
19
|
+
|
|
20
|
+
/** Default USDC token per network */
|
|
21
|
+
export const NETWORK_USDC: Record<string, `0x${string}`> = {
|
|
22
|
+
"eip155:1": USDC_ETH,
|
|
23
|
+
"eip155:8453": USDC_BASE,
|
|
24
|
+
"eip155:137": USDC_POLYGON,
|
|
25
|
+
"eip155:42161": USDC_ARBITRUM,
|
|
26
|
+
"eip155:10": USDC_OPTIMISM,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Default network (Base mainnet) */
|
|
30
|
+
export const DEFAULT_NETWORK = "eip155:8453";
|
|
31
|
+
|
|
32
|
+
/** Default max auto-pay amount: 10 USDC (10,000,000 base units) */
|
|
33
|
+
export const DEFAULT_MAX_AMOUNT = "10000000";
|
|
34
|
+
|
|
35
|
+
/** Default payment timeout: 5 minutes */
|
|
36
|
+
export const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
37
|
+
|
|
38
|
+
/** ERC-20 ABI for balance checks */
|
|
39
|
+
export const ERC20_ABI = [
|
|
40
|
+
{
|
|
41
|
+
name: "balanceOf",
|
|
42
|
+
type: "function",
|
|
43
|
+
stateMutability: "view",
|
|
44
|
+
inputs: [{ name: "account", type: "address" }],
|
|
45
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "decimals",
|
|
49
|
+
type: "function",
|
|
50
|
+
stateMutability: "view",
|
|
51
|
+
inputs: [],
|
|
52
|
+
outputs: [{ name: "", type: "uint8" }],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "symbol",
|
|
56
|
+
type: "function",
|
|
57
|
+
stateMutability: "view",
|
|
58
|
+
inputs: [],
|
|
59
|
+
outputs: [{ name: "", type: "string" }],
|
|
60
|
+
},
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
/** Base mainnet RPC */
|
|
64
|
+
export const BASE_RPC = "https://mainnet.base.org";
|
|
65
|
+
|
|
66
|
+
/** Default RPC per CAIP-2 network */
|
|
67
|
+
export const NETWORK_RPC: Record<string, string> = {
|
|
68
|
+
"eip155:1": "https://eth.llamarpc.com",
|
|
69
|
+
"eip155:8453": BASE_RPC,
|
|
70
|
+
"eip155:137": "https://polygon-rpc.com",
|
|
71
|
+
"eip155:42161": "https://arb1.arbitrum.io/rpc",
|
|
72
|
+
"eip155:10": "https://mainnet.optimism.io",
|
|
73
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @a3stack/payments
|
|
3
|
+
* x402 payment flows for AI agents — client (paying) and server (receiving)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { PaymentClient, createPaymentClient } from "./client.js";
|
|
7
|
+
export { PaymentServer, createPaymentServer } from "./server.js";
|
|
8
|
+
export {
|
|
9
|
+
USDC_BASE,
|
|
10
|
+
USDC_ETH,
|
|
11
|
+
USDC_POLYGON,
|
|
12
|
+
USDC_ARBITRUM,
|
|
13
|
+
USDC_OPTIMISM,
|
|
14
|
+
NETWORK_USDC,
|
|
15
|
+
DEFAULT_NETWORK,
|
|
16
|
+
DEFAULT_MAX_AMOUNT,
|
|
17
|
+
} from "./constants.js";
|
|
18
|
+
export type {
|
|
19
|
+
PaymentClientConfig,
|
|
20
|
+
PaymentServerConfig,
|
|
21
|
+
PaymentDetails,
|
|
22
|
+
PaymentBalance,
|
|
23
|
+
PaymentVerifyResult,
|
|
24
|
+
PaymentRequirements,
|
|
25
|
+
PaymentContext,
|
|
26
|
+
} from "./types.js";
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Server — receive x402 payments in your HTTP server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_NETWORK,
|
|
8
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
9
|
+
NETWORK_USDC,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import type {
|
|
12
|
+
PaymentServerConfig,
|
|
13
|
+
PaymentRequirements,
|
|
14
|
+
PaymentVerifyResult,
|
|
15
|
+
PaymentDetails,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Payment server for accepting x402 payments
|
|
20
|
+
*/
|
|
21
|
+
export class PaymentServer {
|
|
22
|
+
private config: Required<PaymentServerConfig>;
|
|
23
|
+
|
|
24
|
+
constructor(config: PaymentServerConfig) {
|
|
25
|
+
const network = config.network ?? DEFAULT_NETWORK;
|
|
26
|
+
this.config = {
|
|
27
|
+
payTo: config.payTo,
|
|
28
|
+
amount: config.amount,
|
|
29
|
+
asset: config.asset ?? NETWORK_USDC[network] ?? NETWORK_USDC["eip155:8453"],
|
|
30
|
+
network,
|
|
31
|
+
description: config.description ?? "AI agent service payment",
|
|
32
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build x402 PaymentRequirements for a resource URL
|
|
38
|
+
*/
|
|
39
|
+
buildRequirements(
|
|
40
|
+
resource: string,
|
|
41
|
+
overrides?: Partial<PaymentServerConfig>
|
|
42
|
+
): PaymentRequirements {
|
|
43
|
+
return {
|
|
44
|
+
scheme: "exact",
|
|
45
|
+
network: overrides?.network ?? this.config.network,
|
|
46
|
+
maxAmountRequired: overrides?.amount ?? this.config.amount,
|
|
47
|
+
resource,
|
|
48
|
+
description: overrides?.description ?? this.config.description,
|
|
49
|
+
payTo: overrides?.payTo ?? this.config.payTo,
|
|
50
|
+
maxTimeoutSeconds: this.config.maxTimeoutSeconds,
|
|
51
|
+
asset: overrides?.asset ?? this.config.asset,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the X-PAYMENT-REQUIRED header value (base64-encoded JSON)
|
|
57
|
+
*/
|
|
58
|
+
buildRequirementsHeader(resource: string, overrides?: Partial<PaymentServerConfig>): string {
|
|
59
|
+
const requirements = this.buildRequirements(resource, overrides);
|
|
60
|
+
// x402 v2 format: { version: 2, accepts: [...] }
|
|
61
|
+
const payload = {
|
|
62
|
+
version: 2,
|
|
63
|
+
accepts: [requirements],
|
|
64
|
+
};
|
|
65
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse the X-PAYMENT header from an incoming request
|
|
70
|
+
*/
|
|
71
|
+
parsePaymentHeader(header: string | null): {
|
|
72
|
+
payload: unknown;
|
|
73
|
+
network: string;
|
|
74
|
+
} | null {
|
|
75
|
+
if (!header) return null;
|
|
76
|
+
try {
|
|
77
|
+
const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
|
|
78
|
+
return {
|
|
79
|
+
payload: decoded.payload ?? decoded,
|
|
80
|
+
network: decoded.network ?? decoded.x402Version?.network ?? this.config.network,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Verify a payment — this is a lightweight signature check.
|
|
89
|
+
* For production, use a proper facilitator (e.g. x402.org/faciliate).
|
|
90
|
+
*
|
|
91
|
+
* Note: Full on-chain verification requires a funded facilitator wallet
|
|
92
|
+
* to settle the payment. This SDK provides the verification primitive;
|
|
93
|
+
* settlement is handled by the facilitator service.
|
|
94
|
+
*/
|
|
95
|
+
async verify(
|
|
96
|
+
request: Request | IncomingMessage
|
|
97
|
+
): Promise<PaymentVerifyResult> {
|
|
98
|
+
// Get the X-PAYMENT header
|
|
99
|
+
let paymentHeader: string | null = null;
|
|
100
|
+
|
|
101
|
+
if (request instanceof Request) {
|
|
102
|
+
paymentHeader =
|
|
103
|
+
request.headers.get("x-payment") ??
|
|
104
|
+
request.headers.get("X-PAYMENT");
|
|
105
|
+
} else {
|
|
106
|
+
const raw = (request as IncomingMessage).headers["x-payment"];
|
|
107
|
+
paymentHeader = Array.isArray(raw) ? raw[0] : raw ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!paymentHeader) {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
error: "Missing X-PAYMENT header. This endpoint requires x402 payment.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parsed = this.parsePaymentHeader(paymentHeader);
|
|
118
|
+
if (!parsed) {
|
|
119
|
+
return { valid: false, error: "Invalid X-PAYMENT header format" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For full on-chain verification, we'd use ExactEvmFacilitator from @x402/evm
|
|
123
|
+
// Here we do a structural validation (signature format check)
|
|
124
|
+
try {
|
|
125
|
+
const payload = parsed.payload as Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
// Check if it's an EIP-3009 payload
|
|
128
|
+
if (payload.authorization && typeof payload.authorization === "object") {
|
|
129
|
+
const auth = payload.authorization as Record<string, unknown>;
|
|
130
|
+
if (!auth.from || !auth.to || !auth.value || !auth.nonce) {
|
|
131
|
+
return { valid: false, error: "Invalid EIP-3009 authorization structure" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payment: PaymentDetails = {
|
|
135
|
+
from: auth.from as `0x${string}`,
|
|
136
|
+
to: auth.to as `0x${string}`,
|
|
137
|
+
amount: auth.value as string,
|
|
138
|
+
asset: this.config.asset,
|
|
139
|
+
network: parsed.network,
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return { valid: true, payment };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if it's a Permit2 payload
|
|
147
|
+
if (payload.permit2Authorization) {
|
|
148
|
+
const p2 = payload.permit2Authorization as Record<string, unknown>;
|
|
149
|
+
const payment: PaymentDetails = {
|
|
150
|
+
from: p2.from as `0x${string}`,
|
|
151
|
+
to: (p2 as Record<string, Record<string, unknown>>).witness?.to as `0x${string}`,
|
|
152
|
+
amount: (p2.permitted as Record<string, string>)?.amount,
|
|
153
|
+
asset: (p2.permitted as Record<string, string>)?.token as `0x${string}`,
|
|
154
|
+
network: parsed.network,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
};
|
|
157
|
+
return { valid: true, payment };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { valid: false, error: "Unrecognized payment payload format" };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { valid: false, error: `Payment verification error: ${(e as Error).message}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Express-compatible middleware for payment verification.
|
|
168
|
+
* Automatically sends 402 response if payment is missing/invalid.
|
|
169
|
+
*
|
|
170
|
+
* Usage:
|
|
171
|
+
* app.use('/paid-tool', paymentServer.middleware(), handler)
|
|
172
|
+
*/
|
|
173
|
+
middleware(overrides?: Partial<PaymentServerConfig>) {
|
|
174
|
+
return async (
|
|
175
|
+
req: IncomingMessage & { url?: string; payment?: PaymentDetails },
|
|
176
|
+
res: ServerResponse,
|
|
177
|
+
next: () => void
|
|
178
|
+
) => {
|
|
179
|
+
const paymentHeader =
|
|
180
|
+
(req.headers["x-payment"] as string) ??
|
|
181
|
+
(req.headers["X-PAYMENT"] as string);
|
|
182
|
+
|
|
183
|
+
if (!paymentHeader) {
|
|
184
|
+
// Return 402 with payment requirements
|
|
185
|
+
const resource = `${req.headers["host"] ?? ""}${req.url ?? "/"}`;
|
|
186
|
+
const requirementsHeader = this.buildRequirementsHeader(
|
|
187
|
+
`https://${resource}`,
|
|
188
|
+
overrides
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
res.writeHead(402, {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
"X-PAYMENT-REQUIRED": requirementsHeader,
|
|
194
|
+
});
|
|
195
|
+
res.end(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
error: "Payment Required",
|
|
198
|
+
message: this.config.description,
|
|
199
|
+
amount: `${this.config.amount} USDC base units`,
|
|
200
|
+
network: this.config.network,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await this.verify(req);
|
|
207
|
+
if (!result.valid) {
|
|
208
|
+
res.writeHead(402, { "Content-Type": "application/json" });
|
|
209
|
+
res.end(JSON.stringify({ error: "Payment Invalid", message: result.error }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Attach payment details to request for downstream handlers
|
|
214
|
+
(req as typeof req & { payment: PaymentDetails }).payment = result.payment!;
|
|
215
|
+
next();
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create a payment server for receiving x402 payments
|
|
222
|
+
*/
|
|
223
|
+
export function createPaymentServer(config: PaymentServerConfig): PaymentServer {
|
|
224
|
+
return new PaymentServer(config);
|
|
225
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @a3stack/payments — Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PaymentClientConfig {
|
|
6
|
+
/**
|
|
7
|
+
* viem Account (e.g. from privateKeyToAccount())
|
|
8
|
+
* Must have signTypedData for payment signing.
|
|
9
|
+
*/
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
account: any;
|
|
12
|
+
/**
|
|
13
|
+
* Supported chains in CAIP-2 format.
|
|
14
|
+
* Defaults to ["eip155:*"] (all EVM chains)
|
|
15
|
+
*/
|
|
16
|
+
chains?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Maximum USDC amount (in base units, 6 decimals) to auto-pay per request.
|
|
19
|
+
* e.g. "1000000" = 1.00 USDC
|
|
20
|
+
* Defaults to "10000000" (10 USDC)
|
|
21
|
+
*/
|
|
22
|
+
maxAmountPerRequest?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PaymentServerConfig {
|
|
26
|
+
/** Address to receive payments */
|
|
27
|
+
payTo: `0x${string}`;
|
|
28
|
+
/**
|
|
29
|
+
* Required payment amount in USDC base units (6 decimals).
|
|
30
|
+
* e.g. "100000" = 0.10 USDC
|
|
31
|
+
*/
|
|
32
|
+
amount: string;
|
|
33
|
+
/**
|
|
34
|
+
* Token asset address. Defaults to USDC on Base mainnet.
|
|
35
|
+
*/
|
|
36
|
+
asset?: `0x${string}`;
|
|
37
|
+
/**
|
|
38
|
+
* Network in CAIP-2 format. Defaults to "eip155:8453" (Base mainnet)
|
|
39
|
+
*/
|
|
40
|
+
network?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Description shown in payment requirements
|
|
43
|
+
*/
|
|
44
|
+
description?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Max timeout for payment verification in seconds. Defaults to 300.
|
|
47
|
+
*/
|
|
48
|
+
maxTimeoutSeconds?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PaymentDetails {
|
|
52
|
+
from: `0x${string}`;
|
|
53
|
+
to: `0x${string}`;
|
|
54
|
+
amount: string;
|
|
55
|
+
asset: `0x${string}`;
|
|
56
|
+
network: string;
|
|
57
|
+
txHash?: `0x${string}`;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PaymentVerifyResult {
|
|
62
|
+
valid: boolean;
|
|
63
|
+
payment?: PaymentDetails;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PaymentBalance {
|
|
68
|
+
amount: bigint;
|
|
69
|
+
formatted: string;
|
|
70
|
+
symbol: string;
|
|
71
|
+
decimals: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PaymentRequirements {
|
|
75
|
+
scheme: string;
|
|
76
|
+
network: string;
|
|
77
|
+
maxAmountRequired: string;
|
|
78
|
+
resource: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
mimeType?: string;
|
|
81
|
+
payTo: string;
|
|
82
|
+
maxTimeoutSeconds: number;
|
|
83
|
+
asset: string;
|
|
84
|
+
outputSchema?: unknown;
|
|
85
|
+
extra?: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Middleware context — added to express req/res
|
|
90
|
+
*/
|
|
91
|
+
export interface PaymentContext {
|
|
92
|
+
payment: PaymentDetails;
|
|
93
|
+
requirements: PaymentRequirements;
|
|
94
|
+
}
|