cronos-agent-wallet 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Cronos Merchant SDK
2
+
3
+ The official SDK for building AI agents that can pay for resources on the Cronos blockchain using the x402 protocol.
4
+
5
+ ## 🛡️ Security Features (v1.1.1)
6
+ - **Zero Trust Payer:** Derives identity strictly from chain data.
7
+ - **Strong Replay Protection:** Enforces cryptographic binding of `merchantId + route + nonce`.
8
+ - **Anti-Spoofing:** Validates payment challenges against requested routes before paying.
9
+ - **Fail-Safe:** Supports "Fail Closed" mode for maximum security.
10
+
11
+ Key features:
12
+ * **Automatic 402 Payments**: Handles "Payment Required" challenges seamlessly.
13
+ * **Multi-Chain**: Auto-negotiates with backends (EVM/Solana support).
14
+ * **Safe**: Built-in daily spending limits and policy controls.
15
+ * **Type-Safe**: Generic `fetch` and structured `AgentError` handling.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @cronos-merchant/sdk ethers
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```typescript
26
+ import { AgentClient, AgentError } from "@cronos-merchant/sdk";
27
+
28
+ // 1. Initialize
29
+ const agent = new AgentClient({
30
+ privateKey: process.env.AGENT_KEY,
31
+ rpcUrl: "https://evm-t3.cronos.org", // Cronos Testnet
32
+ chainId: 338,
33
+ usdcAddress: "0xc01..." // Your payment token
34
+ });
35
+
36
+ async function main() {
37
+ try {
38
+ // 2. Fetch paid resources (just like axios/fetch)
39
+ const response = await agent.fetch<{ answer: string }>("http://localhost:3000/premium", {
40
+ method: "POST",
41
+ body: { prompt: "Hello World" }
42
+ });
43
+
44
+ console.log("Success:", response.answer);
45
+
46
+ } catch (err: any) {
47
+ // 3. Handle Errors
48
+ if (err instanceof AgentError) {
49
+ console.error(`Status: ${err.status}`); // 402, 500
50
+ console.error(`Code: ${err.code}`); // POLICY_REJECTED, NETWORK_ERROR
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ `new AgentClient(config)`
59
+
60
+ | Option | Type | Required | Description |
61
+ | :--- | :--- | :--- | :--- |
62
+ | `privateKey` | `string` | Yes | EVM private key for the agent wallet. |
63
+ | `rpcUrl` | `string` | Yes | RPC Endpoint (e.g., Cronos Testnet). |
64
+ | `chainId` | `number` | Yes | Chain ID (e.g., 338). Sent to backend for negotiation. |
65
+ | `usdcAddress` | `string` | Yes | ERC20 Token Address used for payment. |
66
+ | `dailyLimit` | `number` | No | Max USDC allowed to spend per 24h. Default: 1.0 |
67
+ | `allowedMerchants` | `string[]` | No | List of Merchant IDs to trust. If empty, allows all. |
68
+ | `trustedFacilitators` | `string[]` | No | List of Gateway URLs to trust (e.g., localhost). |
69
+
70
+ ## API Reference
71
+
72
+ ### `agent.fetch<T>(url, options): Promise<T>`
73
+
74
+ Performs an HTTP request. If the server responds with **402 Payment Required**:
75
+ 1. SDK parses the payment request (from Headers or Body).
76
+ 2. Checks spending policies (Daily Limit, Whitelist).
77
+ 3. Executes the payment on-chain.
78
+ 4. Retries the original request with the Payment Proof.
79
+
80
+ **Generics**:
81
+ Pass the expected response type `<T>` for TypeScript autocomplete.
82
+
83
+ **Options**:
84
+ * `method`: "GET" | "POST"
85
+ * `headers`: Dictionary of headers.
86
+ * `body`: JSON object or string.
87
+ * `allowBodyFallback`: `boolean`. Defaults to `false`. Set to `true` to allow parsing 402 details from the response body if headers are missing. DANGEROUS: Only use with trusted facilitators.
88
+
89
+ ### `AgentError`
90
+
91
+ Thrown when a request fails or is blocked by policy.
92
+
93
+ * `err.status`: The HTTP status code (e.g., 400, 402, 500).
94
+ * `err.code`: A machine-readable string:
95
+ * `POLICY_REJECTED`: Blocked by daily limit or whitelist.
96
+ * `INSUFFICIENT_FUNDS`: Not enough USDC/Gas.
97
+ * `HTTP_ERROR`: Server returned an error (details in `err.details`).
98
+ * `err.details`: The raw response body from the server.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,46 @@
1
+ import { AgentConfig } from "./config";
2
+ /**
3
+ * AgentClient
4
+ * -----------
5
+ * Public SDK entry.
6
+ * - One instance per user/session
7
+ * - Wraps wallet + executor + x402 flow
8
+ */
9
+ export declare class AgentClient {
10
+ private wallet;
11
+ private context;
12
+ constructor(config: AgentConfig);
13
+ /**
14
+ * agent.fetch()
15
+ * --------------
16
+ * Drop-in replacement for fetch/axios
17
+ * Handles:
18
+ * - free APIs
19
+ * - x402 paid APIs
20
+ * - auto-pay + retry
21
+ */
22
+ fetch<T>(url: string, options?: {
23
+ method?: "GET" | "POST";
24
+ headers?: Record<string, string>;
25
+ body?: any;
26
+ allowBodyFallback?: boolean;
27
+ }): Promise<T>;
28
+ fetchWithDetails<T>(url: string, options?: {
29
+ method?: "GET" | "POST";
30
+ headers?: Record<string, string>;
31
+ body?: any;
32
+ timeoutMs?: number;
33
+ allowBodyFallback?: boolean;
34
+ }): Promise<{
35
+ data: T;
36
+ payment: any;
37
+ }>;
38
+ /**
39
+ * Emergency stop
40
+ */
41
+ stop(): void;
42
+ /**
43
+ * Wallet address (agent wallet)
44
+ */
45
+ getAddress(): string;
46
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentClient = void 0;
4
+ // AgentClient.ts
5
+ const AgentWallet_1 = require("./internal/AgentWallet");
6
+ const CronosUsdcExecutor_1 = require("./internal/CronosUsdcExecutor");
7
+ const x402ToolClient_1 = require("./internal/x402ToolClient");
8
+ /**
9
+ * AgentClient
10
+ * -----------
11
+ * Public SDK entry.
12
+ * - One instance per user/session
13
+ * - Wraps wallet + executor + x402 flow
14
+ */
15
+ class AgentClient {
16
+ wallet;
17
+ context;
18
+ constructor(config) {
19
+ if (!config.privateKey) {
20
+ throw new Error("AgentClient: privateKey is required");
21
+ }
22
+ if (!config.rpcUrl) {
23
+ throw new Error("AgentClient: rpcUrl is required");
24
+ }
25
+ if (!config.chainId) {
26
+ throw new Error("AgentClient: chainId is required");
27
+ }
28
+ // 1. Create executor (chain-specific)
29
+ const executor = new CronosUsdcExecutor_1.CronosUsdcExecutor(config.rpcUrl, config.privateKey, config.usdcAddress, config.chainId);
30
+ // 2. Create wallet (policy + persistence)
31
+ this.wallet = new AgentWallet_1.AgentWallet(executor.getAddress(), executor, {
32
+ dailyLimit: config.dailyLimit,
33
+ maxPerTransaction: config.maxPerTransaction,
34
+ trustedFacilitators: config.trustedFacilitators,
35
+ allowedMerchants: config.allowedMerchants,
36
+ });
37
+ // 3. Context passed per request
38
+ this.context = {
39
+ chainId: config.chainId,
40
+ merchantId: config.merchantId,
41
+ analyticsUrl: config.analyticsUrl
42
+ };
43
+ }
44
+ /**
45
+ * agent.fetch()
46
+ * --------------
47
+ * Drop-in replacement for fetch/axios
48
+ * Handles:
49
+ * - free APIs
50
+ * - x402 paid APIs
51
+ * - auto-pay + retry
52
+ */
53
+ async fetch(url, options) {
54
+ const res = await this.fetchWithDetails(url, options);
55
+ return res.data;
56
+ }
57
+ async fetchWithDetails(url, options) {
58
+ console.log("[SDK] AgentClient.fetch called");
59
+ console.log(`[SDK] URL: ${url}`);
60
+ console.log(`[SDK] Options:`, options);
61
+ return (0, x402ToolClient_1.x402Request)(url, this.wallet, this.context, options);
62
+ }
63
+ /**
64
+ * Emergency stop
65
+ */
66
+ stop() {
67
+ this.wallet.emergencyStop();
68
+ }
69
+ /**
70
+ * Wallet address (agent wallet)
71
+ */
72
+ getAddress() {
73
+ return this.wallet.getAddress();
74
+ }
75
+ }
76
+ exports.AgentClient = AgentClient;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ethers_1 = require("ethers");
4
+ const RPC = "https://evm.cronos.org";
5
+ const ADDR = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
6
+ const USER = "0x2149AbA8305029f8Db6dBC858dadf4112832D0ff";
7
+ async function check() {
8
+ const provider = new ethers_1.ethers.JsonRpcProvider(RPC);
9
+ // Probe userInfo structure and balanceOf (no args)
10
+ const abi = [
11
+ "function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 cakeAtLastUserAction, uint256 lastUserActionTime)", // Standard VVS Vault pattern guess
12
+ "function balanceOf() view returns (uint256)",
13
+ "function getPricePerFullShare() view returns (uint256)"
14
+ ];
15
+ const contract = new ethers_1.ethers.Contract(ADDR, abi, provider);
16
+ try {
17
+ console.log("Calling balanceOf()..."); // Total vault balance
18
+ const bal = await contract.balanceOf();
19
+ console.log("✅ Total Vault Balance:", bal.toString());
20
+ }
21
+ catch (e) {
22
+ console.log("⚠️ balanceOf() failed:", e.reason || e.message);
23
+ }
24
+ try {
25
+ console.log(`Calling userInfo(${USER})...`);
26
+ // We try to fetch it as an array/object to see what comes back
27
+ const info = await contract.userInfo(USER);
28
+ console.log("✅ userInfo result:", info);
29
+ }
30
+ catch (e) {
31
+ console.log("⚠️ userInfo failed:", e.reason || e.message);
32
+ // Fallback: try simpler ABI if the struct guess failed
33
+ try {
34
+ const abiSimple = ["function userInfo(address) view returns (uint256, uint256, uint256, uint256)"];
35
+ const c2 = new ethers_1.ethers.Contract(ADDR, abiSimple, provider);
36
+ const info2 = await c2.userInfo(USER);
37
+ console.log("✅ userInfo (generic) result:", info2);
38
+ }
39
+ catch (ex) {
40
+ console.log("❌ userInfo generic failed:", ex.reason || ex.message);
41
+ }
42
+ }
43
+ }
44
+ check();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ethers_1 = require("ethers");
4
+ const RPC = "https://evm.cronos.org";
5
+ const ADDR = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
6
+ async function check() {
7
+ const provider = new ethers_1.ethers.JsonRpcProvider(RPC);
8
+ console.log(`Checking code at ${ADDR} on ${RPC}...`);
9
+ const code = await provider.getCode(ADDR);
10
+ console.log(`Code length: ${code.length}`);
11
+ if (code === "0x") {
12
+ console.log("❌ No code found (EOA or empty).");
13
+ return;
14
+ }
15
+ console.log("✅ Contract code found.");
16
+ const abi = [
17
+ "function name() view returns (string)",
18
+ "function symbol() view returns (string)",
19
+ "function poolLength() view returns (uint256)",
20
+ "function balanceOf(address) view returns (uint256)"
21
+ ];
22
+ const contract = new ethers_1.ethers.Contract(ADDR, abi, provider);
23
+ try {
24
+ console.log("Probing name()...");
25
+ const name = await contract.name();
26
+ console.log("✅ Name:", name);
27
+ }
28
+ catch (e) {
29
+ console.log("⚠️ name() failed");
30
+ }
31
+ try {
32
+ console.log("Probing poolLength()...");
33
+ const len = await contract.poolLength();
34
+ console.log("✅ poolLength:", len.toString());
35
+ }
36
+ catch (e) {
37
+ console.log("⚠️ poolLength() failed");
38
+ }
39
+ try {
40
+ console.log("Probing balanceOf(random)...");
41
+ const bal = await contract.balanceOf("0x000000000000000000000000000000000000dead");
42
+ console.log("✅ balanceOf:", bal.toString());
43
+ }
44
+ catch (e) {
45
+ console.log("⚠️ balanceOf() failed");
46
+ }
47
+ }
48
+ check();
@@ -0,0 +1,12 @@
1
+ export interface AgentConfig {
2
+ privateKey: string;
3
+ rpcUrl: string;
4
+ chainId: number;
5
+ usdcAddress: string;
6
+ dailyLimit?: number;
7
+ maxPerTransaction?: number;
8
+ allowedMerchants?: string[];
9
+ trustedFacilitators?: string[];
10
+ analyticsUrl?: string;
11
+ merchantId?: string;
12
+ }
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/demo.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "dotenv/config";
package/dist/demo.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // demo.ts
4
+ require("dotenv/config");
5
+ const AgentWallet_1 = require("./internal/AgentWallet");
6
+ const CronosUsdcExecutor_1 = require("./internal/CronosUsdcExecutor");
7
+ const x402ToolClient_1 = require("./internal/x402ToolClient");
8
+ const ethers_1 = require("ethers");
9
+ const VvsYieldExecutor_1 = require("./internal/VvsYieldExecutor");
10
+ async function run() {
11
+ // --------------------------------------------------
12
+ // 1. Merchant API
13
+ // --------------------------------------------------
14
+ const MERCHANT_API_URL = "http://localhost:3001";
15
+ const TARGET_PATH = "/photos";
16
+ const FULL_URL = `${MERCHANT_API_URL}${TARGET_PATH}`;
17
+ // --------------------------------------------------
18
+ // 2. RPCs
19
+ // --------------------------------------------------
20
+ const MAINNET_RPC = "https://evm.cronos.org";
21
+ const TESTNET_RPC = process.env.CRONOS_RPC_URL || "https://evm-t3.cronos.org";
22
+ const USDC_ADDR = process.env.USDC_CONTRACT_ADDRESS ||
23
+ "0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0";
24
+ const PRIVATE_KEY = process.env.AGENT_WALLET_PRIVATE_KEY;
25
+ if (!PRIVATE_KEY)
26
+ throw new Error("Missing AGENT_WALLET_PRIVATE_KEY");
27
+ // --------------------------------------------------
28
+ // 3. Agent (payments)
29
+ // --------------------------------------------------
30
+ const paymentExecutor = new CronosUsdcExecutor_1.CronosUsdcExecutor(TESTNET_RPC, PRIVATE_KEY, USDC_ADDR, 338);
31
+ const wallet = new AgentWallet_1.AgentWallet(new ethers_1.ethers.Wallet(PRIVATE_KEY).address, paymentExecutor);
32
+ // --------------------------------------------------
33
+ // 4. AutoVVS Yield Sensor (Mainnet)
34
+ // --------------------------------------------------
35
+ const provider = new ethers_1.ethers.JsonRpcProvider(MAINNET_RPC);
36
+ const ethersWallet = new ethers_1.ethers.Wallet(PRIVATE_KEY, provider);
37
+ const yieldExec = new VvsYieldExecutor_1.VvsYieldExecutor(provider, ethersWallet);
38
+ console.log("--------------------------------------------------");
39
+ console.log(`🤖 AGENT: ${ethersWallet.address}`);
40
+ console.log("🌱 AutoVVS Vault: read-only position");
41
+ console.log("--------------------------------------------------");
42
+ try {
43
+ const pos = await yieldExec.getVaultPosition();
44
+ console.log("📊 Vault Position (RAW):", {
45
+ shares: pos.shares.toString(),
46
+ pricePerShare: pos.pricePerShare.toString(),
47
+ underlyingValue: pos.underlyingValue.toString()
48
+ });
49
+ console.log("📊 Underlying (formatted, 18d):", ethers_1.ethers.formatUnits(pos.underlyingValue, 18));
50
+ }
51
+ catch (err) {
52
+ console.error("❌ AutoVVS sensor error:", err.message);
53
+ }
54
+ // --------------------------------------------------
55
+ // 5. x402 flow (unchanged)
56
+ // --------------------------------------------------
57
+ console.log(`[x402] Negotiating access for: ${TARGET_PATH}...`);
58
+ try {
59
+ const result = await (0, x402ToolClient_1.x402Request)(FULL_URL, wallet, {
60
+ chainId: 338,
61
+ merchantId: "b9805b9e-fa6c-4640-8470-f5b230dee6d4"
62
+ }, {});
63
+ console.log("✅ Success! Data Received:");
64
+ console.dir(result, { depth: null });
65
+ }
66
+ catch (error) {
67
+ console.error(`❌ Failed: ${error.message}`);
68
+ }
69
+ }
70
+ run();
@@ -0,0 +1,9 @@
1
+ export type AgentErrorCode = "POLICY_REJECTED" | "INSUFFICIENT_FUNDS" | "UNTRUSTED_FACILITATOR" | "DAILY_LIMIT_EXCEEDED" | "NETWORK_ERROR" | "HTTP_ERROR" | "PROTOCOL_ERROR" | "UNKNOWN_ERROR";
2
+ export declare class AgentError extends Error {
3
+ message: string;
4
+ code: AgentErrorCode;
5
+ status?: number | undefined;
6
+ details?: any | undefined;
7
+ constructor(message: string, code?: AgentErrorCode, status?: number | undefined, // HTTP Status (400, 402, 500)
8
+ details?: any | undefined);
9
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ // errors.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.AgentError = void 0;
5
+ class AgentError extends Error {
6
+ message;
7
+ code;
8
+ status;
9
+ details;
10
+ constructor(message, code = "UNKNOWN_ERROR", status, // HTTP Status (400, 402, 500)
11
+ details // Full response body or context
12
+ ) {
13
+ super(message);
14
+ this.message = message;
15
+ this.code = code;
16
+ this.status = status;
17
+ this.details = details;
18
+ this.name = "AgentError";
19
+ }
20
+ }
21
+ exports.AgentError = AgentError;
@@ -0,0 +1,3 @@
1
+ export { AgentClient } from "./AgentClient";
2
+ export type { AgentConfig } from "./config";
3
+ export { AgentError } from "./errors";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentError = exports.AgentClient = void 0;
4
+ var AgentClient_1 = require("./AgentClient");
5
+ Object.defineProperty(exports, "AgentClient", { enumerable: true, get: function () { return AgentClient_1.AgentClient; } });
6
+ var errors_1 = require("./errors");
7
+ Object.defineProperty(exports, "AgentError", { enumerable: true, get: function () { return errors_1.AgentError; } });
@@ -0,0 +1,50 @@
1
+ import { PaymentRequest, WalletContext, AgentWalletConfig } from "./types";
2
+ import { PaymentExecutor } from "./executors";
3
+ /**
4
+ * AgentWallet
5
+ * ------------
6
+ * - Owns identity (address)
7
+ * - Decides whether to pay
8
+ * - Delegates execution to PaymentExecutor
9
+ * - Enforces security + policy
10
+ * - Persists state (Isomorphic: FS or LocalStorage)
11
+ */
12
+ export declare class AgentWallet {
13
+ private readonly address;
14
+ private readonly executor;
15
+ private dailyLimit;
16
+ private maxPerTransaction;
17
+ private spentToday;
18
+ private lastResetDate;
19
+ private allowedMerchants;
20
+ private trustedFacilitatorOrigins;
21
+ private stopped;
22
+ private persistence;
23
+ /**
24
+ * Replay protection
25
+ * key -> timestamp
26
+ */
27
+ private paidRequests;
28
+ constructor(address: string, executor: PaymentExecutor, config?: AgentWalletConfig);
29
+ private getTodayDate;
30
+ private loadState;
31
+ private saveState;
32
+ getAddress(): string;
33
+ private getHeader;
34
+ private canonicalOrigin;
35
+ /**
36
+ * Strong uniqueness:
37
+ * merchant + route + nonce
38
+ */
39
+ private paymentKey;
40
+ parse402Header(headers: any): PaymentRequest;
41
+ parse402Body(body: any, defaultMerchantId: string, requestUrl: string): PaymentRequest;
42
+ emergencyStop(): void;
43
+ shouldPay(request: PaymentRequest, context: WalletContext, originalChallenge?: {
44
+ route?: string;
45
+ }): {
46
+ allow: boolean;
47
+ reason?: string;
48
+ };
49
+ executePayment(request: PaymentRequest): Promise<string>;
50
+ }
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentWallet = void 0;
4
+ class NodePersistence {
5
+ fs;
6
+ path;
7
+ statePath = "";
8
+ constructor() {
9
+ // Dynamic require to avoid bundling issues in browser
10
+ try {
11
+ this.fs = require("fs");
12
+ this.path = require("path");
13
+ this.statePath = this.path.resolve(__dirname, "wallet-state.json");
14
+ }
15
+ catch (e) { /* Browser environment */ }
16
+ }
17
+ load() {
18
+ if (!this.fs || !this.fs.existsSync(this.statePath))
19
+ return null;
20
+ try {
21
+ return JSON.parse(this.fs.readFileSync(this.statePath, "utf-8"));
22
+ }
23
+ catch (e) {
24
+ console.warn("[WALLET] Failed to load local state file", e);
25
+ return null;
26
+ }
27
+ }
28
+ save(data) {
29
+ if (!this.fs)
30
+ return;
31
+ try {
32
+ const tmpPath = this.statePath + ".tmp";
33
+ this.fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
34
+ this.fs.renameSync(tmpPath, this.statePath);
35
+ }
36
+ catch (e) {
37
+ console.error("[WALLET] Failed to save local state file", e);
38
+ }
39
+ }
40
+ }
41
+ class BrowserPersistence {
42
+ key = "cronos_agent_wallet_state";
43
+ load() {
44
+ if (typeof localStorage === "undefined")
45
+ return null;
46
+ const item = localStorage.getItem(this.key);
47
+ return item ? JSON.parse(item) : null;
48
+ }
49
+ save(data) {
50
+ if (typeof localStorage === "undefined")
51
+ return;
52
+ localStorage.setItem(this.key, JSON.stringify(data));
53
+ }
54
+ }
55
+ /**
56
+ * AgentWallet
57
+ * ------------
58
+ * - Owns identity (address)
59
+ * - Decides whether to pay
60
+ * - Delegates execution to PaymentExecutor
61
+ * - Enforces security + policy
62
+ * - Persists state (Isomorphic: FS or LocalStorage)
63
+ */
64
+ class AgentWallet {
65
+ address;
66
+ executor;
67
+ // ---------------- CONFIG ----------------
68
+ // Reduced limit for testing persistence (0.5 USDC)
69
+ dailyLimit = 0.5;
70
+ maxPerTransaction = 0.5; // Default safe limit
71
+ spentToday = 0.0;
72
+ lastResetDate = "";
73
+ allowedMerchants = new Set([]);
74
+ trustedFacilitatorOrigins = new Set([
75
+ "http://localhost:5000",
76
+ "http://localhost:3000",
77
+ "https://agentx402.onrender.com",
78
+ "https://cronos-x-402-production.up.railway.app",
79
+ ]);
80
+ stopped = false;
81
+ persistence;
82
+ /**
83
+ * Replay protection
84
+ * key -> timestamp
85
+ */
86
+ paidRequests = new Map();
87
+ constructor(address, executor, config) {
88
+ this.address = address;
89
+ this.executor = executor;
90
+ if (config) {
91
+ if (config.dailyLimit !== undefined)
92
+ this.dailyLimit = config.dailyLimit;
93
+ if (config.maxPerTransaction !== undefined)
94
+ this.maxPerTransaction = config.maxPerTransaction;
95
+ if (config.allowedMerchants)
96
+ this.allowedMerchants = new Set(config.allowedMerchants);
97
+ if (config.trustedFacilitators)
98
+ this.trustedFacilitatorOrigins = new Set(config.trustedFacilitators);
99
+ }
100
+ // Initialize Persistence based on env
101
+ if (typeof window === "undefined") {
102
+ this.persistence = new NodePersistence();
103
+ }
104
+ else {
105
+ this.persistence = new BrowserPersistence();
106
+ }
107
+ this.loadState();
108
+ }
109
+ // ---------------- PERSISTENCE ----------------
110
+ getTodayDate() {
111
+ return new Date().toISOString().split("T")[0]; // YYYY-MM-DD
112
+ }
113
+ loadState() {
114
+ const state = this.persistence.load();
115
+ if (state) {
116
+ // 1. Check if we need to reset for a new day
117
+ const today = this.getTodayDate();
118
+ if (state.lastResetDate !== today) {
119
+ console.log(`[WALLET] New day detected! Resetting limit. (Last: ${state.lastResetDate}, Today: ${today})`);
120
+ this.spentToday = 0;
121
+ this.lastResetDate = today;
122
+ // Cleanup old requests (older than 24h) to prevent memory leak
123
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
124
+ const now = Date.now();
125
+ this.paidRequests = new Map(state.paidRequests.filter(([_, ts]) => now - ts < MAX_AGE_MS));
126
+ }
127
+ else {
128
+ console.log(`[WALLET] State loaded. Spent today: ${state.spentToday.toFixed(4)} / ${this.dailyLimit}`);
129
+ this.spentToday = state.spentToday;
130
+ this.lastResetDate = state.lastResetDate;
131
+ // Also cleanup on every load to be safe
132
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
133
+ const now = Date.now();
134
+ this.paidRequests = new Map(state.paidRequests.filter(([_, ts]) => now - ts < MAX_AGE_MS));
135
+ }
136
+ }
137
+ else {
138
+ // Initialize fresh
139
+ this.lastResetDate = this.getTodayDate();
140
+ }
141
+ }
142
+ saveState() {
143
+ const state = {
144
+ lastResetDate: this.lastResetDate,
145
+ spentToday: this.spentToday,
146
+ paidRequests: Array.from(this.paidRequests.entries())
147
+ };
148
+ this.persistence.save(state);
149
+ }
150
+ // ---------------- PUBLIC ----------------
151
+ getAddress() {
152
+ return this.address;
153
+ }
154
+ // ---------------- HELPERS ----------------
155
+ getHeader(headers, key) {
156
+ return headers[key] ?? headers[key.toLowerCase()];
157
+ }
158
+ canonicalOrigin(url) {
159
+ try {
160
+ return new URL(url).origin;
161
+ }
162
+ catch {
163
+ return "";
164
+ }
165
+ }
166
+ /**
167
+ * Strong uniqueness:
168
+ * merchant + route + nonce
169
+ */
170
+ paymentKey(req) {
171
+ return `${req.merchantId}:${req.route}:${req.nonce}`;
172
+ }
173
+ // ---------------- x402 PARSER ----------------
174
+ parse402Header(headers) {
175
+ const amount = this.getHeader(headers, "x-payment-amount");
176
+ const currency = this.getHeader(headers, "x-payment-currency");
177
+ const payTo = this.getHeader(headers, "x-payment-payto");
178
+ const merchantId = this.getHeader(headers, "x-merchant-id");
179
+ const facilitatorUrl = this.getHeader(headers, "x-facilitator-url");
180
+ const chainId = this.getHeader(headers, "x-chain-id");
181
+ const route = this.getHeader(headers, "x-route");
182
+ const nonce = this.getHeader(headers, "x-nonce");
183
+ if (!amount ||
184
+ !currency ||
185
+ !payTo ||
186
+ !merchantId ||
187
+ !facilitatorUrl ||
188
+ !chainId ||
189
+ !route ||
190
+ !nonce) {
191
+ throw new Error("Malformed x402 payment headers");
192
+ }
193
+ return {
194
+ amount: Number(amount),
195
+ currency,
196
+ payTo,
197
+ merchantId,
198
+ facilitatorUrl,
199
+ chainId: Number(chainId),
200
+ route,
201
+ nonce,
202
+ };
203
+ }
204
+ parse402Body(body, defaultMerchantId, requestUrl) {
205
+ // Expecting body structure:
206
+ // { paymentRequest: { chainId, currency, receiver, amount, token ... } }
207
+ // Support both wrapped { paymentRequest: ... } and raw { ... } structures
208
+ // Robust strategy: Check if 'paymentRequest' key exists, otherwise assume body IS the request if it has 'amount'
209
+ let req = body?.paymentRequest || body;
210
+ // Double check deep nesting (some frameworks might wrap twice)
211
+ if (req?.paymentRequest)
212
+ req = req.paymentRequest;
213
+ if (!req) {
214
+ throw new Error("Missing 'paymentRequest' in response body");
215
+ }
216
+ const amount = req.amount;
217
+ const currency = req.currency;
218
+ const payTo = req.receiver;
219
+ // Body usually doesn't repeat merchantId
220
+ const merchantId = defaultMerchantId;
221
+ const facilitatorUrl = requestUrl;
222
+ const chainId = req.chainId;
223
+ const route = "premium";
224
+ // [SECURITY] NONCE AUTHORITY
225
+ // Server MUST provide nonce. No fallbacks allowed.
226
+ const nonce = req.nonce;
227
+ if (!nonce) {
228
+ throw new Error("Missing 'nonce' in 402 body - Server is the sole nonce authority.");
229
+ }
230
+ if (!amount || !currency || !payTo || !chainId) {
231
+ throw new Error(`Incomplete 402 body: ${JSON.stringify(req)}`);
232
+ }
233
+ return {
234
+ amount: Number(amount),
235
+ currency,
236
+ payTo,
237
+ merchantId,
238
+ facilitatorUrl, // Contextual
239
+ chainId: Number(chainId),
240
+ route,
241
+ nonce,
242
+ };
243
+ }
244
+ // ---------------- RULES ENGINE ----------------
245
+ emergencyStop() {
246
+ this.stopped = true;
247
+ console.warn("[WALLET] Emergency stop activated!");
248
+ }
249
+ shouldPay(request, context, originalChallenge) {
250
+ // -1. Emergency Stop
251
+ if (this.stopped) {
252
+ return { allow: false, reason: "Emergency stop active" };
253
+ }
254
+ // [SECURITY] Spoofing Check
255
+ // Ensure the route we are paying for matches the invalid route (if known)
256
+ if (originalChallenge?.route && request.route !== originalChallenge.route) {
257
+ return { allow: false, reason: `Route mismatch: Challenge=${request.route}, Expected=${originalChallenge.route}` };
258
+ }
259
+ // 0. Currency check [NEW]
260
+ if (request.currency !== "USDC") {
261
+ return { allow: false, reason: `Unsupported currency: ${request.currency}` };
262
+ }
263
+ // 1. Chain verification
264
+ if (request.chainId !== context.chainId) {
265
+ return { allow: false, reason: "ChainId mismatch" };
266
+ }
267
+ // 2. Facilitator trust
268
+ const origin = this.canonicalOrigin(request.facilitatorUrl);
269
+ if (!this.trustedFacilitatorOrigins.has(origin)) {
270
+ return { allow: false, reason: "Untrusted facilitator" };
271
+ }
272
+ // 3. Merchant allowlist
273
+ // Enabled for safety test
274
+ if (this.allowedMerchants.size > 0 && !this.allowedMerchants.has(request.merchantId)) {
275
+ return { allow: false, reason: `Merchant not allowlisted: ${request.merchantId}` };
276
+ }
277
+ // 4. Daily spend limit
278
+ // Floating point fix: Use a small epsilon or round
279
+ if ((this.spentToday + request.amount) > (this.dailyLimit + 0.0001)) {
280
+ return { allow: false, reason: `Daily limit exceeded (${this.spentToday.toFixed(2)} + ${request.amount} > ${this.dailyLimit})` };
281
+ }
282
+ // 5. Max Per Transaction Check
283
+ if (request.amount > (this.maxPerTransaction + 0.0001)) {
284
+ return { allow: false, reason: `Transaction limit exceeded (${request.amount} > ${this.maxPerTransaction})` };
285
+ }
286
+ return { allow: true };
287
+ }
288
+ // ---------------- PAYMENT EXECUTION ----------------
289
+ async executePayment(request) {
290
+ const key = this.paymentKey(request);
291
+ if (this.paidRequests.has(key)) {
292
+ throw new Error("Replay detected: payment already executed");
293
+ }
294
+ // Execute chain tx
295
+ const proof = await this.executor.execute(request);
296
+ // Update state
297
+ this.spentToday += request.amount;
298
+ this.paidRequests.set(key, Date.now());
299
+ // Persist immediately
300
+ this.saveState();
301
+ return proof;
302
+ }
303
+ }
304
+ exports.AgentWallet = AgentWallet;
@@ -0,0 +1,14 @@
1
+ import "dotenv/config";
2
+ import { PaymentExecutor } from "./executors";
3
+ import { PaymentRequest } from "./types";
4
+ export declare class CronosUsdcExecutor implements PaymentExecutor {
5
+ private provider;
6
+ private wallet;
7
+ private usdc;
8
+ private chainId;
9
+ constructor(rpcUrl: string, privateKey: string, usdcAddress: string, expectedChainId?: number);
10
+ private verifyChain;
11
+ private withRetry;
12
+ execute(request: PaymentRequest): Promise<string>;
13
+ getAddress(): string;
14
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CronosUsdcExecutor = void 0;
4
+ require("dotenv/config");
5
+ const ethers_1 = require("ethers");
6
+ const errors_1 = require("../errors");
7
+ const ERC20_ABI = [
8
+ "function transfer(address to, uint256 amount) returns (bool)",
9
+ "function balanceOf(address owner) view returns (uint256)",
10
+ ];
11
+ const CONFIRMATIONS = 1; // safe for Cronos testnet
12
+ const TX_TIMEOUT_MS = 60_000; // 60s timeout
13
+ class CronosUsdcExecutor {
14
+ provider;
15
+ wallet;
16
+ usdc;
17
+ chainId;
18
+ constructor(rpcUrl, privateKey, usdcAddress, expectedChainId) {
19
+ this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
20
+ const signingKey = new ethers_1.ethers.SigningKey(privateKey);
21
+ this.wallet = new ethers_1.ethers.Wallet(signingKey, this.provider);
22
+ this.usdc = new ethers_1.ethers.Contract(usdcAddress, ERC20_ABI, this.wallet);
23
+ // Optional but recommended
24
+ if (expectedChainId) {
25
+ this.verifyChain(expectedChainId);
26
+ }
27
+ }
28
+ // ---------------- SAFETY ----------------
29
+ async verifyChain(expectedChainId) {
30
+ const network = await this.provider.getNetwork();
31
+ this.chainId = Number(network.chainId);
32
+ if (this.chainId !== expectedChainId) {
33
+ throw new Error(`ChainId mismatch: expected ${expectedChainId}, got ${this.chainId} `);
34
+ }
35
+ console.log(`[CRONOS] Connected to chainId ${this.chainId} `);
36
+ }
37
+ // ---------------- EXECUTION ----------------
38
+ async withRetry(fn, retries = 3) {
39
+ let lastError;
40
+ for (let i = 0; i < retries; i++) {
41
+ try {
42
+ return await fn();
43
+ }
44
+ catch (e) {
45
+ lastError = e;
46
+ // Don't retry revert errors
47
+ if (e.code === 'CALL_EXCEPTION' || e.code === 'INSUFFICIENT_FUNDS')
48
+ throw e;
49
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
50
+ }
51
+ }
52
+ throw lastError;
53
+ }
54
+ async execute(request) {
55
+ const amount = ethers_1.ethers.parseUnits(request.amount.toString(), 6 // USDC decimals
56
+ );
57
+ console.log(`[CRONOS] Paying ${request.amount} USDC -> ${request.payTo}
58
+ merchant = ${request.merchantId}
59
+ route = ${request.route}
60
+ nonce = ${request.nonce} `);
61
+ // 0. Connection Check
62
+ if (!this.chainId)
63
+ await this.verifyChain(Number(request.chainId));
64
+ // 1. Balance check (Retryable)
65
+ // Note: balanceOf returns bigint in v6
66
+ const balance = await this.withRetry(() => this.usdc.balanceOf(this.wallet.address));
67
+ if (balance < amount) {
68
+ throw new Error(`Insufficient USDC balance: have ${ethers_1.ethers.formatUnits(balance, 6)}, need ${request.amount} `);
69
+ }
70
+ // ... inside class ...
71
+ // 1.5 Gas Check (CRO Balance)
72
+ const croBalance = await this.withRetry(() => this.provider.getBalance(this.wallet.address));
73
+ const MIN_GAS = ethers_1.ethers.parseEther("0.1"); // Reserve 0.1 CRO for gas
74
+ if (croBalance < MIN_GAS) {
75
+ throw new errors_1.AgentError(`Insufficient CRO for gas: have ${ethers_1.ethers.formatEther(croBalance)}, need > 0.1`, "INSUFFICIENT_FUNDS");
76
+ }
77
+ // 2. Gas Estimation (Safety)
78
+ try {
79
+ const gasEstimate = await this.withRetry(() => this.usdc.transfer.estimateGas(request.payTo, amount));
80
+ // Optional: Cap gas limit if needed, usually estimate is fine
81
+ }
82
+ catch (e) {
83
+ throw new Error(`Gas estimation failed(likely insufficient funds for gas): ${e.message} `);
84
+ }
85
+ // 3. Send tx
86
+ const tx = await this.withRetry(() => this.usdc.transfer(request.payTo, amount));
87
+ console.log(`[CRONOS] Tx sent: ${tx.hash}. Waiting for confirmation...`);
88
+ // 4. Wait with timeout + confirmations
89
+ const receipt = await Promise.race([
90
+ this.withRetry(() => tx.wait(CONFIRMATIONS)),
91
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction confirmation timeout")), TX_TIMEOUT_MS)),
92
+ ]);
93
+ if (!receipt || receipt.status !== 1) {
94
+ throw new Error("USDC transfer failed (Reverted)");
95
+ }
96
+ console.log(`[CRONOS] Payment confirmed: ${receipt.hash} `);
97
+ return receipt.hash;
98
+ }
99
+ getAddress() {
100
+ return this.wallet.address;
101
+ }
102
+ }
103
+ exports.CronosUsdcExecutor = CronosUsdcExecutor;
@@ -0,0 +1,18 @@
1
+ import { ethers } from "ethers";
2
+ import { YieldExecutor } from "./YieldExecutor";
3
+ export declare class VvsYieldExecutor implements YieldExecutor {
4
+ private readonly provider;
5
+ private readonly wallet;
6
+ private vault;
7
+ constructor(provider: ethers.JsonRpcProvider, wallet: ethers.Wallet, vaultAddress?: string);
8
+ /**
9
+ * READ-ONLY SENSOR (Phase 5A)
10
+ * All values are RAW bigint
11
+ */
12
+ getVaultPosition(): Promise<{
13
+ shares: bigint;
14
+ pricePerShare: bigint;
15
+ underlyingValue: bigint;
16
+ }>;
17
+ harvest(): Promise<number>;
18
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VvsYieldExecutor = void 0;
4
+ // internal/VvsYieldExecutor.ts
5
+ const ethers_1 = require("ethers");
6
+ // Minimal AutoVVS vault ABI (verified)
7
+ const AUTO_VVS_VAULT_ABI = [
8
+ "function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 cakeAtLastUserAction, uint256 lastUserActionTime)",
9
+ "function getPricePerFullShare() view returns (uint256)"
10
+ ];
11
+ // Cronos Mainnet AutoVVS Vault
12
+ const AUTO_VVS_VAULT = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
13
+ class VvsYieldExecutor {
14
+ provider;
15
+ wallet;
16
+ vault;
17
+ constructor(provider, wallet, vaultAddress = AUTO_VVS_VAULT) {
18
+ this.provider = provider;
19
+ this.wallet = wallet;
20
+ this.vault = new ethers_1.ethers.Contract(vaultAddress, AUTO_VVS_VAULT_ABI, this.provider);
21
+ }
22
+ /**
23
+ * READ-ONLY SENSOR (Phase 5A)
24
+ * All values are RAW bigint
25
+ */
26
+ async getVaultPosition() {
27
+ const [userInfo, pricePerShare] = await Promise.all([
28
+ this.vault.userInfo(this.wallet.address),
29
+ this.vault.getPricePerFullShare()
30
+ ]);
31
+ const shares = userInfo[0];
32
+ /**
33
+ * Convention:
34
+ * pricePerShare is scaled by 1e18
35
+ */
36
+ const underlyingValue = (shares * pricePerShare) / 10n ** 18n;
37
+ return {
38
+ shares,
39
+ pricePerShare,
40
+ underlyingValue
41
+ };
42
+ }
43
+ // 🚫 Phase 5A boundary
44
+ async harvest() {
45
+ throw new Error("harvest() not implemented (AutoVVS Phase 5A)");
46
+ }
47
+ }
48
+ exports.VvsYieldExecutor = VvsYieldExecutor;
@@ -0,0 +1,4 @@
1
+ export interface YieldExecutor {
2
+ getVaultPosition(): Promise<any>;
3
+ harvest(): Promise<number>;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ import { PaymentRequest } from "./types";
2
+ export interface PaymentExecutor {
3
+ execute(request: PaymentRequest): Promise<string>;
4
+ }
5
+ export declare class MockPaymentExecutor implements PaymentExecutor {
6
+ execute(request: PaymentRequest): Promise<string>;
7
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MockPaymentExecutor = void 0;
4
+ // Phase 3A — Mock executor
5
+ class MockPaymentExecutor {
6
+ async execute(request) {
7
+ console.log(`[EXECUTOR] Paying ${request.amount} ${request.currency} to ${request.payTo}`);
8
+ return `mock_tx_${Date.now()}`;
9
+ }
10
+ }
11
+ exports.MockPaymentExecutor = MockPaymentExecutor;
@@ -0,0 +1,21 @@
1
+ export interface PaymentRequest {
2
+ amount: number;
3
+ currency: string;
4
+ payTo: string;
5
+ merchantId: string;
6
+ facilitatorUrl: string;
7
+ chainId: number;
8
+ route: string;
9
+ nonce: string;
10
+ }
11
+ export interface WalletContext {
12
+ chainId: number;
13
+ merchantId?: string;
14
+ analyticsUrl?: string;
15
+ }
16
+ export interface AgentWalletConfig {
17
+ dailyLimit?: number;
18
+ maxPerTransaction?: number;
19
+ trustedFacilitators?: string[];
20
+ allowedMerchants?: string[];
21
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ {
2
+ "lastResetDate": "2026-01-12",
3
+ "spentToday": 0.2,
4
+ "paidRequests": [
5
+ [
6
+ "a644d13b-b537-4ad5-bc5a-65705fd3ccec:POST /api/premium:rnvj9m",
7
+ 1768211109096
8
+ ],
9
+ [
10
+ "a644d13b-b537-4ad5-bc5a-65705fd3ccec:POST /api/premium:mnuc9o",
11
+ 1768211999104
12
+ ]
13
+ ]
14
+ }
@@ -0,0 +1,20 @@
1
+ import { AgentWallet } from "./AgentWallet";
2
+ import { WalletContext } from "./types";
3
+ export declare function x402Request(url: string, wallet: AgentWallet, context: WalletContext, options?: {
4
+ method?: "GET" | "POST";
5
+ headers?: Record<string, string>;
6
+ body?: any;
7
+ timeoutMs?: number;
8
+ allowBodyFallback?: boolean;
9
+ }): Promise<{
10
+ data: any;
11
+ payment: null;
12
+ } | {
13
+ data: any;
14
+ payment: {
15
+ txHash: string;
16
+ amount: number;
17
+ currency: string;
18
+ chainId: number;
19
+ };
20
+ }>;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.x402Request = x402Request;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const errors_1 = require("../errors");
9
+ const HTTP_TIMEOUT_MS = 180_000;
10
+ // Helper for fire-and-forget logging
11
+ async function logDecision(context, agentAddress, req, decision, reason, txHash) {
12
+ if (!context.analyticsUrl)
13
+ return;
14
+ // Non-blocking catch
15
+ axios_1.default.post(`${context.analyticsUrl}/api/analytics/log`, {
16
+ agentAddress,
17
+ url: req.facilitatorUrl,
18
+ merchantId: req.merchantId,
19
+ amount: req.amount,
20
+ currency: req.currency,
21
+ decision,
22
+ reason,
23
+ txHash,
24
+ chainId: context.chainId
25
+ }).catch(err => {
26
+ // Silent fail for analytics to not break flow
27
+ });
28
+ }
29
+ async function x402Request(url, wallet, context, options = {}) {
30
+ let paymentAttempted = false; // 🛡️ CRITICAL: Prevents the "Drain Loop"
31
+ // 0. Version Check
32
+ console.log("[SDK] v1.0.4 - ROBUST MODE");
33
+ // Normalize inputs to support both nested headers and flat structure
34
+ const { method, body, headers, ...rest } = options;
35
+ // MERGE both sources: flat properties (rest) and nested headers
36
+ const effectiveHeaders = { ...rest, ...(headers || {}) };
37
+ try {
38
+ // 1. Initial attempt to fetch data
39
+ const res = await (0, axios_1.default)({
40
+ url,
41
+ method: method ?? "GET",
42
+ data: body,
43
+ headers: {
44
+ ...effectiveHeaders,
45
+ "x-merchant-id": context.merchantId || "",
46
+ "x-chain-id": context.chainId.toString(),
47
+ },
48
+ timeout: options.timeoutMs ?? HTTP_TIMEOUT_MS,
49
+ validateStatus: (status) => status === 200 || status === 402,
50
+ });
51
+ if (res.status === 200) {
52
+ return { data: res.data, payment: null };
53
+ }
54
+ // 2. 402 Flow
55
+ console.log("[x402] Payment Required Challenge received");
56
+ console.log("[SDK] Response Headers:", JSON.stringify(res.headers, null, 2));
57
+ console.log("[SDK] Response Body:", JSON.stringify(res.data, null, 2));
58
+ if (paymentAttempted) {
59
+ // If the server asks for pay again immediately after we just paid, something is wrong.
60
+ throw new Error("Recursive 402 detected: Server refused proof or loop occurred.");
61
+ }
62
+ let paymentRequest;
63
+ try {
64
+ paymentRequest = wallet.parse402Header(res.headers);
65
+ }
66
+ catch (headerErr) {
67
+ // [SECURITY] Explicit Opt-In for Body Fallback
68
+ if (!options.allowBodyFallback) {
69
+ throw new Error("402 Header Parsing Failed and allowBodyFallback is disabled. Server must provide X-Payment headers.");
70
+ }
71
+ console.warn("[SDK] parsing headers failed, trying body...", headerErr);
72
+ try {
73
+ paymentRequest = wallet.parse402Body(res.data, context.merchantId || "unknown", url);
74
+ }
75
+ catch (bodyErr) {
76
+ throw new Error(`Failed to parse 402 challenge from Headers OR Body. Header error: ${headerErr}. Body error: ${bodyErr}`);
77
+ }
78
+ }
79
+ const decision = wallet.shouldPay(paymentRequest, context, {
80
+ route: wallet.parse402Header(res.headers).route // Validate against what the server claimed in headers
81
+ });
82
+ const agentAddress = wallet.getAddress();
83
+ if (!decision.allow) {
84
+ logDecision(context, agentAddress, paymentRequest, "BLOCKED", decision.reason);
85
+ throw new Error(`Agent Policy Rejection: ${decision.reason}`);
86
+ }
87
+ // Mark as attempted BEFORE the async call
88
+ paymentAttempted = true;
89
+ // 3. Execute Payment (Authorized)
90
+ console.log(`[x402] Payment Approved. Executing on Chain ${context.chainId}...`);
91
+ let txHash;
92
+ try {
93
+ txHash = await wallet.executePayment(paymentRequest);
94
+ }
95
+ catch (execErr) {
96
+ logDecision(context, agentAddress, paymentRequest, "BLOCKED", `Execution Failed: ${execErr.message}`);
97
+ throw execErr;
98
+ }
99
+ logDecision(context, agentAddress, paymentRequest, "APPROVED", "Paid", txHash);
100
+ // 4. Retry Request with Payment Proof
101
+ console.log(`[x402] Payment Successful (${txHash}). Retrying request...`);
102
+ // [SECURITY] Protocol Alignment
103
+ // Must send: x-payment-proof (txHash) AND x-nonce (to verify binding)
104
+ const retryHeaders = {
105
+ ...effectiveHeaders,
106
+ "X-Merchant-ID": context.merchantId || "",
107
+ "X-Chain-ID": context.chainId.toString(),
108
+ "X-Payment-Proof": txHash, // Canonical
109
+ "X-Nonce": paymentRequest.nonce // Canonical
110
+ };
111
+ const retryRes = await (0, axios_1.default)({
112
+ url,
113
+ method: method ?? "GET",
114
+ data: {
115
+ ...(body || {}), // Preserve original body
116
+ nonce: paymentRequest.nonce // [CRITICAL] Send nonce in body for ReplayKey
117
+ },
118
+ headers: retryHeaders,
119
+ timeout: options.timeoutMs ?? HTTP_TIMEOUT_MS,
120
+ });
121
+ return {
122
+ data: retryRes.data,
123
+ payment: {
124
+ txHash,
125
+ amount: paymentRequest.amount,
126
+ currency: paymentRequest.currency,
127
+ chainId: paymentRequest.chainId // Normalized property name
128
+ }
129
+ };
130
+ }
131
+ catch (error) {
132
+ // Enhanced error logging
133
+ if (error.response?.status === 402) {
134
+ // This happens if the proof was rejected (REPLAY_DETECTED, etc)
135
+ console.error("[SDK] Payment Proof Rejected by Server:", error.response.data);
136
+ throw new Error(`Use Access Denied: ${error.response.data.error || "UNKNOWN"} - ${error.response.data.message || "Proof rejected"}`);
137
+ }
138
+ // Continue to standard error handling
139
+ if (error.name === "AgentError")
140
+ throw error; // Already wrapped
141
+ const status = error.response?.status;
142
+ const errorData = error.response?.data || error.message;
143
+ // Log minimal info, let the caller handle the details
144
+ console.warn(`[x402] Request Failed: ${status || "Network"} - ${error.message}`);
145
+ throw new errors_1.AgentError(`Request failed with status code ${status || "unknown"}: ${JSON.stringify(errorData)}`, status ? "HTTP_ERROR" : "NETWORK_ERROR", status, errorData);
146
+ }
147
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "cronos-agent-wallet",
3
+ "version": "1.2.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "dependencies": {
14
+ "axios": "^1.13.2",
15
+ "dotenv": "^17.2.3",
16
+ "ethers": "^6.0.0",
17
+ "mongoose": "^9.1.1"
18
+ },
19
+ "peerDependencies": {
20
+ "ethers": "^6.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/mocha": "^10.0.10",
24
+ "@types/node": "^25.0.6",
25
+ "@types/uuid": "^10.0.0",
26
+ "ts-node": "^10.9.2",
27
+ "typescript": "^5.5.3"
28
+ }
29
+ }