@agether/sdk 1.0.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 +480 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2149 -0
- package/dist/cli.mjs +0 -0
- package/dist/clients/AgentIdentityClient.d.ts +163 -0
- package/dist/clients/AgentIdentityClient.d.ts.map +1 -0
- package/dist/clients/AgentIdentityClient.js +293 -0
- package/dist/clients/AgetherClient.d.ts +101 -0
- package/dist/clients/AgetherClient.d.ts.map +1 -0
- package/dist/clients/AgetherClient.js +272 -0
- package/dist/clients/ScoringClient.d.ts +138 -0
- package/dist/clients/ScoringClient.d.ts.map +1 -0
- package/dist/clients/ScoringClient.js +135 -0
- package/dist/clients/VaultClient.d.ts +62 -0
- package/dist/clients/VaultClient.d.ts.map +1 -0
- package/dist/clients/VaultClient.js +157 -0
- package/dist/clients/WalletClient.d.ts +73 -0
- package/dist/clients/WalletClient.d.ts.map +1 -0
- package/dist/clients/WalletClient.js +174 -0
- package/dist/clients/X402Client.d.ts +61 -0
- package/dist/clients/X402Client.d.ts.map +1 -0
- package/dist/clients/X402Client.js +303 -0
- package/dist/index.d.mts +932 -0
- package/dist/index.d.ts +932 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1680 -0
- package/dist/index.mjs +1610 -0
- package/dist/types/index.d.ts +220 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +52 -0
- package/dist/utils/abis.d.ts +21 -0
- package/dist/utils/abis.d.ts.map +1 -0
- package/dist/utils/abis.js +134 -0
- package/dist/utils/config.d.ts +31 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +117 -0
- package/dist/utils/format.d.ts +44 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/package.json +57 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 HTTP Client — Make paid API calls via the x402 protocol (v2)
|
|
3
|
+
*
|
|
4
|
+
* Implements the Coinbase x402 spec:
|
|
5
|
+
* https://github.com/coinbase/x402
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Client → Resource Server (normal request)
|
|
9
|
+
* 2. Resource Server → 402 with PaymentRequired JSON body
|
|
10
|
+
* Body: { x402Version, error, resource, accepts: [PaymentRequirements…] }
|
|
11
|
+
* 3. Client picks a PaymentRequirements from `accepts`,
|
|
12
|
+
* signs an EIP-3009 transferWithAuthorization (EIP-712 typed data),
|
|
13
|
+
* builds a PaymentPayload, base64-encodes it as PAYMENT-SIGNATURE header
|
|
14
|
+
* 4. Client → Resource Server (retries with PAYMENT-SIGNATURE)
|
|
15
|
+
* 5. Resource Server forwards to Facilitator /verify → /settle
|
|
16
|
+
* 6. Resource Server → 200 + data (or error)
|
|
17
|
+
*
|
|
18
|
+
* Chain support: Base (8453), Ethereum (1), and Hardhat fork (31337).
|
|
19
|
+
* USDC domain is resolved per-chain from USDC_DOMAINS map.
|
|
20
|
+
*/
|
|
21
|
+
import { ethers } from 'ethers';
|
|
22
|
+
// ──────────────────── Well-known USDC metadata ────────────────────
|
|
23
|
+
// EIP-3009 domain per chain (name / version / verifyingContract)
|
|
24
|
+
const USDC_DOMAINS = {
|
|
25
|
+
'eip155:1': { name: 'USD Coin', version: '2', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
|
|
26
|
+
'eip155:8453': { name: 'USD Coin', version: '2', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
|
|
27
|
+
'eip155:84532': { name: 'USD Coin', version: '2', address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' },
|
|
28
|
+
'eip155:42161': { name: 'USD Coin', version: '2', address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' },
|
|
29
|
+
'eip155:10': { name: 'USD Coin', version: '2', address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85' },
|
|
30
|
+
};
|
|
31
|
+
function chainIdFromNetwork(network) {
|
|
32
|
+
const m = network.match(/^eip155:(\d+)$/);
|
|
33
|
+
return m ? Number(m[1]) : 1;
|
|
34
|
+
}
|
|
35
|
+
// ──────────────────── Client ────────────────────
|
|
36
|
+
export class X402Client {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
const provider = new ethers.JsonRpcProvider(config.rpcUrl);
|
|
40
|
+
this.wallet = new ethers.Wallet(config.privateKey, provider);
|
|
41
|
+
}
|
|
42
|
+
async get(url, opts) {
|
|
43
|
+
return this.request(url, { ...opts, method: 'GET' });
|
|
44
|
+
}
|
|
45
|
+
async post(url, body, opts) {
|
|
46
|
+
return this.request(url, {
|
|
47
|
+
...opts,
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
50
|
+
headers: { 'Content-Type': 'application/json', ...opts?.headers },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
getAddress() {
|
|
54
|
+
return this.wallet.address;
|
|
55
|
+
}
|
|
56
|
+
// ──────────── Core request / 402-retry loop ────────────
|
|
57
|
+
async request(url, options) {
|
|
58
|
+
try {
|
|
59
|
+
// 1️⃣ First attempt — may get 402
|
|
60
|
+
console.log(' [1/4] Calling resource server…');
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
...options,
|
|
63
|
+
headers: {
|
|
64
|
+
...options?.headers,
|
|
65
|
+
'X-Agent-Id': this.config.agentId || '',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
return { success: true, data };
|
|
71
|
+
}
|
|
72
|
+
if (response.status !== 402) {
|
|
73
|
+
return { success: false, error: `HTTP ${response.status}: ${await response.text()}` };
|
|
74
|
+
}
|
|
75
|
+
// 2️⃣ Parse PaymentRequired
|
|
76
|
+
console.log(' [2/4] 402 received — parsing payment requirements…');
|
|
77
|
+
const parsed = await this.parsePaymentRequired(response);
|
|
78
|
+
if (!parsed) {
|
|
79
|
+
return { success: false, error: 'Could not parse payment requirements from 402 response' };
|
|
80
|
+
}
|
|
81
|
+
const { requirements, resource } = parsed;
|
|
82
|
+
console.log(` scheme : ${requirements.scheme}`);
|
|
83
|
+
console.log(` network : ${requirements.network}`);
|
|
84
|
+
console.log(` amount : ${requirements.amount} (atomic)`);
|
|
85
|
+
console.log(` asset : ${requirements.asset}`);
|
|
86
|
+
console.log(` payTo : ${requirements.payTo}`);
|
|
87
|
+
// 3️⃣ Sign EIP-3009 authorization & build PaymentPayload
|
|
88
|
+
console.log(' [3/4] Signing EIP-3009 transferWithAuthorization…');
|
|
89
|
+
const paymentPayload = await this.buildPaymentPayload(requirements, resource, url);
|
|
90
|
+
const paymentB64 = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
|
|
91
|
+
// Optional: risk-check via our backend
|
|
92
|
+
await this.riskCheck(paymentPayload, requirements);
|
|
93
|
+
// 4️⃣ Retry with PAYMENT-SIGNATURE header
|
|
94
|
+
console.log(' [4/4] Retrying with PAYMENT-SIGNATURE header…');
|
|
95
|
+
const paidResponse = await fetch(url, {
|
|
96
|
+
...options,
|
|
97
|
+
headers: {
|
|
98
|
+
...options?.headers,
|
|
99
|
+
'X-Agent-Id': this.config.agentId || '',
|
|
100
|
+
// v2 header
|
|
101
|
+
'PAYMENT-SIGNATURE': paymentB64,
|
|
102
|
+
// v1 compat header (some servers still use this)
|
|
103
|
+
'X-PAYMENT': paymentB64,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (paidResponse.ok) {
|
|
107
|
+
const data = await paidResponse.json();
|
|
108
|
+
// Check for settlement response
|
|
109
|
+
const settlementHeader = paidResponse.headers.get('PAYMENT-RESPONSE')
|
|
110
|
+
|| paidResponse.headers.get('X-PAYMENT-RESPONSE');
|
|
111
|
+
let txHash;
|
|
112
|
+
if (settlementHeader) {
|
|
113
|
+
try {
|
|
114
|
+
const settlement = JSON.parse(Buffer.from(settlementHeader, 'base64').toString('utf-8'));
|
|
115
|
+
txHash = settlement.transaction;
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
data,
|
|
122
|
+
paymentInfo: {
|
|
123
|
+
amount: requirements.amount,
|
|
124
|
+
asset: requirements.extra?.name || 'USDC',
|
|
125
|
+
network: requirements.network,
|
|
126
|
+
txHash,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const errBody = await paidResponse.text();
|
|
131
|
+
return { success: false, error: `Payment rejected (HTTP ${paidResponse.status}): ${errBody}` };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return { success: false, error: `Request failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ──────────── Parse 402 response ────────────
|
|
138
|
+
async parsePaymentRequired(response) {
|
|
139
|
+
// Strategy 1: PAYMENT-REQUIRED header (base64 JSON — some servers)
|
|
140
|
+
const prHeader = response.headers.get('PAYMENT-REQUIRED') || response.headers.get('x-payment-required');
|
|
141
|
+
if (prHeader) {
|
|
142
|
+
try {
|
|
143
|
+
const decoded = JSON.parse(Buffer.from(prHeader, 'base64').toString('utf-8'));
|
|
144
|
+
if (decoded.accepts?.length) {
|
|
145
|
+
return { requirements: decoded.accepts[0], resource: decoded.resource };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { /* fall through */ }
|
|
149
|
+
}
|
|
150
|
+
// Strategy 2: Response body (primary method per v2 spec)
|
|
151
|
+
try {
|
|
152
|
+
const body = await response.json();
|
|
153
|
+
// v2 format: { accepts: [...] }
|
|
154
|
+
if (body.accepts && Array.isArray(body.accepts) && body.accepts.length > 0) {
|
|
155
|
+
return { requirements: body.accepts[0], resource: body.resource };
|
|
156
|
+
}
|
|
157
|
+
// v1 compat: { paymentRequirements: {...} } or { paymentRequirements: [...] }
|
|
158
|
+
if (body.paymentRequirements) {
|
|
159
|
+
const pr = Array.isArray(body.paymentRequirements) ? body.paymentRequirements[0] : body.paymentRequirements;
|
|
160
|
+
return { requirements: pr, resource: body.resource };
|
|
161
|
+
}
|
|
162
|
+
// Ultra-legacy: flat fields
|
|
163
|
+
if (body.scheme && body.network) {
|
|
164
|
+
return { requirements: body, resource: body.resource };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* fall through */ }
|
|
168
|
+
// Strategy 3: WWW-Authenticate header (old implementations)
|
|
169
|
+
const wwwAuth = response.headers.get('WWW-Authenticate');
|
|
170
|
+
if (wwwAuth) {
|
|
171
|
+
const m = wwwAuth.match(/x402[^"]*"([^"]+)"/);
|
|
172
|
+
if (m) {
|
|
173
|
+
try {
|
|
174
|
+
const decoded = JSON.parse(Buffer.from(m[1], 'base64').toString('utf-8'));
|
|
175
|
+
const reqs = Array.isArray(decoded) ? decoded[0] : decoded;
|
|
176
|
+
return { requirements: reqs };
|
|
177
|
+
}
|
|
178
|
+
catch { /* fall through */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// ──────────── Build x402 v2 PaymentPayload with EIP-3009 ────────────
|
|
184
|
+
//
|
|
185
|
+
// If an AgentAccount is configured, we use it as the `from` address
|
|
186
|
+
// (smart wallet pays directly). The AgentAccount implements EIP-1271
|
|
187
|
+
// so USDC's transferWithAuthorization will call isValidSignature()
|
|
188
|
+
// to verify the owner's ECDSA signature. The facilitator detects
|
|
189
|
+
// the >65-byte or smart-wallet case and uses the bytes overload.
|
|
190
|
+
async buildPaymentPayload(reqs, resource, url) {
|
|
191
|
+
const now = Math.floor(Date.now() / 1000);
|
|
192
|
+
const validAfter = String(now - 60);
|
|
193
|
+
const validBefore = String(now + (reqs.maxTimeoutSeconds || 300));
|
|
194
|
+
const nonce = ethers.hexlify(ethers.randomBytes(32));
|
|
195
|
+
const chainId = chainIdFromNetwork(reqs.network);
|
|
196
|
+
const usdcDomain = USDC_DOMAINS[reqs.network] || USDC_DOMAINS['eip155:1'];
|
|
197
|
+
// Determine payer: AgentAccount (smart wallet) if available, else EOA
|
|
198
|
+
const payerAddress = this.config.accountAddress || this.wallet.address;
|
|
199
|
+
const isSmartWallet = !!this.config.accountAddress;
|
|
200
|
+
// EIP-712 domain for USDC's transferWithAuthorization (EIP-3009)
|
|
201
|
+
const domain = {
|
|
202
|
+
name: usdcDomain.name,
|
|
203
|
+
version: usdcDomain.version,
|
|
204
|
+
chainId,
|
|
205
|
+
verifyingContract: reqs.asset || usdcDomain.address,
|
|
206
|
+
};
|
|
207
|
+
const types = {
|
|
208
|
+
TransferWithAuthorization: [
|
|
209
|
+
{ name: 'from', type: 'address' },
|
|
210
|
+
{ name: 'to', type: 'address' },
|
|
211
|
+
{ name: 'value', type: 'uint256' },
|
|
212
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
213
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
214
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
const value = {
|
|
218
|
+
from: payerAddress, // AgentAccount or EOA
|
|
219
|
+
to: reqs.payTo,
|
|
220
|
+
value: reqs.amount,
|
|
221
|
+
validAfter,
|
|
222
|
+
validBefore,
|
|
223
|
+
nonce,
|
|
224
|
+
};
|
|
225
|
+
// EOA signs the typed data — for smart wallets, EIP-1271 validation
|
|
226
|
+
// will verify this signature against the AgentAccount's owner()
|
|
227
|
+
let signature = await this.wallet.signTypedData(domain, types, value);
|
|
228
|
+
// For smart wallets: pad the signature to >65 bytes (>130 hex chars).
|
|
229
|
+
// The Coinbase x402 facilitator uses signature length to choose the
|
|
230
|
+
// settlement path:
|
|
231
|
+
// - 65 bytes (130 hex) → v,r,s overload (EOA) — ecrecover
|
|
232
|
+
// - >65 bytes (>130 hex) → bytes overload (smart wallet) — EIP-1271
|
|
233
|
+
// Without padding, the facilitator would use the EOA path and
|
|
234
|
+
// ecrecover would return the EOA address, not AgentAccount → fail.
|
|
235
|
+
// With padding, USDC calls AgentAccount.isValidSignature() which
|
|
236
|
+
// extracts r,s,v from the first 65 bytes and verifies the owner.
|
|
237
|
+
if (isSmartWallet) {
|
|
238
|
+
signature = (signature + '00');
|
|
239
|
+
}
|
|
240
|
+
if (isSmartWallet) {
|
|
241
|
+
console.log(` ✓ Signed for AgentAccount ${payerAddress.slice(0, 10)}… (EIP-1271, chain=${chainId})`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(` ✓ Signed (from=${payerAddress.slice(0, 10)}…, chain=${chainId})`);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
x402Version: 2,
|
|
248
|
+
resource: resource || { url, description: '', mimeType: 'application/json' },
|
|
249
|
+
accepted: {
|
|
250
|
+
scheme: reqs.scheme,
|
|
251
|
+
network: reqs.network,
|
|
252
|
+
amount: reqs.amount,
|
|
253
|
+
asset: reqs.asset,
|
|
254
|
+
payTo: reqs.payTo,
|
|
255
|
+
maxTimeoutSeconds: reqs.maxTimeoutSeconds,
|
|
256
|
+
extra: reqs.extra || {},
|
|
257
|
+
},
|
|
258
|
+
payload: {
|
|
259
|
+
signature,
|
|
260
|
+
authorization: {
|
|
261
|
+
from: payerAddress, // AgentAccount address — facilitator checks balance here
|
|
262
|
+
to: reqs.payTo,
|
|
263
|
+
value: reqs.amount,
|
|
264
|
+
validAfter,
|
|
265
|
+
validBefore,
|
|
266
|
+
nonce,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// ──────────── Risk check via our backend ────────────
|
|
272
|
+
async riskCheck(paymentPayload, reqs) {
|
|
273
|
+
try {
|
|
274
|
+
const verifyUrl = `${this.config.backendUrl}/x402/verify`;
|
|
275
|
+
const resp = await fetch(verifyUrl, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
'X-Agent-Id': this.config.agentId || '',
|
|
280
|
+
...(this.config.accountAddress ? { 'X-Agent-Account': this.config.accountAddress } : {}),
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
x402Version: 2,
|
|
284
|
+
paymentPayload,
|
|
285
|
+
paymentRequirements: reqs,
|
|
286
|
+
}),
|
|
287
|
+
signal: AbortSignal.timeout(5000),
|
|
288
|
+
});
|
|
289
|
+
if (resp.ok) {
|
|
290
|
+
const result = await resp.json();
|
|
291
|
+
const decision = resp.headers.get('X-Risk-Decision') || (result.isValid ? 'allow' : 'unknown');
|
|
292
|
+
const score = resp.headers.get('X-Risk-Score') || '?';
|
|
293
|
+
console.log(` ✓ Risk check: ${decision} (score=${score})`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
console.log(` ⚠ Risk check failed (HTTP ${resp.status}) — continuing anyway`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
console.log(' ⚠ Risk check unavailable — continuing');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|