@mixrpay/agent-sdk 0.4.1
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 +83 -0
- package/dist/index.cjs +2020 -0
- package/dist/index.d.cts +1431 -0
- package/dist/index.d.ts +1431 -0
- package/dist/index.js +1985 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2020 @@
|
|
|
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/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AgentWallet: () => AgentWallet,
|
|
24
|
+
InsufficientBalanceError: () => InsufficientBalanceError,
|
|
25
|
+
InvalidSessionKeyError: () => InvalidSessionKeyError,
|
|
26
|
+
MixrPayError: () => MixrPayError,
|
|
27
|
+
PaymentFailedError: () => PaymentFailedError,
|
|
28
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
29
|
+
SessionExpiredError: () => SessionExpiredError,
|
|
30
|
+
SessionKeyExpiredError: () => SessionKeyExpiredError,
|
|
31
|
+
SessionLimitExceededError: () => SessionLimitExceededError,
|
|
32
|
+
SessionNotFoundError: () => SessionNotFoundError,
|
|
33
|
+
SessionRevokedError: () => SessionRevokedError,
|
|
34
|
+
SpendingLimitExceededError: () => SpendingLimitExceededError,
|
|
35
|
+
isMixrPayError: () => isMixrPayError
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/session-key.ts
|
|
40
|
+
var import_accounts = require("viem/accounts");
|
|
41
|
+
|
|
42
|
+
// src/errors.ts
|
|
43
|
+
var MixrPayError = class extends Error {
|
|
44
|
+
/** Error code for programmatic handling */
|
|
45
|
+
code;
|
|
46
|
+
constructor(message, code = "MIXRPAY_ERROR") {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "MixrPayError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
if (Error.captureStackTrace) {
|
|
51
|
+
Error.captureStackTrace(this, this.constructor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var InsufficientBalanceError = class extends MixrPayError {
|
|
56
|
+
/** Amount required for the payment in USD */
|
|
57
|
+
required;
|
|
58
|
+
/** Current available balance in USD */
|
|
59
|
+
available;
|
|
60
|
+
/** URL where the user can top up their wallet */
|
|
61
|
+
topUpUrl;
|
|
62
|
+
constructor(required, available) {
|
|
63
|
+
const shortage = required - available;
|
|
64
|
+
super(
|
|
65
|
+
`Insufficient balance: need $${required.toFixed(2)}, have $${available.toFixed(2)} (short $${shortage.toFixed(2)}). Top up your wallet to continue.`,
|
|
66
|
+
"INSUFFICIENT_BALANCE"
|
|
67
|
+
);
|
|
68
|
+
this.name = "InsufficientBalanceError";
|
|
69
|
+
this.required = required;
|
|
70
|
+
this.available = available;
|
|
71
|
+
this.topUpUrl = "/wallet";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var SessionKeyExpiredError = class extends MixrPayError {
|
|
75
|
+
/** When the session key expired */
|
|
76
|
+
expiredAt;
|
|
77
|
+
constructor(expiredAt) {
|
|
78
|
+
super(
|
|
79
|
+
`Session key expired at ${expiredAt}. Request a new session key from the wallet owner or create one at your MixrPay server /wallet/sessions`,
|
|
80
|
+
"SESSION_KEY_EXPIRED"
|
|
81
|
+
);
|
|
82
|
+
this.name = "SessionKeyExpiredError";
|
|
83
|
+
this.expiredAt = expiredAt;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var SpendingLimitExceededError = class extends MixrPayError {
|
|
87
|
+
/** Type of limit that was exceeded */
|
|
88
|
+
limitType;
|
|
89
|
+
/** The limit amount in USD */
|
|
90
|
+
limit;
|
|
91
|
+
/** The amount that was attempted in USD */
|
|
92
|
+
attempted;
|
|
93
|
+
constructor(limitType, limit, attempted) {
|
|
94
|
+
const limitNames = {
|
|
95
|
+
per_tx: "Per-transaction",
|
|
96
|
+
daily: "Daily",
|
|
97
|
+
total: "Total",
|
|
98
|
+
client_max: "Client-side"
|
|
99
|
+
};
|
|
100
|
+
const limitName = limitNames[limitType] || limitType;
|
|
101
|
+
const suggestion = limitType === "daily" ? "Wait until tomorrow or request a higher limit." : limitType === "client_max" ? "Increase maxPaymentUsd in your AgentWallet configuration." : "Request a new session key with a higher limit.";
|
|
102
|
+
super(
|
|
103
|
+
`${limitName} spending limit exceeded: limit is $${limit.toFixed(2)}, attempted $${attempted.toFixed(2)}. ${suggestion}`,
|
|
104
|
+
"SPENDING_LIMIT_EXCEEDED"
|
|
105
|
+
);
|
|
106
|
+
this.name = "SpendingLimitExceededError";
|
|
107
|
+
this.limitType = limitType;
|
|
108
|
+
this.limit = limit;
|
|
109
|
+
this.attempted = attempted;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var PaymentFailedError = class extends MixrPayError {
|
|
113
|
+
/** Detailed reason for the failure */
|
|
114
|
+
reason;
|
|
115
|
+
/** Transaction hash if the tx was submitted (for debugging) */
|
|
116
|
+
txHash;
|
|
117
|
+
constructor(reason, txHash) {
|
|
118
|
+
let message = `Payment failed: ${reason}`;
|
|
119
|
+
if (txHash) {
|
|
120
|
+
message += ` (tx: ${txHash} - check on basescan.org)`;
|
|
121
|
+
}
|
|
122
|
+
super(message, "PAYMENT_FAILED");
|
|
123
|
+
this.name = "PaymentFailedError";
|
|
124
|
+
this.reason = reason;
|
|
125
|
+
this.txHash = txHash;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var InvalidSessionKeyError = class extends MixrPayError {
|
|
129
|
+
/** Detailed reason why the key is invalid */
|
|
130
|
+
reason;
|
|
131
|
+
constructor(reason = "Invalid session key format") {
|
|
132
|
+
super(
|
|
133
|
+
`${reason}. Session keys should be in format: sk_live_<64 hex chars> or sk_test_<64 hex chars>. Get one from your MixrPay server /wallet/sessions`,
|
|
134
|
+
"INVALID_SESSION_KEY"
|
|
135
|
+
);
|
|
136
|
+
this.name = "InvalidSessionKeyError";
|
|
137
|
+
this.reason = reason;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var X402ProtocolError = class extends MixrPayError {
|
|
141
|
+
/** Detailed reason for the protocol error */
|
|
142
|
+
reason;
|
|
143
|
+
constructor(reason) {
|
|
144
|
+
super(
|
|
145
|
+
`x402 protocol error: ${reason}. This may indicate a server configuration issue. If the problem persists, contact the API provider.`,
|
|
146
|
+
"X402_PROTOCOL_ERROR"
|
|
147
|
+
);
|
|
148
|
+
this.name = "X402ProtocolError";
|
|
149
|
+
this.reason = reason;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var SessionExpiredError = class extends MixrPayError {
|
|
153
|
+
/** ID of the expired session */
|
|
154
|
+
sessionId;
|
|
155
|
+
/** When the session expired (ISO string or Date) */
|
|
156
|
+
expiredAt;
|
|
157
|
+
constructor(sessionId, expiredAt) {
|
|
158
|
+
super(
|
|
159
|
+
`Session ${sessionId} has expired${expiredAt ? ` at ${expiredAt}` : ""}. A new session will be created automatically on your next request.`,
|
|
160
|
+
"SESSION_EXPIRED"
|
|
161
|
+
);
|
|
162
|
+
this.name = "SessionExpiredError";
|
|
163
|
+
this.sessionId = sessionId;
|
|
164
|
+
this.expiredAt = expiredAt;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var SessionLimitExceededError = class extends MixrPayError {
|
|
168
|
+
/** ID of the session */
|
|
169
|
+
sessionId;
|
|
170
|
+
/** The session's spending limit in USD */
|
|
171
|
+
limit;
|
|
172
|
+
/** The amount requested in USD */
|
|
173
|
+
requested;
|
|
174
|
+
/** Remaining balance in the session (limit - used) */
|
|
175
|
+
remaining;
|
|
176
|
+
constructor(limit, requested, remaining, sessionId) {
|
|
177
|
+
super(
|
|
178
|
+
`Session spending limit exceeded: limit is $${limit.toFixed(2)}, requested $${requested.toFixed(2)}, remaining $${remaining.toFixed(2)}. Create a new session with a higher limit to continue.`,
|
|
179
|
+
"SESSION_LIMIT_EXCEEDED"
|
|
180
|
+
);
|
|
181
|
+
this.name = "SessionLimitExceededError";
|
|
182
|
+
this.sessionId = sessionId;
|
|
183
|
+
this.limit = limit;
|
|
184
|
+
this.requested = requested;
|
|
185
|
+
this.remaining = remaining;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var SessionNotFoundError = class extends MixrPayError {
|
|
189
|
+
/** The session ID that was not found */
|
|
190
|
+
sessionId;
|
|
191
|
+
constructor(sessionId) {
|
|
192
|
+
super(
|
|
193
|
+
`Session ${sessionId} not found. It may have been deleted or never existed. Create a new session with getOrCreateSession().`,
|
|
194
|
+
"SESSION_NOT_FOUND"
|
|
195
|
+
);
|
|
196
|
+
this.name = "SessionNotFoundError";
|
|
197
|
+
this.sessionId = sessionId;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var SessionRevokedError = class extends MixrPayError {
|
|
201
|
+
/** The session ID that was revoked */
|
|
202
|
+
sessionId;
|
|
203
|
+
/** Optional reason for revocation */
|
|
204
|
+
reason;
|
|
205
|
+
constructor(sessionId, reason) {
|
|
206
|
+
super(
|
|
207
|
+
`Session ${sessionId} has been revoked${reason ? `: ${reason}` : ""}. Create a new session with getOrCreateSession().`,
|
|
208
|
+
"SESSION_REVOKED"
|
|
209
|
+
);
|
|
210
|
+
this.name = "SessionRevokedError";
|
|
211
|
+
this.sessionId = sessionId;
|
|
212
|
+
this.reason = reason;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function isMixrPayError(error) {
|
|
216
|
+
return error instanceof MixrPayError;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/session-key.ts
|
|
220
|
+
var USDC_ADDRESSES = {
|
|
221
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
222
|
+
// Base Mainnet
|
|
223
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
224
|
+
// Base Sepolia
|
|
225
|
+
};
|
|
226
|
+
var USDC_EIP712_DOMAIN_BASE = {
|
|
227
|
+
name: "USD Coin",
|
|
228
|
+
version: "2"
|
|
229
|
+
};
|
|
230
|
+
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
231
|
+
TransferWithAuthorization: [
|
|
232
|
+
{ name: "from", type: "address" },
|
|
233
|
+
{ name: "to", type: "address" },
|
|
234
|
+
{ name: "value", type: "uint256" },
|
|
235
|
+
{ name: "validAfter", type: "uint256" },
|
|
236
|
+
{ name: "validBefore", type: "uint256" },
|
|
237
|
+
{ name: "nonce", type: "bytes32" }
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
var SessionKey = class _SessionKey {
|
|
241
|
+
privateKey;
|
|
242
|
+
account;
|
|
243
|
+
address;
|
|
244
|
+
isTest;
|
|
245
|
+
constructor(privateKey, isTest) {
|
|
246
|
+
this.privateKey = privateKey;
|
|
247
|
+
this.account = (0, import_accounts.privateKeyToAccount)(privateKey);
|
|
248
|
+
this.address = this.account.address;
|
|
249
|
+
this.isTest = isTest;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the private key as hex string (without 0x prefix).
|
|
253
|
+
* Used for API authentication headers.
|
|
254
|
+
*/
|
|
255
|
+
get privateKeyHex() {
|
|
256
|
+
return this.privateKey.slice(2);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Parse a session key string into a SessionKey object.
|
|
260
|
+
*
|
|
261
|
+
* @param sessionKey - Session key in format sk_live_... or sk_test_...
|
|
262
|
+
* @returns SessionKey object
|
|
263
|
+
* @throws InvalidSessionKeyError if the format is invalid
|
|
264
|
+
*/
|
|
265
|
+
static fromString(sessionKey) {
|
|
266
|
+
const pattern = /^sk_(live|test)_([a-fA-F0-9]{64})$/;
|
|
267
|
+
const match = sessionKey.match(pattern);
|
|
268
|
+
if (!match) {
|
|
269
|
+
throw new InvalidSessionKeyError(
|
|
270
|
+
"Session key must be in format sk_live_{64_hex} or sk_test_{64_hex}"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const env = match[1];
|
|
274
|
+
const hexKey = match[2];
|
|
275
|
+
try {
|
|
276
|
+
const privateKey = `0x${hexKey}`;
|
|
277
|
+
return new _SessionKey(privateKey, env === "test");
|
|
278
|
+
} catch (e) {
|
|
279
|
+
throw new InvalidSessionKeyError(`Failed to decode session key: ${e}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Sign EIP-712 typed data for TransferWithAuthorization.
|
|
284
|
+
*
|
|
285
|
+
* @param domain - EIP-712 domain
|
|
286
|
+
* @param message - Transfer authorization message
|
|
287
|
+
* @returns Hex-encoded signature
|
|
288
|
+
*/
|
|
289
|
+
async signTransferAuthorization(domain, message) {
|
|
290
|
+
return (0, import_accounts.signTypedData)({
|
|
291
|
+
privateKey: this.privateKey,
|
|
292
|
+
domain,
|
|
293
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
294
|
+
primaryType: "TransferWithAuthorization",
|
|
295
|
+
message
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Sign a plain message (EIP-191 personal sign).
|
|
300
|
+
*
|
|
301
|
+
* @param message - Message to sign
|
|
302
|
+
* @returns Hex-encoded signature
|
|
303
|
+
*/
|
|
304
|
+
async signMessage(message) {
|
|
305
|
+
return (0, import_accounts.signMessage)({
|
|
306
|
+
privateKey: this.privateKey,
|
|
307
|
+
message
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get the chain ID based on whether this is a test key.
|
|
312
|
+
*/
|
|
313
|
+
getDefaultChainId() {
|
|
314
|
+
return this.isTest ? 84532 : 8453;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function buildTransferAuthorizationData(params) {
|
|
318
|
+
const usdcAddress = USDC_ADDRESSES[params.chainId];
|
|
319
|
+
if (!usdcAddress) {
|
|
320
|
+
throw new Error(`USDC not supported on chain ${params.chainId}`);
|
|
321
|
+
}
|
|
322
|
+
const domain = {
|
|
323
|
+
...USDC_EIP712_DOMAIN_BASE,
|
|
324
|
+
chainId: params.chainId,
|
|
325
|
+
verifyingContract: usdcAddress
|
|
326
|
+
};
|
|
327
|
+
const message = {
|
|
328
|
+
from: params.fromAddress,
|
|
329
|
+
to: params.toAddress,
|
|
330
|
+
value: params.value,
|
|
331
|
+
validAfter: params.validAfter,
|
|
332
|
+
validBefore: params.validBefore,
|
|
333
|
+
nonce: params.nonce
|
|
334
|
+
};
|
|
335
|
+
return { domain, message };
|
|
336
|
+
}
|
|
337
|
+
function generateNonce() {
|
|
338
|
+
const bytes = new Uint8Array(32);
|
|
339
|
+
crypto.getRandomValues(bytes);
|
|
340
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
341
|
+
}
|
|
342
|
+
function buildSessionAuthMessage(timestamp, address) {
|
|
343
|
+
return `MixrPay:${timestamp}:${address.toLowerCase()}`;
|
|
344
|
+
}
|
|
345
|
+
async function createSessionAuthPayload(sessionKey) {
|
|
346
|
+
const timestamp = Date.now();
|
|
347
|
+
const message = buildSessionAuthMessage(timestamp, sessionKey.address);
|
|
348
|
+
const signature = await sessionKey.signMessage(message);
|
|
349
|
+
return {
|
|
350
|
+
address: sessionKey.address,
|
|
351
|
+
timestamp,
|
|
352
|
+
signature
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/x402.ts
|
|
357
|
+
async function parse402Response(response) {
|
|
358
|
+
let paymentData = null;
|
|
359
|
+
const paymentHeader = response.headers.get("X-Payment-Required");
|
|
360
|
+
if (paymentHeader) {
|
|
361
|
+
try {
|
|
362
|
+
paymentData = JSON.parse(paymentHeader);
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!paymentData) {
|
|
367
|
+
const authHeader = response.headers.get("WWW-Authenticate");
|
|
368
|
+
if (authHeader?.startsWith("X-402 ")) {
|
|
369
|
+
try {
|
|
370
|
+
const encoded = authHeader.slice(6);
|
|
371
|
+
paymentData = JSON.parse(atob(encoded));
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!paymentData) {
|
|
377
|
+
try {
|
|
378
|
+
paymentData = await response.json();
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!paymentData) {
|
|
383
|
+
throw new X402ProtocolError("Could not parse payment requirements from 402 response");
|
|
384
|
+
}
|
|
385
|
+
if (!paymentData.recipient) {
|
|
386
|
+
throw new X402ProtocolError("Missing recipient in payment requirements");
|
|
387
|
+
}
|
|
388
|
+
if (!paymentData.amount) {
|
|
389
|
+
throw new X402ProtocolError("Missing amount in payment requirements");
|
|
390
|
+
}
|
|
391
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
392
|
+
return {
|
|
393
|
+
recipient: paymentData.recipient,
|
|
394
|
+
amount: BigInt(paymentData.amount),
|
|
395
|
+
currency: paymentData.currency || "USDC",
|
|
396
|
+
chainId: paymentData.chainId || paymentData.chain_id || 8453,
|
|
397
|
+
facilitatorUrl: paymentData.facilitatorUrl || paymentData.facilitator_url || "https://x402.org/facilitator",
|
|
398
|
+
nonce: paymentData.nonce || generateNonce().slice(2),
|
|
399
|
+
// Remove 0x prefix for storage
|
|
400
|
+
expiresAt: paymentData.expiresAt || paymentData.expires_at || now + 300,
|
|
401
|
+
description: paymentData.description
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async function buildXPaymentHeader(requirements, sessionKey, walletAddress) {
|
|
405
|
+
const nonceHex = requirements.nonce.length === 64 ? `0x${requirements.nonce}` : generateNonce();
|
|
406
|
+
const now = BigInt(Math.floor(Date.now() / 1e3));
|
|
407
|
+
const validAfter = now - 60n;
|
|
408
|
+
const validBefore = BigInt(requirements.expiresAt);
|
|
409
|
+
const { domain, message } = buildTransferAuthorizationData({
|
|
410
|
+
fromAddress: walletAddress,
|
|
411
|
+
toAddress: requirements.recipient,
|
|
412
|
+
value: requirements.amount,
|
|
413
|
+
validAfter,
|
|
414
|
+
validBefore,
|
|
415
|
+
nonce: nonceHex,
|
|
416
|
+
chainId: requirements.chainId
|
|
417
|
+
});
|
|
418
|
+
const signature = await sessionKey.signTransferAuthorization(domain, message);
|
|
419
|
+
const paymentPayload = {
|
|
420
|
+
x402Version: 1,
|
|
421
|
+
scheme: "exact",
|
|
422
|
+
network: requirements.chainId === 8453 ? "base" : "base-sepolia",
|
|
423
|
+
payload: {
|
|
424
|
+
signature,
|
|
425
|
+
authorization: {
|
|
426
|
+
from: walletAddress,
|
|
427
|
+
to: requirements.recipient,
|
|
428
|
+
value: requirements.amount.toString(),
|
|
429
|
+
validAfter: validAfter.toString(),
|
|
430
|
+
validBefore: validBefore.toString(),
|
|
431
|
+
nonce: nonceHex
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
return btoa(JSON.stringify(paymentPayload));
|
|
436
|
+
}
|
|
437
|
+
function isPaymentExpired(requirements) {
|
|
438
|
+
return Date.now() / 1e3 > requirements.expiresAt;
|
|
439
|
+
}
|
|
440
|
+
function getAmountUsd(requirements) {
|
|
441
|
+
return Number(requirements.amount) / 1e6;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/agent-wallet.ts
|
|
445
|
+
var SDK_VERSION = "0.4.1";
|
|
446
|
+
var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://www.mixrpay.com";
|
|
447
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
448
|
+
var NETWORKS = {
|
|
449
|
+
BASE_MAINNET: { chainId: 8453, name: "Base", isTestnet: false },
|
|
450
|
+
BASE_SEPOLIA: { chainId: 84532, name: "Base Sepolia", isTestnet: true }
|
|
451
|
+
};
|
|
452
|
+
var LOG_LEVELS = {
|
|
453
|
+
debug: 0,
|
|
454
|
+
info: 1,
|
|
455
|
+
warn: 2,
|
|
456
|
+
error: 3,
|
|
457
|
+
none: 4
|
|
458
|
+
};
|
|
459
|
+
var Logger = class {
|
|
460
|
+
level;
|
|
461
|
+
prefix;
|
|
462
|
+
constructor(level = "none", prefix = "[MixrPay]") {
|
|
463
|
+
this.level = level;
|
|
464
|
+
this.prefix = prefix;
|
|
465
|
+
}
|
|
466
|
+
setLevel(level) {
|
|
467
|
+
this.level = level;
|
|
468
|
+
}
|
|
469
|
+
shouldLog(level) {
|
|
470
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
471
|
+
}
|
|
472
|
+
debug(...args) {
|
|
473
|
+
if (this.shouldLog("debug")) {
|
|
474
|
+
console.log(`${this.prefix} \u{1F50D}`, ...args);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
info(...args) {
|
|
478
|
+
if (this.shouldLog("info")) {
|
|
479
|
+
console.log(`${this.prefix} \u2139\uFE0F`, ...args);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
warn(...args) {
|
|
483
|
+
if (this.shouldLog("warn")) {
|
|
484
|
+
console.warn(`${this.prefix} \u26A0\uFE0F`, ...args);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
error(...args) {
|
|
488
|
+
if (this.shouldLog("error")) {
|
|
489
|
+
console.error(`${this.prefix} \u274C`, ...args);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
payment(amount, recipient, description) {
|
|
493
|
+
if (this.shouldLog("info")) {
|
|
494
|
+
const desc = description ? ` for "${description}"` : "";
|
|
495
|
+
console.log(`${this.prefix} \u{1F4B8} Paid $${amount.toFixed(4)} to ${recipient.slice(0, 10)}...${desc}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var AgentWallet = class {
|
|
500
|
+
sessionKey;
|
|
501
|
+
walletAddress;
|
|
502
|
+
maxPaymentUsd;
|
|
503
|
+
onPayment;
|
|
504
|
+
baseUrl;
|
|
505
|
+
timeout;
|
|
506
|
+
logger;
|
|
507
|
+
// Payment tracking
|
|
508
|
+
payments = [];
|
|
509
|
+
totalSpentUsd = 0;
|
|
510
|
+
// Session key info cache
|
|
511
|
+
sessionKeyInfo;
|
|
512
|
+
sessionKeyInfoFetchedAt;
|
|
513
|
+
/**
|
|
514
|
+
* Create a new AgentWallet instance.
|
|
515
|
+
*
|
|
516
|
+
* @param config - Configuration options
|
|
517
|
+
* @throws {InvalidSessionKeyError} If the session key format is invalid
|
|
518
|
+
*
|
|
519
|
+
* @example Basic initialization
|
|
520
|
+
* ```typescript
|
|
521
|
+
* const wallet = new AgentWallet({
|
|
522
|
+
* sessionKey: 'sk_live_abc123...'
|
|
523
|
+
* });
|
|
524
|
+
* ```
|
|
525
|
+
*
|
|
526
|
+
* @example With all options
|
|
527
|
+
* ```typescript
|
|
528
|
+
* const wallet = new AgentWallet({
|
|
529
|
+
* sessionKey: 'sk_live_abc123...',
|
|
530
|
+
* maxPaymentUsd: 5.0, // Client-side safety limit
|
|
531
|
+
* onPayment: (p) => log(p), // Track payments
|
|
532
|
+
* logLevel: 'info', // Enable logging
|
|
533
|
+
* timeout: 60000, // 60s timeout
|
|
534
|
+
* });
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
constructor(config) {
|
|
538
|
+
this.validateConfig(config);
|
|
539
|
+
this.sessionKey = SessionKey.fromString(config.sessionKey);
|
|
540
|
+
this.walletAddress = config.walletAddress || this.sessionKey.address;
|
|
541
|
+
this.maxPaymentUsd = config.maxPaymentUsd;
|
|
542
|
+
this.onPayment = config.onPayment;
|
|
543
|
+
this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
544
|
+
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
545
|
+
this.logger = new Logger(config.logLevel || "none");
|
|
546
|
+
this.logger.debug("AgentWallet initialized", {
|
|
547
|
+
walletAddress: this.walletAddress,
|
|
548
|
+
isTestnet: this.isTestnet(),
|
|
549
|
+
maxPaymentUsd: this.maxPaymentUsd
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Validate the configuration before initialization.
|
|
554
|
+
*/
|
|
555
|
+
validateConfig(config) {
|
|
556
|
+
if (!config.sessionKey) {
|
|
557
|
+
throw new InvalidSessionKeyError(
|
|
558
|
+
"Session key is required. Get one from the wallet owner or create one at your MixrPay server /wallet/sessions"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
const key = config.sessionKey.trim();
|
|
562
|
+
if (!key.startsWith("sk_live_") && !key.startsWith("sk_test_")) {
|
|
563
|
+
throw new InvalidSessionKeyError(
|
|
564
|
+
`Invalid session key prefix. Expected 'sk_live_' (mainnet) or 'sk_test_' (testnet), got '${key.slice(0, 10)}...'`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const expectedLength = 8 + 64;
|
|
568
|
+
if (key.length !== expectedLength) {
|
|
569
|
+
throw new InvalidSessionKeyError(
|
|
570
|
+
`Invalid session key length. Expected ${expectedLength} characters, got ${key.length}. Make sure you copied the complete key.`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const hexPortion = key.slice(8);
|
|
574
|
+
if (!/^[0-9a-fA-F]+$/.test(hexPortion)) {
|
|
575
|
+
throw new InvalidSessionKeyError(
|
|
576
|
+
"Invalid session key format. The key should contain only hexadecimal characters after the prefix."
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (config.maxPaymentUsd !== void 0) {
|
|
580
|
+
if (config.maxPaymentUsd <= 0) {
|
|
581
|
+
throw new MixrPayError("maxPaymentUsd must be a positive number");
|
|
582
|
+
}
|
|
583
|
+
if (config.maxPaymentUsd > 1e4) {
|
|
584
|
+
this.logger?.warn("maxPaymentUsd is very high ($" + config.maxPaymentUsd + "). Consider using a lower limit for safety.");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// ===========================================================================
|
|
589
|
+
// Core Methods
|
|
590
|
+
// ===========================================================================
|
|
591
|
+
/**
|
|
592
|
+
* Make an HTTP request, automatically handling x402 payment if required.
|
|
593
|
+
*
|
|
594
|
+
* This is a drop-in replacement for the native `fetch()` function.
|
|
595
|
+
* If the server returns 402 Payment Required:
|
|
596
|
+
* 1. Parse payment requirements from response
|
|
597
|
+
* 2. Sign transferWithAuthorization using session key
|
|
598
|
+
* 3. Retry request with X-PAYMENT header
|
|
599
|
+
*
|
|
600
|
+
* @param url - Request URL
|
|
601
|
+
* @param init - Request options (same as native fetch)
|
|
602
|
+
* @returns Response from the server
|
|
603
|
+
*
|
|
604
|
+
* @throws {InsufficientBalanceError} If wallet doesn't have enough USDC
|
|
605
|
+
* @throws {SessionKeyExpiredError} If session key has expired
|
|
606
|
+
* @throws {SpendingLimitExceededError} If payment would exceed limits
|
|
607
|
+
* @throws {PaymentFailedError} If payment transaction failed
|
|
608
|
+
*
|
|
609
|
+
* @example GET request
|
|
610
|
+
* ```typescript
|
|
611
|
+
* const response = await wallet.fetch('https://api.example.com/data');
|
|
612
|
+
* const data = await response.json();
|
|
613
|
+
* ```
|
|
614
|
+
*
|
|
615
|
+
* @example POST request with JSON
|
|
616
|
+
* ```typescript
|
|
617
|
+
* const response = await wallet.fetch('https://api.example.com/generate', {
|
|
618
|
+
* method: 'POST',
|
|
619
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
620
|
+
* body: JSON.stringify({ prompt: 'Hello world' })
|
|
621
|
+
* });
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
async fetch(url, init) {
|
|
625
|
+
this.logger.debug(`Fetching ${init?.method || "GET"} ${url}`);
|
|
626
|
+
const controller = new AbortController();
|
|
627
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
628
|
+
try {
|
|
629
|
+
let response = await fetch(url, {
|
|
630
|
+
...init,
|
|
631
|
+
signal: controller.signal
|
|
632
|
+
});
|
|
633
|
+
this.logger.debug(`Initial response: ${response.status}`);
|
|
634
|
+
if (response.status === 402) {
|
|
635
|
+
this.logger.info(`Payment required for ${url}`);
|
|
636
|
+
response = await this.handlePaymentRequired(url, init, response);
|
|
637
|
+
}
|
|
638
|
+
return response;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
641
|
+
throw new MixrPayError(`Request timeout after ${this.timeout}ms`);
|
|
642
|
+
}
|
|
643
|
+
throw error;
|
|
644
|
+
} finally {
|
|
645
|
+
clearTimeout(timeoutId);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Handle a 402 Payment Required response.
|
|
650
|
+
*/
|
|
651
|
+
async handlePaymentRequired(url, init, response) {
|
|
652
|
+
let requirements;
|
|
653
|
+
try {
|
|
654
|
+
requirements = await parse402Response(response);
|
|
655
|
+
this.logger.debug("Payment requirements:", {
|
|
656
|
+
amount: `$${getAmountUsd(requirements).toFixed(4)}`,
|
|
657
|
+
recipient: requirements.recipient,
|
|
658
|
+
description: requirements.description
|
|
659
|
+
});
|
|
660
|
+
} catch (e) {
|
|
661
|
+
this.logger.error("Failed to parse payment requirements:", e);
|
|
662
|
+
throw new PaymentFailedError(
|
|
663
|
+
`Failed to parse payment requirements: ${e}. The server may not be properly configured for x402 payments.`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (isPaymentExpired(requirements)) {
|
|
667
|
+
throw new PaymentFailedError(
|
|
668
|
+
"Payment requirements have expired. This usually means the request took too long. Try again."
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const amountUsd = getAmountUsd(requirements);
|
|
672
|
+
if (this.maxPaymentUsd !== void 0 && amountUsd > this.maxPaymentUsd) {
|
|
673
|
+
throw new SpendingLimitExceededError(
|
|
674
|
+
"client_max",
|
|
675
|
+
this.maxPaymentUsd,
|
|
676
|
+
amountUsd
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
let xPayment;
|
|
680
|
+
try {
|
|
681
|
+
this.logger.debug("Signing payment authorization...");
|
|
682
|
+
xPayment = await buildXPaymentHeader(
|
|
683
|
+
requirements,
|
|
684
|
+
this.sessionKey,
|
|
685
|
+
this.walletAddress
|
|
686
|
+
);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
this.logger.error("Failed to sign payment:", e);
|
|
689
|
+
throw new PaymentFailedError(
|
|
690
|
+
`Failed to sign payment: ${e}. This may indicate an issue with the session key.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
this.logger.debug("Retrying request with payment...");
|
|
694
|
+
const headers = new Headers(init?.headers);
|
|
695
|
+
headers.set("X-PAYMENT", xPayment);
|
|
696
|
+
const retryResponse = await fetch(url, {
|
|
697
|
+
...init,
|
|
698
|
+
headers
|
|
699
|
+
});
|
|
700
|
+
if (retryResponse.status !== 402) {
|
|
701
|
+
const payment = {
|
|
702
|
+
amountUsd,
|
|
703
|
+
recipient: requirements.recipient,
|
|
704
|
+
txHash: retryResponse.headers.get("X-Payment-TxHash"),
|
|
705
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
706
|
+
description: requirements.description,
|
|
707
|
+
url
|
|
708
|
+
};
|
|
709
|
+
this.payments.push(payment);
|
|
710
|
+
this.totalSpentUsd += amountUsd;
|
|
711
|
+
this.logger.payment(amountUsd, requirements.recipient, requirements.description);
|
|
712
|
+
if (this.onPayment) {
|
|
713
|
+
this.onPayment(payment);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
await this.handlePaymentError(retryResponse);
|
|
717
|
+
}
|
|
718
|
+
return retryResponse;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Handle payment-specific errors from the response.
|
|
722
|
+
*/
|
|
723
|
+
async handlePaymentError(response) {
|
|
724
|
+
let errorData = {};
|
|
725
|
+
try {
|
|
726
|
+
errorData = await response.json();
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
const errorCode = errorData.error_code || "";
|
|
730
|
+
const errorMessage = errorData.error || errorData.message || "Payment required";
|
|
731
|
+
this.logger.error("Payment failed:", { errorCode, errorMessage, errorData });
|
|
732
|
+
if (errorCode === "insufficient_balance") {
|
|
733
|
+
const required = errorData.required || 0;
|
|
734
|
+
const available = errorData.available || 0;
|
|
735
|
+
throw new InsufficientBalanceError(required, available);
|
|
736
|
+
} else if (errorCode === "session_key_expired") {
|
|
737
|
+
throw new SessionKeyExpiredError(errorData.expired_at || "unknown");
|
|
738
|
+
} else if (errorCode === "spending_limit_exceeded") {
|
|
739
|
+
throw new SpendingLimitExceededError(
|
|
740
|
+
errorData.limit_type || "unknown",
|
|
741
|
+
errorData.limit || 0,
|
|
742
|
+
errorData.attempted || 0
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
throw new PaymentFailedError(errorMessage);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// ===========================================================================
|
|
749
|
+
// Wallet Information
|
|
750
|
+
// ===========================================================================
|
|
751
|
+
/**
|
|
752
|
+
* Get the wallet address.
|
|
753
|
+
*
|
|
754
|
+
* @returns The Ethereum address of the smart wallet
|
|
755
|
+
*/
|
|
756
|
+
getWalletAddress() {
|
|
757
|
+
return this.walletAddress;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Check if using testnet session key.
|
|
761
|
+
*
|
|
762
|
+
* @returns true if using Base Sepolia (testnet), false if using Base Mainnet
|
|
763
|
+
*/
|
|
764
|
+
isTestnet() {
|
|
765
|
+
return this.sessionKey.isTest;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Get the network information.
|
|
769
|
+
*
|
|
770
|
+
* @returns Network details including chain ID and name
|
|
771
|
+
*/
|
|
772
|
+
getNetwork() {
|
|
773
|
+
return this.isTestnet() ? NETWORKS.BASE_SEPOLIA : NETWORKS.BASE_MAINNET;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get current USDC balance of the wallet.
|
|
777
|
+
*
|
|
778
|
+
* @returns USDC balance in USD
|
|
779
|
+
*
|
|
780
|
+
* @example
|
|
781
|
+
* ```typescript
|
|
782
|
+
* const balance = await wallet.getBalance();
|
|
783
|
+
* console.log(`Balance: $${balance.toFixed(2)}`);
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
786
|
+
async getBalance() {
|
|
787
|
+
this.logger.debug("Fetching wallet balance...");
|
|
788
|
+
try {
|
|
789
|
+
const response = await fetch(`${this.baseUrl}/v1/wallet/balance`, {
|
|
790
|
+
headers: {
|
|
791
|
+
"X-Session-Key": this.sessionKey.address
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
if (response.ok) {
|
|
795
|
+
const data = await response.json();
|
|
796
|
+
const balance = data.balance_usd || data.balanceUsd || 0;
|
|
797
|
+
this.logger.debug(`Balance: $${balance}`);
|
|
798
|
+
return balance;
|
|
799
|
+
}
|
|
800
|
+
} catch (error) {
|
|
801
|
+
this.logger.warn("Failed to fetch balance:", error);
|
|
802
|
+
}
|
|
803
|
+
this.logger.debug("Using estimated balance based on tracking");
|
|
804
|
+
return Math.max(0, 100 - this.totalSpentUsd);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get information about the session key.
|
|
808
|
+
*
|
|
809
|
+
* @param refresh - Force refresh from server (default: use cache if < 60s old)
|
|
810
|
+
* @returns Session key details including limits and expiration
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```typescript
|
|
814
|
+
* const info = await wallet.getSessionKeyInfo();
|
|
815
|
+
* console.log(`Daily limit: $${info.limits.dailyUsd}`);
|
|
816
|
+
* console.log(`Expires: ${info.expiresAt}`);
|
|
817
|
+
* ```
|
|
818
|
+
*/
|
|
819
|
+
async getSessionKeyInfo(refresh = false) {
|
|
820
|
+
const cacheAge = this.sessionKeyInfoFetchedAt ? Date.now() - this.sessionKeyInfoFetchedAt : Infinity;
|
|
821
|
+
if (!refresh && this.sessionKeyInfo && cacheAge < 6e4) {
|
|
822
|
+
return this.sessionKeyInfo;
|
|
823
|
+
}
|
|
824
|
+
this.logger.debug("Fetching session key info...");
|
|
825
|
+
try {
|
|
826
|
+
const response = await fetch(`${this.baseUrl}/v1/session-key/info`, {
|
|
827
|
+
headers: {
|
|
828
|
+
"X-Session-Key": this.sessionKey.address
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
if (response.ok) {
|
|
832
|
+
const data = await response.json();
|
|
833
|
+
this.sessionKeyInfo = {
|
|
834
|
+
address: this.sessionKey.address,
|
|
835
|
+
isValid: data.is_valid ?? data.isValid ?? true,
|
|
836
|
+
limits: {
|
|
837
|
+
perTxUsd: data.per_tx_limit_usd ?? data.perTxLimitUsd ?? null,
|
|
838
|
+
dailyUsd: data.daily_limit_usd ?? data.dailyLimitUsd ?? null,
|
|
839
|
+
totalUsd: data.total_limit_usd ?? data.totalLimitUsd ?? null
|
|
840
|
+
},
|
|
841
|
+
usage: {
|
|
842
|
+
todayUsd: data.today_spent_usd ?? data.todaySpentUsd ?? 0,
|
|
843
|
+
totalUsd: data.total_spent_usd ?? data.totalSpentUsd ?? 0,
|
|
844
|
+
txCount: data.tx_count ?? data.txCount ?? 0
|
|
845
|
+
},
|
|
846
|
+
expiresAt: data.expires_at ? new Date(data.expires_at) : null,
|
|
847
|
+
createdAt: data.created_at ? new Date(data.created_at) : null,
|
|
848
|
+
name: data.name
|
|
849
|
+
};
|
|
850
|
+
this.sessionKeyInfoFetchedAt = Date.now();
|
|
851
|
+
return this.sessionKeyInfo;
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this.logger.warn("Failed to fetch session key info:", error);
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
address: this.sessionKey.address,
|
|
858
|
+
isValid: true,
|
|
859
|
+
limits: { perTxUsd: null, dailyUsd: null, totalUsd: null },
|
|
860
|
+
usage: { todayUsd: this.totalSpentUsd, totalUsd: this.totalSpentUsd, txCount: this.payments.length },
|
|
861
|
+
expiresAt: null,
|
|
862
|
+
createdAt: null
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Get spending stats for this session key.
|
|
867
|
+
*
|
|
868
|
+
* @returns Spending statistics
|
|
869
|
+
*
|
|
870
|
+
* @example
|
|
871
|
+
* ```typescript
|
|
872
|
+
* const stats = await wallet.getSpendingStats();
|
|
873
|
+
* console.log(`Spent: $${stats.totalSpentUsd.toFixed(2)}`);
|
|
874
|
+
* console.log(`Remaining daily: $${stats.remainingDailyUsd?.toFixed(2) ?? 'unlimited'}`);
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
async getSpendingStats() {
|
|
878
|
+
this.logger.debug("Fetching spending stats...");
|
|
879
|
+
try {
|
|
880
|
+
const response = await fetch(`${this.baseUrl}/v1/session-key/stats`, {
|
|
881
|
+
headers: {
|
|
882
|
+
"X-Session-Key": this.sessionKey.address
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
if (response.ok) {
|
|
886
|
+
const data = await response.json();
|
|
887
|
+
return {
|
|
888
|
+
totalSpentUsd: data.total_spent_usd || data.totalSpentUsd || this.totalSpentUsd,
|
|
889
|
+
txCount: data.tx_count || data.txCount || this.payments.length,
|
|
890
|
+
remainingDailyUsd: data.remaining_daily_usd || data.remainingDailyUsd || null,
|
|
891
|
+
remainingTotalUsd: data.remaining_total_usd || data.remainingTotalUsd || null,
|
|
892
|
+
expiresAt: data.expires_at ? new Date(data.expires_at) : null
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
} catch (error) {
|
|
896
|
+
this.logger.warn("Failed to fetch spending stats:", error);
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
totalSpentUsd: this.totalSpentUsd,
|
|
900
|
+
txCount: this.payments.length,
|
|
901
|
+
remainingDailyUsd: null,
|
|
902
|
+
remainingTotalUsd: null,
|
|
903
|
+
expiresAt: null
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get list of payments made in this session.
|
|
908
|
+
*
|
|
909
|
+
* @returns Array of payment events
|
|
910
|
+
*/
|
|
911
|
+
getPaymentHistory() {
|
|
912
|
+
return [...this.payments];
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Get the total amount spent in this session.
|
|
916
|
+
*
|
|
917
|
+
* @returns Total spent in USD
|
|
918
|
+
*/
|
|
919
|
+
getTotalSpent() {
|
|
920
|
+
return this.totalSpentUsd;
|
|
921
|
+
}
|
|
922
|
+
// ===========================================================================
|
|
923
|
+
// Diagnostics
|
|
924
|
+
// ===========================================================================
|
|
925
|
+
/**
|
|
926
|
+
* Run diagnostics to verify the wallet is properly configured.
|
|
927
|
+
*
|
|
928
|
+
* This is useful for debugging integration issues.
|
|
929
|
+
*
|
|
930
|
+
* @returns Diagnostic results with status and any issues found
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```typescript
|
|
934
|
+
* const diagnostics = await wallet.runDiagnostics();
|
|
935
|
+
* if (diagnostics.healthy) {
|
|
936
|
+
* console.log('✅ Wallet is ready to use');
|
|
937
|
+
* } else {
|
|
938
|
+
* console.log('❌ Issues found:');
|
|
939
|
+
* diagnostics.issues.forEach(issue => console.log(` - ${issue}`));
|
|
940
|
+
* }
|
|
941
|
+
* ```
|
|
942
|
+
*/
|
|
943
|
+
async runDiagnostics() {
|
|
944
|
+
this.logger.info("Running diagnostics...");
|
|
945
|
+
const issues = [];
|
|
946
|
+
const checks = {};
|
|
947
|
+
checks.sessionKeyFormat = true;
|
|
948
|
+
try {
|
|
949
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
950
|
+
method: "GET",
|
|
951
|
+
signal: AbortSignal.timeout(5e3)
|
|
952
|
+
});
|
|
953
|
+
checks.apiConnectivity = response.ok;
|
|
954
|
+
if (!response.ok) {
|
|
955
|
+
issues.push(`API server returned ${response.status}. Check baseUrl configuration.`);
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
checks.apiConnectivity = false;
|
|
959
|
+
issues.push("Cannot connect to MixrPay API. Check your network connection and baseUrl.");
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
const info = await this.getSessionKeyInfo(true);
|
|
963
|
+
checks.sessionKeyValid = info.isValid;
|
|
964
|
+
if (!info.isValid) {
|
|
965
|
+
issues.push("Session key is invalid or has been revoked.");
|
|
966
|
+
}
|
|
967
|
+
if (info.expiresAt && info.expiresAt < /* @__PURE__ */ new Date()) {
|
|
968
|
+
checks.sessionKeyValid = false;
|
|
969
|
+
issues.push(`Session key expired on ${info.expiresAt.toISOString()}`);
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
checks.sessionKeyValid = false;
|
|
973
|
+
issues.push("Could not verify session key validity.");
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
const balance = await this.getBalance();
|
|
977
|
+
checks.hasBalance = balance > 0;
|
|
978
|
+
if (balance <= 0) {
|
|
979
|
+
issues.push("Wallet has no USDC balance. Top up at your MixrPay server /wallet");
|
|
980
|
+
} else if (balance < 1) {
|
|
981
|
+
issues.push(`Low balance: $${balance.toFixed(2)}. Consider topping up.`);
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
checks.hasBalance = false;
|
|
985
|
+
issues.push("Could not fetch wallet balance.");
|
|
986
|
+
}
|
|
987
|
+
const healthy = issues.length === 0;
|
|
988
|
+
this.logger.info("Diagnostics complete:", { healthy, issues });
|
|
989
|
+
return {
|
|
990
|
+
healthy,
|
|
991
|
+
issues,
|
|
992
|
+
checks,
|
|
993
|
+
sdkVersion: SDK_VERSION,
|
|
994
|
+
network: this.getNetwork().name,
|
|
995
|
+
walletAddress: this.walletAddress
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Enable or disable debug logging.
|
|
1000
|
+
*
|
|
1001
|
+
* @param enable - true to enable debug logging, false to disable
|
|
1002
|
+
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* wallet.setDebug(true); // Enable detailed logs
|
|
1006
|
+
* await wallet.fetch(...);
|
|
1007
|
+
* wallet.setDebug(false); // Disable logs
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
setDebug(enable) {
|
|
1011
|
+
this.logger.setLevel(enable ? "debug" : "none");
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Set the log level.
|
|
1015
|
+
*
|
|
1016
|
+
* @param level - 'debug' | 'info' | 'warn' | 'error' | 'none'
|
|
1017
|
+
*/
|
|
1018
|
+
setLogLevel(level) {
|
|
1019
|
+
this.logger.setLevel(level);
|
|
1020
|
+
}
|
|
1021
|
+
// ===========================================================================
|
|
1022
|
+
// Session Authorization Methods (for MixrPay Merchants)
|
|
1023
|
+
// ===========================================================================
|
|
1024
|
+
/**
|
|
1025
|
+
* Create the X-Session-Auth header for secure API authentication.
|
|
1026
|
+
* Uses signature-based authentication - private key is NEVER transmitted.
|
|
1027
|
+
*
|
|
1028
|
+
* @returns Headers object with X-Session-Auth
|
|
1029
|
+
*/
|
|
1030
|
+
async getSessionAuthHeaders() {
|
|
1031
|
+
const payload = await createSessionAuthPayload(this.sessionKey);
|
|
1032
|
+
return {
|
|
1033
|
+
"X-Session-Auth": JSON.stringify(payload)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Get an existing session or create a new one with a MixrPay merchant.
|
|
1038
|
+
*
|
|
1039
|
+
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
1040
|
+
* If an active session exists, it will be returned. Otherwise, a new
|
|
1041
|
+
* session authorization request will be created and confirmed.
|
|
1042
|
+
*
|
|
1043
|
+
* @param options - Session creation options
|
|
1044
|
+
* @returns Active session authorization
|
|
1045
|
+
*
|
|
1046
|
+
* @throws {MixrPayError} If merchant is not found or session creation fails
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```typescript
|
|
1050
|
+
* const session = await wallet.getOrCreateSession({
|
|
1051
|
+
* merchantPublicKey: 'pk_live_abc123...',
|
|
1052
|
+
* spendingLimitUsd: 25.00,
|
|
1053
|
+
* durationDays: 7,
|
|
1054
|
+
* });
|
|
1055
|
+
*
|
|
1056
|
+
* console.log(`Session active: $${session.remainingLimitUsd} remaining`);
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
async getOrCreateSession(options) {
|
|
1060
|
+
this.logger.debug("getOrCreateSession called", options);
|
|
1061
|
+
const { merchantPublicKey, spendingLimitUsd = 25, durationDays = 7 } = options;
|
|
1062
|
+
try {
|
|
1063
|
+
const existingSession = await this.getSessionByMerchant(merchantPublicKey);
|
|
1064
|
+
if (existingSession && existingSession.status === "active") {
|
|
1065
|
+
this.logger.debug("Found existing active session", existingSession.id);
|
|
1066
|
+
return existingSession;
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
this.logger.info(`Creating new session with merchant ${merchantPublicKey.slice(0, 20)}...`);
|
|
1071
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1072
|
+
const authorizeResponse = await fetch(`${this.baseUrl}/api/v2/session/authorize`, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: {
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
...authHeaders
|
|
1077
|
+
},
|
|
1078
|
+
body: JSON.stringify({
|
|
1079
|
+
merchant_public_key: merchantPublicKey,
|
|
1080
|
+
spending_limit_usd: spendingLimitUsd,
|
|
1081
|
+
duration_days: durationDays
|
|
1082
|
+
})
|
|
1083
|
+
});
|
|
1084
|
+
if (!authorizeResponse.ok) {
|
|
1085
|
+
const error = await authorizeResponse.json().catch(() => ({}));
|
|
1086
|
+
throw new MixrPayError(
|
|
1087
|
+
error.message || error.error || `Failed to create session: ${authorizeResponse.status}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const authorizeData = await authorizeResponse.json();
|
|
1091
|
+
const sessionId = authorizeData.session_id;
|
|
1092
|
+
const messageToSign = authorizeData.message_to_sign;
|
|
1093
|
+
if (!sessionId || !messageToSign) {
|
|
1094
|
+
throw new MixrPayError("Invalid authorize response: missing session_id or message_to_sign");
|
|
1095
|
+
}
|
|
1096
|
+
this.logger.debug("Signing session authorization message...");
|
|
1097
|
+
const signature = await this.sessionKey.signMessage(messageToSign);
|
|
1098
|
+
const confirmResponse = await fetch(`${this.baseUrl}/api/v2/session/confirm`, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "application/json"
|
|
1102
|
+
},
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
session_id: sessionId,
|
|
1105
|
+
signature,
|
|
1106
|
+
wallet_address: this.walletAddress
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
if (!confirmResponse.ok) {
|
|
1110
|
+
const error = await confirmResponse.json().catch(() => ({}));
|
|
1111
|
+
throw new MixrPayError(
|
|
1112
|
+
error.message || `Failed to confirm session: ${confirmResponse.status}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const confirmData = await confirmResponse.json();
|
|
1116
|
+
this.logger.info(`Session created: ${confirmData.session?.id || sessionId}`);
|
|
1117
|
+
return this.parseSessionResponse(confirmData.session || confirmData);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Get session status for a specific merchant.
|
|
1121
|
+
*
|
|
1122
|
+
* @param merchantPublicKey - The merchant's public key
|
|
1123
|
+
* @returns Session authorization or null if not found
|
|
1124
|
+
*/
|
|
1125
|
+
async getSessionByMerchant(merchantPublicKey) {
|
|
1126
|
+
this.logger.debug("getSessionByMerchant", merchantPublicKey);
|
|
1127
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1128
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(merchantPublicKey)}`, {
|
|
1129
|
+
headers: authHeaders
|
|
1130
|
+
});
|
|
1131
|
+
if (response.status === 404) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
if (!response.ok) {
|
|
1135
|
+
const error = await response.json().catch(() => ({}));
|
|
1136
|
+
throw new MixrPayError(error.message || `Failed to get session: ${response.status}`);
|
|
1137
|
+
}
|
|
1138
|
+
const data = await response.json();
|
|
1139
|
+
return data.session ? this.parseSessionResponse(data.session) : null;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* List all session authorizations for this wallet.
|
|
1143
|
+
*
|
|
1144
|
+
* @returns Array of session authorizations
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* ```typescript
|
|
1148
|
+
* const sessions = await wallet.listSessions();
|
|
1149
|
+
* for (const session of sessions) {
|
|
1150
|
+
* console.log(`${session.merchantName}: $${session.remainingLimitUsd} remaining`);
|
|
1151
|
+
* }
|
|
1152
|
+
* ```
|
|
1153
|
+
*/
|
|
1154
|
+
async listSessions() {
|
|
1155
|
+
this.logger.debug("listSessions");
|
|
1156
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1157
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/list`, {
|
|
1158
|
+
headers: authHeaders
|
|
1159
|
+
});
|
|
1160
|
+
if (!response.ok) {
|
|
1161
|
+
const error = await response.json().catch(() => ({}));
|
|
1162
|
+
throw new MixrPayError(error.message || `Failed to list sessions: ${response.status}`);
|
|
1163
|
+
}
|
|
1164
|
+
const data = await response.json();
|
|
1165
|
+
return (data.sessions || []).map((s) => this.parseSessionResponse(s));
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Revoke a session authorization.
|
|
1169
|
+
*
|
|
1170
|
+
* After revocation, no further charges can be made against this session.
|
|
1171
|
+
*
|
|
1172
|
+
* @param sessionId - The session ID to revoke
|
|
1173
|
+
* @returns true if revoked successfully
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* ```typescript
|
|
1177
|
+
* const revoked = await wallet.revokeSession('sess_abc123');
|
|
1178
|
+
* if (revoked) {
|
|
1179
|
+
* console.log('Session revoked successfully');
|
|
1180
|
+
* }
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
async revokeSession(sessionId) {
|
|
1184
|
+
this.logger.debug("revokeSession", sessionId);
|
|
1185
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1186
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/revoke`, {
|
|
1187
|
+
method: "POST",
|
|
1188
|
+
headers: {
|
|
1189
|
+
"Content-Type": "application/json",
|
|
1190
|
+
...authHeaders
|
|
1191
|
+
},
|
|
1192
|
+
body: JSON.stringify({ session_id: sessionId })
|
|
1193
|
+
});
|
|
1194
|
+
if (!response.ok) {
|
|
1195
|
+
const error = await response.json().catch(() => ({}));
|
|
1196
|
+
this.logger.error("Failed to revoke session:", error);
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
this.logger.info(`Session ${sessionId} revoked`);
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Get statistics about all session authorizations.
|
|
1204
|
+
*
|
|
1205
|
+
* This provides an overview of active, expired, and revoked sessions,
|
|
1206
|
+
* along with aggregate spending information.
|
|
1207
|
+
*
|
|
1208
|
+
* @returns Session statistics
|
|
1209
|
+
*
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```typescript
|
|
1212
|
+
* const stats = await wallet.getSessionStats();
|
|
1213
|
+
* console.log(`Active sessions: ${stats.activeCount}`);
|
|
1214
|
+
* console.log(`Total authorized: $${stats.totalAuthorizedUsd.toFixed(2)}`);
|
|
1215
|
+
* console.log(`Total remaining: $${stats.totalRemainingUsd.toFixed(2)}`);
|
|
1216
|
+
*
|
|
1217
|
+
* for (const session of stats.activeSessions) {
|
|
1218
|
+
* console.log(`${session.merchantName}: $${session.remainingUsd} remaining`);
|
|
1219
|
+
* }
|
|
1220
|
+
* ```
|
|
1221
|
+
*/
|
|
1222
|
+
async getSessionStats() {
|
|
1223
|
+
this.logger.debug("getSessionStats");
|
|
1224
|
+
const sessions = await this.listSessions();
|
|
1225
|
+
const now = /* @__PURE__ */ new Date();
|
|
1226
|
+
const active = [];
|
|
1227
|
+
const expired = [];
|
|
1228
|
+
const revoked = [];
|
|
1229
|
+
for (const session of sessions) {
|
|
1230
|
+
if (session.status === "revoked") {
|
|
1231
|
+
revoked.push(session);
|
|
1232
|
+
} else if (session.status === "expired" || session.expiresAt && session.expiresAt < now) {
|
|
1233
|
+
expired.push(session);
|
|
1234
|
+
} else if (session.status === "active") {
|
|
1235
|
+
active.push(session);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const totalAuthorizedUsd = active.reduce((sum, s) => sum + s.spendingLimitUsd, 0);
|
|
1239
|
+
const totalSpentUsd = sessions.reduce((sum, s) => sum + s.amountUsedUsd, 0);
|
|
1240
|
+
const totalRemainingUsd = active.reduce((sum, s) => sum + s.remainingLimitUsd, 0);
|
|
1241
|
+
return {
|
|
1242
|
+
activeCount: active.length,
|
|
1243
|
+
expiredCount: expired.length,
|
|
1244
|
+
revokedCount: revoked.length,
|
|
1245
|
+
totalAuthorizedUsd,
|
|
1246
|
+
totalSpentUsd,
|
|
1247
|
+
totalRemainingUsd,
|
|
1248
|
+
activeSessions: active.map((s) => ({
|
|
1249
|
+
id: s.id,
|
|
1250
|
+
merchantName: s.merchantName,
|
|
1251
|
+
merchantPublicKey: s.merchantId,
|
|
1252
|
+
// merchantId is the public key in this context
|
|
1253
|
+
spendingLimitUsd: s.spendingLimitUsd,
|
|
1254
|
+
remainingUsd: s.remainingLimitUsd,
|
|
1255
|
+
expiresAt: s.expiresAt
|
|
1256
|
+
}))
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Charge against an active session authorization.
|
|
1261
|
+
*
|
|
1262
|
+
* This is useful when you need to manually charge a session outside of
|
|
1263
|
+
* the `callMerchantApi()` flow.
|
|
1264
|
+
*
|
|
1265
|
+
* @param sessionId - The session ID to charge
|
|
1266
|
+
* @param amountUsd - Amount to charge in USD
|
|
1267
|
+
* @param options - Additional charge options
|
|
1268
|
+
* @returns Charge result
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```typescript
|
|
1272
|
+
* const result = await wallet.chargeSession('sess_abc123', 0.05, {
|
|
1273
|
+
* feature: 'ai-generation',
|
|
1274
|
+
* idempotencyKey: 'unique-key-123',
|
|
1275
|
+
* });
|
|
1276
|
+
*
|
|
1277
|
+
* console.log(`Charged $${result.amountUsd}, remaining: $${result.remainingSessionBalanceUsd}`);
|
|
1278
|
+
* ```
|
|
1279
|
+
*/
|
|
1280
|
+
async chargeSession(sessionId, amountUsd, options = {}) {
|
|
1281
|
+
this.logger.debug("chargeSession", { sessionId, amountUsd, options });
|
|
1282
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1283
|
+
const response = await fetch(`${this.baseUrl}/api/v2/charge`, {
|
|
1284
|
+
method: "POST",
|
|
1285
|
+
headers: {
|
|
1286
|
+
"Content-Type": "application/json",
|
|
1287
|
+
...authHeaders
|
|
1288
|
+
},
|
|
1289
|
+
body: JSON.stringify({
|
|
1290
|
+
session_id: sessionId,
|
|
1291
|
+
price_usd: amountUsd,
|
|
1292
|
+
feature: options.feature,
|
|
1293
|
+
idempotency_key: options.idempotencyKey,
|
|
1294
|
+
metadata: options.metadata
|
|
1295
|
+
})
|
|
1296
|
+
});
|
|
1297
|
+
if (!response.ok) {
|
|
1298
|
+
const error = await response.json().catch(() => ({}));
|
|
1299
|
+
const errorCode = error.error || error.error_code || "";
|
|
1300
|
+
if (response.status === 402) {
|
|
1301
|
+
if (errorCode === "session_limit_exceeded") {
|
|
1302
|
+
const limit = error.sessionLimitUsd || error.session_limit_usd || 0;
|
|
1303
|
+
const remaining = error.remainingUsd || error.remaining_usd || 0;
|
|
1304
|
+
throw new SessionLimitExceededError(limit, amountUsd, remaining, sessionId);
|
|
1305
|
+
}
|
|
1306
|
+
if (errorCode === "insufficient_balance") {
|
|
1307
|
+
throw new InsufficientBalanceError(
|
|
1308
|
+
amountUsd,
|
|
1309
|
+
error.availableUsd || error.available_usd || 0
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (response.status === 404 || errorCode === "session_not_found") {
|
|
1314
|
+
throw new SessionNotFoundError(sessionId);
|
|
1315
|
+
}
|
|
1316
|
+
if (errorCode === "session_expired") {
|
|
1317
|
+
throw new SessionExpiredError(sessionId, error.expiredAt || error.expires_at);
|
|
1318
|
+
}
|
|
1319
|
+
if (errorCode === "session_revoked") {
|
|
1320
|
+
throw new SessionRevokedError(sessionId, error.reason);
|
|
1321
|
+
}
|
|
1322
|
+
throw new MixrPayError(error.message || `Charge failed: ${response.status}`);
|
|
1323
|
+
}
|
|
1324
|
+
const data = await response.json();
|
|
1325
|
+
this.logger.payment(amountUsd, sessionId, options.feature);
|
|
1326
|
+
return {
|
|
1327
|
+
success: true,
|
|
1328
|
+
chargeId: data.chargeId || data.charge_id,
|
|
1329
|
+
amountUsd: data.amountUsd || data.amount_usd || amountUsd,
|
|
1330
|
+
txHash: data.txHash || data.tx_hash,
|
|
1331
|
+
remainingSessionBalanceUsd: data.remainingSessionBalanceUsd || data.remaining_session_balance_usd || 0
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Call a MixrPay merchant's API with automatic session management.
|
|
1336
|
+
*
|
|
1337
|
+
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
1338
|
+
* It automatically:
|
|
1339
|
+
* 1. Gets or creates a session authorization
|
|
1340
|
+
* 2. Adds the `X-Mixr-Session` header to the request
|
|
1341
|
+
* 3. Handles payment errors and session expiration
|
|
1342
|
+
*
|
|
1343
|
+
* @param options - API call options
|
|
1344
|
+
* @returns Response from the merchant API
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```typescript
|
|
1348
|
+
* const response = await wallet.callMerchantApi({
|
|
1349
|
+
* url: 'https://api.merchant.com/generate',
|
|
1350
|
+
* merchantPublicKey: 'pk_live_abc123...',
|
|
1351
|
+
* method: 'POST',
|
|
1352
|
+
* body: { prompt: 'Hello world' },
|
|
1353
|
+
* priceUsd: 0.05,
|
|
1354
|
+
* });
|
|
1355
|
+
*
|
|
1356
|
+
* const data = await response.json();
|
|
1357
|
+
* ```
|
|
1358
|
+
*/
|
|
1359
|
+
async callMerchantApi(options) {
|
|
1360
|
+
const {
|
|
1361
|
+
url,
|
|
1362
|
+
method = "POST",
|
|
1363
|
+
body,
|
|
1364
|
+
headers: customHeaders = {},
|
|
1365
|
+
merchantPublicKey,
|
|
1366
|
+
priceUsd,
|
|
1367
|
+
feature
|
|
1368
|
+
} = options;
|
|
1369
|
+
this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
|
|
1370
|
+
if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
|
|
1371
|
+
throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
|
|
1372
|
+
}
|
|
1373
|
+
const session = await this.getOrCreateSession({
|
|
1374
|
+
merchantPublicKey,
|
|
1375
|
+
spendingLimitUsd: 25,
|
|
1376
|
+
// Default limit
|
|
1377
|
+
durationDays: 7
|
|
1378
|
+
});
|
|
1379
|
+
const headers = {
|
|
1380
|
+
"Content-Type": "application/json",
|
|
1381
|
+
"X-Mixr-Session": session.id,
|
|
1382
|
+
...customHeaders
|
|
1383
|
+
};
|
|
1384
|
+
if (feature) {
|
|
1385
|
+
headers["X-Mixr-Feature"] = feature;
|
|
1386
|
+
}
|
|
1387
|
+
const requestBody = body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
|
|
1388
|
+
const response = await fetch(url, {
|
|
1389
|
+
method,
|
|
1390
|
+
headers,
|
|
1391
|
+
body: requestBody,
|
|
1392
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1393
|
+
});
|
|
1394
|
+
const chargedAmount = response.headers.get("X-Mixr-Charged");
|
|
1395
|
+
if (chargedAmount) {
|
|
1396
|
+
const amountUsd = parseFloat(chargedAmount);
|
|
1397
|
+
if (!isNaN(amountUsd)) {
|
|
1398
|
+
const payment = {
|
|
1399
|
+
amountUsd,
|
|
1400
|
+
recipient: merchantPublicKey,
|
|
1401
|
+
txHash: response.headers.get("X-Payment-TxHash"),
|
|
1402
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1403
|
+
description: feature || "API call",
|
|
1404
|
+
url
|
|
1405
|
+
};
|
|
1406
|
+
this.payments.push(payment);
|
|
1407
|
+
this.totalSpentUsd += amountUsd;
|
|
1408
|
+
this.logger.payment(amountUsd, merchantPublicKey, feature);
|
|
1409
|
+
if (this.onPayment) {
|
|
1410
|
+
this.onPayment(payment);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (response.status === 402) {
|
|
1415
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1416
|
+
const errorCode = errorData.error || errorData.error_code || "";
|
|
1417
|
+
if (errorCode === "session_expired") {
|
|
1418
|
+
this.logger.info("Session expired, creating new one...");
|
|
1419
|
+
const newSession = await this.getOrCreateSession({
|
|
1420
|
+
merchantPublicKey,
|
|
1421
|
+
spendingLimitUsd: 25,
|
|
1422
|
+
durationDays: 7
|
|
1423
|
+
});
|
|
1424
|
+
headers["X-Mixr-Session"] = newSession.id;
|
|
1425
|
+
return fetch(url, {
|
|
1426
|
+
method,
|
|
1427
|
+
headers,
|
|
1428
|
+
body: requestBody,
|
|
1429
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
if (errorCode === "session_limit_exceeded") {
|
|
1433
|
+
const limit = errorData.sessionLimitUsd || errorData.session_limit_usd || 0;
|
|
1434
|
+
const remaining = errorData.remainingUsd || errorData.remaining_usd || 0;
|
|
1435
|
+
throw new SessionLimitExceededError(limit, priceUsd || 0, remaining, session.id);
|
|
1436
|
+
}
|
|
1437
|
+
if (errorCode === "session_revoked") {
|
|
1438
|
+
throw new SessionRevokedError(session.id, errorData.reason);
|
|
1439
|
+
}
|
|
1440
|
+
if (errorCode === "session_not_found") {
|
|
1441
|
+
throw new SessionNotFoundError(session.id);
|
|
1442
|
+
}
|
|
1443
|
+
if (errorCode === "insufficient_balance") {
|
|
1444
|
+
throw new InsufficientBalanceError(
|
|
1445
|
+
priceUsd || 0,
|
|
1446
|
+
errorData.availableUsd || errorData.available_usd || 0
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return response;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Parse session response data into SessionAuthorization object.
|
|
1454
|
+
*/
|
|
1455
|
+
parseSessionResponse(data) {
|
|
1456
|
+
return {
|
|
1457
|
+
id: data.id || data.session_id || data.sessionId,
|
|
1458
|
+
merchantId: data.merchantId || data.merchant_id,
|
|
1459
|
+
merchantName: data.merchantName || data.merchant_name || "Unknown",
|
|
1460
|
+
status: data.status || "active",
|
|
1461
|
+
spendingLimitUsd: Number(data.spendingLimitUsd || data.spending_limit_usd || data.spendingLimit || 0),
|
|
1462
|
+
amountUsedUsd: Number(data.amountUsedUsd || data.amount_used_usd || data.amountUsed || 0),
|
|
1463
|
+
remainingLimitUsd: Number(
|
|
1464
|
+
data.remainingLimitUsd || data.remaining_limit_usd || data.remainingLimit || Number(data.spendingLimitUsd || data.spending_limit_usd || 0) - Number(data.amountUsedUsd || data.amount_used_usd || 0)
|
|
1465
|
+
),
|
|
1466
|
+
expiresAt: new Date(data.expiresAt || data.expires_at),
|
|
1467
|
+
createdAt: new Date(data.createdAt || data.created_at)
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
// ===========================================================================
|
|
1471
|
+
// MCP (Model Context Protocol) Methods
|
|
1472
|
+
// ===========================================================================
|
|
1473
|
+
/**
|
|
1474
|
+
* Get authentication headers for MCP wallet-based authentication.
|
|
1475
|
+
*
|
|
1476
|
+
* These headers prove wallet ownership without transmitting the private key.
|
|
1477
|
+
* Use for direct pay-per-call mode (no session needed).
|
|
1478
|
+
*
|
|
1479
|
+
* @returns Headers object with X-Mixr-Wallet, X-Mixr-Signature, X-Mixr-Timestamp
|
|
1480
|
+
*
|
|
1481
|
+
* @example
|
|
1482
|
+
* ```typescript
|
|
1483
|
+
* const headers = await wallet.getMCPAuthHeaders();
|
|
1484
|
+
* const response = await fetch('https://mixrpay.com/api/mcp', {
|
|
1485
|
+
* method: 'POST',
|
|
1486
|
+
* headers: {
|
|
1487
|
+
* 'Content-Type': 'application/json',
|
|
1488
|
+
* ...headers,
|
|
1489
|
+
* },
|
|
1490
|
+
* body: JSON.stringify({
|
|
1491
|
+
* jsonrpc: '2.0',
|
|
1492
|
+
* method: 'tools/list',
|
|
1493
|
+
* id: 1,
|
|
1494
|
+
* }),
|
|
1495
|
+
* });
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
async getMCPAuthHeaders() {
|
|
1499
|
+
const timestamp = Date.now().toString();
|
|
1500
|
+
const message = `MixrPay MCP Auth
|
|
1501
|
+
Wallet: ${this.walletAddress}
|
|
1502
|
+
Timestamp: ${timestamp}`;
|
|
1503
|
+
const signature = await this.sessionKey.signMessage(message);
|
|
1504
|
+
return {
|
|
1505
|
+
"X-Mixr-Wallet": this.walletAddress,
|
|
1506
|
+
"X-Mixr-Signature": signature,
|
|
1507
|
+
"X-Mixr-Timestamp": timestamp
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* List available MCP tools from the MixrPay gateway.
|
|
1512
|
+
*
|
|
1513
|
+
* Returns all tools exposed by MCP providers on the MixrPay marketplace.
|
|
1514
|
+
* Each tool includes pricing information.
|
|
1515
|
+
*
|
|
1516
|
+
* @returns Array of MCP tools with pricing and metadata
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* ```typescript
|
|
1520
|
+
* const tools = await wallet.listMCPTools();
|
|
1521
|
+
* for (const tool of tools) {
|
|
1522
|
+
* console.log(`${tool.name}: $${tool.priceUsd} - ${tool.description}`);
|
|
1523
|
+
* }
|
|
1524
|
+
* ```
|
|
1525
|
+
*/
|
|
1526
|
+
async listMCPTools() {
|
|
1527
|
+
this.logger.debug("listMCPTools");
|
|
1528
|
+
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
headers: { "Content-Type": "application/json" },
|
|
1531
|
+
body: JSON.stringify({
|
|
1532
|
+
jsonrpc: "2.0",
|
|
1533
|
+
method: "tools/list",
|
|
1534
|
+
id: Date.now()
|
|
1535
|
+
})
|
|
1536
|
+
});
|
|
1537
|
+
if (!response.ok) {
|
|
1538
|
+
throw new MixrPayError(`Failed to list MCP tools: ${response.status}`);
|
|
1539
|
+
}
|
|
1540
|
+
const result = await response.json();
|
|
1541
|
+
if (result.error) {
|
|
1542
|
+
throw new MixrPayError(result.error.message || "Failed to list MCP tools");
|
|
1543
|
+
}
|
|
1544
|
+
return (result.result?.tools || []).map((tool) => ({
|
|
1545
|
+
name: tool.name,
|
|
1546
|
+
description: tool.description,
|
|
1547
|
+
inputSchema: tool.inputSchema,
|
|
1548
|
+
priceUsd: tool["x-mixrpay"]?.priceUsd || 0,
|
|
1549
|
+
merchantName: tool["x-mixrpay"]?.merchantName,
|
|
1550
|
+
merchantSlug: tool["x-mixrpay"]?.merchantSlug,
|
|
1551
|
+
verified: tool["x-mixrpay"]?.verified || false
|
|
1552
|
+
}));
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Call an MCP tool with wallet authentication (direct pay per call).
|
|
1556
|
+
*
|
|
1557
|
+
* This method signs a fresh auth message for each call, charging
|
|
1558
|
+
* directly from your wallet balance without needing a session.
|
|
1559
|
+
*
|
|
1560
|
+
* @param toolName - The tool name in format "merchant/tool"
|
|
1561
|
+
* @param args - Arguments to pass to the tool
|
|
1562
|
+
* @returns Tool execution result
|
|
1563
|
+
*
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```typescript
|
|
1566
|
+
* const result = await wallet.callMCPTool('firecrawl/scrape', {
|
|
1567
|
+
* url: 'https://example.com',
|
|
1568
|
+
* });
|
|
1569
|
+
* console.log(result.data);
|
|
1570
|
+
* console.log(`Charged: $${result.chargedUsd}`);
|
|
1571
|
+
* ```
|
|
1572
|
+
*/
|
|
1573
|
+
async callMCPTool(toolName, args = {}) {
|
|
1574
|
+
this.logger.debug("callMCPTool", { toolName, args });
|
|
1575
|
+
const authHeaders = await this.getMCPAuthHeaders();
|
|
1576
|
+
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
1577
|
+
method: "POST",
|
|
1578
|
+
headers: {
|
|
1579
|
+
"Content-Type": "application/json",
|
|
1580
|
+
...authHeaders
|
|
1581
|
+
},
|
|
1582
|
+
body: JSON.stringify({
|
|
1583
|
+
jsonrpc: "2.0",
|
|
1584
|
+
method: "tools/call",
|
|
1585
|
+
params: { name: toolName, arguments: args },
|
|
1586
|
+
id: Date.now()
|
|
1587
|
+
})
|
|
1588
|
+
});
|
|
1589
|
+
const result = await response.json();
|
|
1590
|
+
if (result.error) {
|
|
1591
|
+
throw new MixrPayError(result.error.message || "MCP tool call failed");
|
|
1592
|
+
}
|
|
1593
|
+
const content = result.result?.content?.[0];
|
|
1594
|
+
const data = content?.text ? JSON.parse(content.text) : null;
|
|
1595
|
+
const mixrpay = result.result?._mixrpay || {};
|
|
1596
|
+
if (mixrpay.chargedUsd) {
|
|
1597
|
+
const payment = {
|
|
1598
|
+
amountUsd: mixrpay.chargedUsd,
|
|
1599
|
+
recipient: toolName.split("/")[0] || toolName,
|
|
1600
|
+
txHash: mixrpay.txHash,
|
|
1601
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1602
|
+
description: `MCP: ${toolName}`,
|
|
1603
|
+
url: `${this.baseUrl}/api/mcp`
|
|
1604
|
+
};
|
|
1605
|
+
this.payments.push(payment);
|
|
1606
|
+
this.totalSpentUsd += mixrpay.chargedUsd;
|
|
1607
|
+
this.logger.payment(mixrpay.chargedUsd, toolName, "MCP call");
|
|
1608
|
+
if (this.onPayment) {
|
|
1609
|
+
this.onPayment(payment);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
data,
|
|
1614
|
+
chargedUsd: mixrpay.chargedUsd || 0,
|
|
1615
|
+
txHash: mixrpay.txHash,
|
|
1616
|
+
latencyMs: mixrpay.latencyMs
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
// ===========================================================================
|
|
1620
|
+
// Agent Runtime API
|
|
1621
|
+
// ===========================================================================
|
|
1622
|
+
/**
|
|
1623
|
+
* Run an AI agent with LLM and tool execution.
|
|
1624
|
+
*
|
|
1625
|
+
* This method orchestrates a full agentic loop:
|
|
1626
|
+
* - Multi-turn reasoning with LLM
|
|
1627
|
+
* - Automatic tool execution
|
|
1628
|
+
* - Bundled billing (single charge at end)
|
|
1629
|
+
* - Optional streaming via SSE
|
|
1630
|
+
*
|
|
1631
|
+
* @param options - Agent run options
|
|
1632
|
+
* @returns Agent run result with response and cost breakdown
|
|
1633
|
+
*
|
|
1634
|
+
* @example Basic usage
|
|
1635
|
+
* ```typescript
|
|
1636
|
+
* const result = await wallet.runAgent({
|
|
1637
|
+
* sessionId: 'sess_abc123',
|
|
1638
|
+
* messages: [{ role: 'user', content: 'Find AI startups in SF' }],
|
|
1639
|
+
* });
|
|
1640
|
+
*
|
|
1641
|
+
* console.log(result.response);
|
|
1642
|
+
* console.log(`Cost: $${result.cost.totalUsd.toFixed(4)}`);
|
|
1643
|
+
* ```
|
|
1644
|
+
*
|
|
1645
|
+
* @example With custom config
|
|
1646
|
+
* ```typescript
|
|
1647
|
+
* const result = await wallet.runAgent({
|
|
1648
|
+
* sessionId: 'sess_abc123',
|
|
1649
|
+
* messages: [{ role: 'user', content: 'Research quantum computing' }],
|
|
1650
|
+
* config: {
|
|
1651
|
+
* model: 'gpt-4o',
|
|
1652
|
+
* maxIterations: 15,
|
|
1653
|
+
* tools: ['platform/exa-search', 'platform/firecrawl-scrape'],
|
|
1654
|
+
* systemPrompt: 'You are a research assistant.',
|
|
1655
|
+
* },
|
|
1656
|
+
* });
|
|
1657
|
+
* ```
|
|
1658
|
+
*
|
|
1659
|
+
* @example With streaming
|
|
1660
|
+
* ```typescript
|
|
1661
|
+
* await wallet.runAgent({
|
|
1662
|
+
* sessionId: 'sess_abc123',
|
|
1663
|
+
* messages: [{ role: 'user', content: 'Analyze this company' }],
|
|
1664
|
+
* stream: true,
|
|
1665
|
+
* onEvent: (event) => {
|
|
1666
|
+
* if (event.type === 'llm_chunk') {
|
|
1667
|
+
* process.stdout.write(event.delta);
|
|
1668
|
+
* } else if (event.type === 'tool_call') {
|
|
1669
|
+
* console.log(`Calling tool: ${event.tool}`);
|
|
1670
|
+
* }
|
|
1671
|
+
* },
|
|
1672
|
+
* });
|
|
1673
|
+
* ```
|
|
1674
|
+
*/
|
|
1675
|
+
async runAgent(options) {
|
|
1676
|
+
const {
|
|
1677
|
+
sessionId,
|
|
1678
|
+
messages,
|
|
1679
|
+
config = {},
|
|
1680
|
+
stream = false,
|
|
1681
|
+
idempotencyKey,
|
|
1682
|
+
onEvent
|
|
1683
|
+
} = options;
|
|
1684
|
+
this.logger.debug("runAgent", { sessionId, messageCount: messages.length, config, stream });
|
|
1685
|
+
const body = {
|
|
1686
|
+
session_id: sessionId,
|
|
1687
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1688
|
+
config: {
|
|
1689
|
+
model: config.model,
|
|
1690
|
+
max_iterations: config.maxIterations,
|
|
1691
|
+
tools: config.tools,
|
|
1692
|
+
system_prompt: config.systemPrompt
|
|
1693
|
+
},
|
|
1694
|
+
stream,
|
|
1695
|
+
idempotency_key: idempotencyKey
|
|
1696
|
+
};
|
|
1697
|
+
const AGENT_RUN_TIMEOUT = 18e4;
|
|
1698
|
+
if (!stream) {
|
|
1699
|
+
const response = await fetch(`${this.baseUrl}/api/v2/agent/run`, {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers: {
|
|
1702
|
+
"Content-Type": "application/json",
|
|
1703
|
+
"X-Mixr-Session": sessionId
|
|
1704
|
+
},
|
|
1705
|
+
body: JSON.stringify(body),
|
|
1706
|
+
signal: AbortSignal.timeout(AGENT_RUN_TIMEOUT)
|
|
1707
|
+
});
|
|
1708
|
+
if (!response.ok) {
|
|
1709
|
+
const error = await response.json().catch(() => ({}));
|
|
1710
|
+
throw new MixrPayError(error.error || `Agent run failed: ${response.status}`);
|
|
1711
|
+
}
|
|
1712
|
+
const data = await response.json();
|
|
1713
|
+
if (data.cost?.total_usd > 0) {
|
|
1714
|
+
const payment = {
|
|
1715
|
+
amountUsd: data.cost.total_usd,
|
|
1716
|
+
recipient: "mixrpay-agent-run",
|
|
1717
|
+
txHash: data.tx_hash,
|
|
1718
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1719
|
+
description: `Agent run: ${data.run_id}`,
|
|
1720
|
+
url: `${this.baseUrl}/api/v2/agent/run`
|
|
1721
|
+
};
|
|
1722
|
+
this.payments.push(payment);
|
|
1723
|
+
this.totalSpentUsd += data.cost.total_usd;
|
|
1724
|
+
this.logger.payment(data.cost.total_usd, "agent-run", data.run_id);
|
|
1725
|
+
if (this.onPayment) {
|
|
1726
|
+
this.onPayment(payment);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return {
|
|
1730
|
+
runId: data.run_id,
|
|
1731
|
+
status: data.status,
|
|
1732
|
+
response: data.response,
|
|
1733
|
+
iterations: data.iterations,
|
|
1734
|
+
toolsUsed: data.tools_used,
|
|
1735
|
+
cost: {
|
|
1736
|
+
llmUsd: data.cost.llm_usd,
|
|
1737
|
+
toolsUsd: data.cost.tools_usd,
|
|
1738
|
+
totalUsd: data.cost.total_usd
|
|
1739
|
+
},
|
|
1740
|
+
tokens: data.tokens,
|
|
1741
|
+
sessionRemainingUsd: data.session_remaining_usd,
|
|
1742
|
+
txHash: data.tx_hash
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
return this.runAgentStreaming(sessionId, body, onEvent);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Internal: Handle streaming agent run via SSE
|
|
1749
|
+
*/
|
|
1750
|
+
async runAgentStreaming(sessionId, body, onEvent) {
|
|
1751
|
+
const STREAMING_TIMEOUT = 3e5;
|
|
1752
|
+
const response = await fetch(`${this.baseUrl}/api/v2/agent/run`, {
|
|
1753
|
+
method: "POST",
|
|
1754
|
+
headers: {
|
|
1755
|
+
"Content-Type": "application/json",
|
|
1756
|
+
"X-Mixr-Session": sessionId
|
|
1757
|
+
},
|
|
1758
|
+
body: JSON.stringify(body),
|
|
1759
|
+
signal: AbortSignal.timeout(STREAMING_TIMEOUT)
|
|
1760
|
+
});
|
|
1761
|
+
if (!response.ok) {
|
|
1762
|
+
const error = await response.json().catch(() => ({}));
|
|
1763
|
+
throw new MixrPayError(error.error || `Agent run failed: ${response.status}`);
|
|
1764
|
+
}
|
|
1765
|
+
const reader = response.body?.getReader();
|
|
1766
|
+
if (!reader) {
|
|
1767
|
+
throw new MixrPayError("No response body for streaming");
|
|
1768
|
+
}
|
|
1769
|
+
const decoder = new TextDecoder();
|
|
1770
|
+
let buffer = "";
|
|
1771
|
+
let result = null;
|
|
1772
|
+
while (true) {
|
|
1773
|
+
const { done, value } = await reader.read();
|
|
1774
|
+
if (done) break;
|
|
1775
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1776
|
+
const lines = buffer.split("\n");
|
|
1777
|
+
buffer = lines.pop() || "";
|
|
1778
|
+
let currentEvent = "";
|
|
1779
|
+
for (const line of lines) {
|
|
1780
|
+
if (line.startsWith("event: ")) {
|
|
1781
|
+
currentEvent = line.slice(7).trim();
|
|
1782
|
+
} else if (line.startsWith("data: ") && currentEvent) {
|
|
1783
|
+
try {
|
|
1784
|
+
const data = JSON.parse(line.slice(6));
|
|
1785
|
+
const event = this.parseSSEEvent(currentEvent, data);
|
|
1786
|
+
if (event) {
|
|
1787
|
+
onEvent?.(event);
|
|
1788
|
+
if (currentEvent === "complete") {
|
|
1789
|
+
result = {
|
|
1790
|
+
runId: data.run_id,
|
|
1791
|
+
status: "completed",
|
|
1792
|
+
response: data.response,
|
|
1793
|
+
iterations: data.iterations,
|
|
1794
|
+
toolsUsed: data.tools_used,
|
|
1795
|
+
cost: {
|
|
1796
|
+
llmUsd: 0,
|
|
1797
|
+
// Not provided in streaming complete
|
|
1798
|
+
toolsUsd: 0,
|
|
1799
|
+
totalUsd: data.total_cost_usd
|
|
1800
|
+
},
|
|
1801
|
+
tokens: { prompt: 0, completion: 0 },
|
|
1802
|
+
sessionRemainingUsd: 0,
|
|
1803
|
+
txHash: data.tx_hash
|
|
1804
|
+
};
|
|
1805
|
+
if (data.total_cost_usd > 0) {
|
|
1806
|
+
const payment = {
|
|
1807
|
+
amountUsd: data.total_cost_usd,
|
|
1808
|
+
recipient: "mixrpay-agent-run",
|
|
1809
|
+
txHash: data.tx_hash,
|
|
1810
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1811
|
+
description: `Agent run: ${data.run_id}`,
|
|
1812
|
+
url: `${this.baseUrl}/api/v2/agent/run`
|
|
1813
|
+
};
|
|
1814
|
+
this.payments.push(payment);
|
|
1815
|
+
this.totalSpentUsd += data.total_cost_usd;
|
|
1816
|
+
if (this.onPayment) {
|
|
1817
|
+
this.onPayment(payment);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (currentEvent === "error") {
|
|
1822
|
+
throw new MixrPayError(data.error || "Agent run failed");
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
} catch (e) {
|
|
1826
|
+
if (e instanceof MixrPayError) throw e;
|
|
1827
|
+
this.logger.warn("Failed to parse SSE event:", e);
|
|
1828
|
+
}
|
|
1829
|
+
currentEvent = "";
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!result) {
|
|
1834
|
+
throw new MixrPayError("Agent run completed without final result");
|
|
1835
|
+
}
|
|
1836
|
+
return result;
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Internal: Parse SSE event into typed event object
|
|
1840
|
+
*/
|
|
1841
|
+
parseSSEEvent(eventType, data) {
|
|
1842
|
+
switch (eventType) {
|
|
1843
|
+
case "run_start":
|
|
1844
|
+
return { type: "run_start", runId: data.run_id };
|
|
1845
|
+
case "iteration_start":
|
|
1846
|
+
return { type: "iteration_start", iteration: data.iteration };
|
|
1847
|
+
case "llm_chunk":
|
|
1848
|
+
return { type: "llm_chunk", delta: data.delta };
|
|
1849
|
+
case "tool_call":
|
|
1850
|
+
return { type: "tool_call", tool: data.tool, arguments: data.arguments };
|
|
1851
|
+
case "tool_result":
|
|
1852
|
+
return {
|
|
1853
|
+
type: "tool_result",
|
|
1854
|
+
tool: data.tool,
|
|
1855
|
+
success: data.success,
|
|
1856
|
+
costUsd: data.cost_usd,
|
|
1857
|
+
error: data.error
|
|
1858
|
+
};
|
|
1859
|
+
case "iteration_complete":
|
|
1860
|
+
return {
|
|
1861
|
+
type: "iteration_complete",
|
|
1862
|
+
iteration: data.iteration,
|
|
1863
|
+
tokens: data.tokens,
|
|
1864
|
+
costUsd: data.cost_usd
|
|
1865
|
+
};
|
|
1866
|
+
case "complete":
|
|
1867
|
+
return {
|
|
1868
|
+
type: "complete",
|
|
1869
|
+
runId: data.run_id,
|
|
1870
|
+
response: data.response,
|
|
1871
|
+
totalCostUsd: data.total_cost_usd,
|
|
1872
|
+
txHash: data.tx_hash,
|
|
1873
|
+
iterations: data.iterations,
|
|
1874
|
+
toolsUsed: data.tools_used
|
|
1875
|
+
};
|
|
1876
|
+
case "error":
|
|
1877
|
+
return {
|
|
1878
|
+
type: "error",
|
|
1879
|
+
error: data.error,
|
|
1880
|
+
partialCostUsd: data.partial_cost_usd
|
|
1881
|
+
};
|
|
1882
|
+
default:
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Get the status of an agent run by ID.
|
|
1888
|
+
*
|
|
1889
|
+
* @param runId - The agent run ID
|
|
1890
|
+
* @param sessionId - The session ID (for authentication)
|
|
1891
|
+
* @returns Agent run status and results
|
|
1892
|
+
*
|
|
1893
|
+
* @example
|
|
1894
|
+
* ```typescript
|
|
1895
|
+
* const status = await wallet.getAgentRunStatus('run_abc123', 'sess_xyz789');
|
|
1896
|
+
* console.log(`Status: ${status.status}`);
|
|
1897
|
+
* if (status.status === 'completed') {
|
|
1898
|
+
* console.log(status.response);
|
|
1899
|
+
* }
|
|
1900
|
+
* ```
|
|
1901
|
+
*/
|
|
1902
|
+
async getAgentRunStatus(runId, sessionId) {
|
|
1903
|
+
this.logger.debug("getAgentRunStatus", { runId, sessionId });
|
|
1904
|
+
const response = await fetch(`${this.baseUrl}/api/v2/agent/run/${runId}`, {
|
|
1905
|
+
headers: {
|
|
1906
|
+
"X-Mixr-Session": sessionId
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
if (!response.ok) {
|
|
1910
|
+
const error = await response.json().catch(() => ({}));
|
|
1911
|
+
throw new MixrPayError(error.error || `Failed to get run status: ${response.status}`);
|
|
1912
|
+
}
|
|
1913
|
+
const data = await response.json();
|
|
1914
|
+
return {
|
|
1915
|
+
runId: data.run_id,
|
|
1916
|
+
status: data.status,
|
|
1917
|
+
response: data.response,
|
|
1918
|
+
iterations: data.iterations,
|
|
1919
|
+
toolsUsed: data.tools_used,
|
|
1920
|
+
cost: {
|
|
1921
|
+
llmUsd: data.cost.llm_usd,
|
|
1922
|
+
toolsUsd: data.cost.tools_usd,
|
|
1923
|
+
totalUsd: data.cost.total_usd
|
|
1924
|
+
},
|
|
1925
|
+
tokens: data.tokens,
|
|
1926
|
+
txHash: data.tx_hash,
|
|
1927
|
+
error: data.error,
|
|
1928
|
+
startedAt: new Date(data.started_at),
|
|
1929
|
+
completedAt: data.completed_at ? new Date(data.completed_at) : void 0
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Call an MCP tool using session authorization (pre-authorized spending limit).
|
|
1934
|
+
*
|
|
1935
|
+
* Use this when you've already created a session with the tool provider
|
|
1936
|
+
* and want to use that spending limit instead of direct wallet charges.
|
|
1937
|
+
*
|
|
1938
|
+
* @param sessionId - The session ID for the tool provider
|
|
1939
|
+
* @param toolName - The tool name in format "merchant/tool"
|
|
1940
|
+
* @param args - Arguments to pass to the tool
|
|
1941
|
+
* @returns Tool execution result
|
|
1942
|
+
*
|
|
1943
|
+
* @example
|
|
1944
|
+
* ```typescript
|
|
1945
|
+
* // Create session with provider first
|
|
1946
|
+
* const session = await wallet.getOrCreateSession({
|
|
1947
|
+
* merchantPublicKey: 'pk_live_firecrawl_...',
|
|
1948
|
+
* spendingLimitUsd: 50,
|
|
1949
|
+
* });
|
|
1950
|
+
*
|
|
1951
|
+
* // Use session for multiple calls
|
|
1952
|
+
* const result = await wallet.callMCPToolWithSession(
|
|
1953
|
+
* session.id,
|
|
1954
|
+
* 'firecrawl/scrape',
|
|
1955
|
+
* { url: 'https://example.com' }
|
|
1956
|
+
* );
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
async callMCPToolWithSession(sessionId, toolName, args = {}) {
|
|
1960
|
+
this.logger.debug("callMCPToolWithSession", { sessionId, toolName, args });
|
|
1961
|
+
const response = await fetch(`${this.baseUrl}/api/mcp`, {
|
|
1962
|
+
method: "POST",
|
|
1963
|
+
headers: {
|
|
1964
|
+
"Content-Type": "application/json",
|
|
1965
|
+
"X-Mixr-Session": sessionId
|
|
1966
|
+
},
|
|
1967
|
+
body: JSON.stringify({
|
|
1968
|
+
jsonrpc: "2.0",
|
|
1969
|
+
method: "tools/call",
|
|
1970
|
+
params: { name: toolName, arguments: args },
|
|
1971
|
+
id: Date.now()
|
|
1972
|
+
})
|
|
1973
|
+
});
|
|
1974
|
+
const result = await response.json();
|
|
1975
|
+
if (result.error) {
|
|
1976
|
+
throw new MixrPayError(result.error.message || "MCP tool call failed");
|
|
1977
|
+
}
|
|
1978
|
+
const content = result.result?.content?.[0];
|
|
1979
|
+
const data = content?.text ? JSON.parse(content.text) : null;
|
|
1980
|
+
const mixrpay = result.result?._mixrpay || {};
|
|
1981
|
+
if (mixrpay.chargedUsd) {
|
|
1982
|
+
const payment = {
|
|
1983
|
+
amountUsd: mixrpay.chargedUsd,
|
|
1984
|
+
recipient: toolName.split("/")[0] || toolName,
|
|
1985
|
+
txHash: mixrpay.txHash,
|
|
1986
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1987
|
+
description: `MCP: ${toolName}`,
|
|
1988
|
+
url: `${this.baseUrl}/api/mcp`
|
|
1989
|
+
};
|
|
1990
|
+
this.payments.push(payment);
|
|
1991
|
+
this.totalSpentUsd += mixrpay.chargedUsd;
|
|
1992
|
+
this.logger.payment(mixrpay.chargedUsd, toolName, "MCP call (session)");
|
|
1993
|
+
if (this.onPayment) {
|
|
1994
|
+
this.onPayment(payment);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return {
|
|
1998
|
+
data,
|
|
1999
|
+
chargedUsd: mixrpay.chargedUsd || 0,
|
|
2000
|
+
txHash: mixrpay.txHash,
|
|
2001
|
+
latencyMs: mixrpay.latencyMs
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2006
|
+
0 && (module.exports = {
|
|
2007
|
+
AgentWallet,
|
|
2008
|
+
InsufficientBalanceError,
|
|
2009
|
+
InvalidSessionKeyError,
|
|
2010
|
+
MixrPayError,
|
|
2011
|
+
PaymentFailedError,
|
|
2012
|
+
SDK_VERSION,
|
|
2013
|
+
SessionExpiredError,
|
|
2014
|
+
SessionKeyExpiredError,
|
|
2015
|
+
SessionLimitExceededError,
|
|
2016
|
+
SessionNotFoundError,
|
|
2017
|
+
SessionRevokedError,
|
|
2018
|
+
SpendingLimitExceededError,
|
|
2019
|
+
isMixrPayError
|
|
2020
|
+
});
|