@goplausible/algorand-mcp 4.2.7 → 4.2.9
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 +141 -40
- package/dist/index.js +7 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/utilityManager.js +1 -1
- package/dist/tools/x402Manager.d.ts +25 -0
- package/dist/tools/x402Manager.js +322 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
-
|
|
16
|
+
- Agent wallet — mnemonics stored in a local SQLite database, used by the MCP server to sign on the agent's behalf (mnemonics never returned in tool responses)
|
|
17
17
|
- Wallet accounts with human-readable nicknames
|
|
18
18
|
- Account creation, key management, and rekeying
|
|
19
19
|
- Transaction building, signing, and submission (payments, assets, applications, key registration)
|
|
@@ -21,7 +21,8 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
21
21
|
- TEAL compilation and disassembly
|
|
22
22
|
- Full Algod and Indexer API access
|
|
23
23
|
- NFDomains (NFD) name service integration
|
|
24
|
-
- x402 and
|
|
24
|
+
- x402 HTTP micropayments — automatic discovery and one-call paid requests using the active wallet (USDC/ALGO)
|
|
25
|
+
- AP2 tooling for Algorand
|
|
25
26
|
- Tinyman AMM integration (pools, swaps, liquidity)
|
|
26
27
|
- Haystack Router DEX aggregation (best-price swaps across Tinyman, Pact, Folks)
|
|
27
28
|
- Alpha Arcade prediction market trading (browse markets, orderbooks, limit/market orders, positions, claims)
|
|
@@ -267,57 +268,148 @@ If no `network` is provided, tools default to **mainnet**.
|
|
|
267
268
|
|
|
268
269
|
API responses are automatically paginated. Every tool accepts an optional `itemsPerPage` parameter (default: 10). Pass the `pageToken` from a previous response to fetch the next page.
|
|
269
270
|
|
|
270
|
-
##
|
|
271
|
+
## Agent Wallet
|
|
271
272
|
|
|
272
273
|
### Architecture
|
|
273
274
|
|
|
274
|
-
The wallet
|
|
275
|
+
The agent wallet is a local SQLite database that the MCP server controls on the agent's behalf. The server holds the mnemonics and signs transactions for the agent — the agent never sees the mnemonics in any tool response.
|
|
275
276
|
|
|
276
|
-
| Layer | What it stores | Where |
|
|
277
|
-
|
|
278
|
-
| **
|
|
279
|
-
| **Embedded SQLite** | Account metadata (nicknames, active-account index) | `~/.algorand-mcp/wallet.db` | Plaintext (no secrets) |
|
|
277
|
+
| Layer | What it stores | Where |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| **SQLite (`wallet.db`)** | Account rows (`address`, `public_key`, `nickname`, `mnemonic`, `created_at`) and the active-account index | `~/.algorand-mcp/wallet.db` (mode `0600`) |
|
|
280
280
|
|
|
281
|
-
**
|
|
281
|
+
**Threat model.** The wallet.db file is the secret. Anyone with read access to it can recover every mnemonic stored in the wallet. The mitigations are filesystem permissions (`0600`, owner-only), keeping the data directory off shared/world-readable volumes, and treating the data directory like any other secret store (snapshot it carefully, restrict backups, encrypt the host disk for at-rest protection). For Docker deployments, mount `~/.algorand-mcp` as a named volume and restrict access to it like you would any secret material.
|
|
282
282
|
|
|
283
283
|
### How it works
|
|
284
284
|
|
|
285
285
|
```
|
|
286
|
-
Agent (LLM) MCP Server
|
|
287
|
-
────────── ──────────
|
|
288
|
-
│ │
|
|
289
|
-
│ wallet_add_account │
|
|
290
|
-
│ { nickname: "main" } │
|
|
291
|
-
│ ──────────────────────────► │ generate keypair
|
|
292
|
-
│ │
|
|
293
|
-
│ │
|
|
294
|
-
│ ◄─ { address, publicKey
|
|
295
|
-
│
|
|
296
|
-
│
|
|
297
|
-
│
|
|
298
|
-
│
|
|
299
|
-
│
|
|
300
|
-
│
|
|
301
|
-
│ │
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
286
|
+
Agent (LLM) MCP Server Storage
|
|
287
|
+
────────── ────────── ───────
|
|
288
|
+
│ │ │
|
|
289
|
+
│ wallet_add_account │ │
|
|
290
|
+
│ { nickname: "main" } │ │
|
|
291
|
+
│ ──────────────────────────► │ generate keypair │
|
|
292
|
+
│ │ INSERT (address, public_key, │
|
|
293
|
+
│ │ nickname, mnemonic) ──►│ wallet.db
|
|
294
|
+
│ ◄─ { address, publicKey, │ │
|
|
295
|
+
│ nickname, index } │ │
|
|
296
|
+
│ │ │
|
|
297
|
+
│ wallet_sign_transaction │ │
|
|
298
|
+
│ { transaction: {...} } │ │
|
|
299
|
+
│ ──────────────────────────► │ SELECT mnemonic FROM accounts ◄─│
|
|
300
|
+
│ │ WHERE address=<active> │
|
|
301
|
+
│ │ sign in memory │
|
|
302
|
+
│ ◄─ { txID, blob } │ (key discarded after sign) │
|
|
303
|
+
│ │ │
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
1. **Account creation** (`wallet_add_account`) — Generates a keypair and inserts a row containing the mnemonic into `accounts`. Returns address, public key, nickname, and index. The mnemonic is never returned.
|
|
305
307
|
2. **Active account** — One account is active at a time. `wallet_switch_account` changes it by nickname or index. All signing and query tools operate on the active account.
|
|
306
|
-
3. **Transaction signing** (`wallet_sign_transaction`) —
|
|
308
|
+
3. **Transaction signing** (`wallet_sign_transaction`) — Reads the mnemonic from the DB, signs in memory, returns only the signed blob.
|
|
307
309
|
4. **Data signing** (`wallet_sign_data`) — Signs arbitrary hex data using raw Ed25519 via the [`@noble/curves`](https://github.com/paulmillr/noble-curves) library (no Algorand SDK prefix). Useful for off-chain authentication.
|
|
308
310
|
5. **Asset opt-in** (`wallet_optin_asset`) — Creates, signs, and submits an opt-in transaction for the active account in one step.
|
|
309
311
|
|
|
310
|
-
###
|
|
312
|
+
### Backward compatibility (silent migration from OS keychain)
|
|
311
313
|
|
|
312
|
-
|
|
314
|
+
Older installs of this MCP stored mnemonics in the OS keychain (`@napi-rs/keyring`). On first startup after upgrading, the server runs a one-shot, silent migration:
|
|
313
315
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
- For every `accounts` row whose `mnemonic` column is `NULL` or empty, it attempts to read the mnemonic from the OS keychain under the service name `algorand-mcp` keyed by the address.
|
|
317
|
+
- If found, the mnemonic is copied into the DB column.
|
|
318
|
+
- The original keychain entry is left in place as a redundant backup; nothing is deleted.
|
|
319
|
+
|
|
320
|
+
After this completes, the DB is the sole source of truth. The keychain is consulted only as a fallback if the DB still has a `NULL` mnemonic for an address (e.g., the keychain was unavailable during startup and became available later). All new accounts created after the upgrade are written directly to the DB and never touch the keychain.
|
|
321
|
+
|
|
322
|
+
No user action is required. No prompts, no env vars, no migration logs.
|
|
323
|
+
|
|
324
|
+
## x402 HTTP Payments
|
|
325
|
+
|
|
326
|
+
[x402](https://x402.org) is an HTTP-native micropayments protocol. It uses the long-reserved `402 Payment Required` status as a real handshake: when a client requests a paid resource without paying, the server returns 402 with a JSON body listing what it accepts (networks, assets, amounts, the recipient address). The client constructs a payment, attaches it as an HTTP header, retries the same request, and the server returns 200 with the resource. No API keys, no Stripe webhooks, no accounts to manage — payment is part of the request itself.
|
|
327
|
+
|
|
328
|
+
This MCP implements the Algorand flavor of x402, where payments are USDC (or native ALGO) transfers on Algorand. It exposes two tools that collapse the seven-step manual flow (probe → parse → opt-in check → build fee payer → build payment → group → sign → encode → header → retry) into a single tool call.
|
|
329
|
+
|
|
330
|
+
### Protocol shape (Algorand variant)
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
Agent (LLM) algorand-mcp Endpoint Facilitator
|
|
334
|
+
────────── ──────────── ──────── ───────────
|
|
335
|
+
│ │ │ │
|
|
336
|
+
│ make_http_request_ │ │ │
|
|
337
|
+
│ with_x402 { url, ... } │ │ │
|
|
338
|
+
│ ────────────────────────► │ HTTP request │ │
|
|
339
|
+
│ │ ─────────────────────────► │ │
|
|
340
|
+
│ │ 402 PaymentRequired │ │
|
|
341
|
+
│ │ ◄───────────────────────── │ │
|
|
342
|
+
│ │ pick accepts[i] for │ │
|
|
343
|
+
│ │ Algorand network │ │
|
|
344
|
+
│ │ build fee-payer + payment │ │
|
|
345
|
+
│ │ (atomic group of 2) │ │
|
|
346
|
+
│ │ sign payment leg │ │
|
|
347
|
+
│ │ via agent wallet DB │ │
|
|
348
|
+
│ │ encode unsigned fee-payer │ │
|
|
349
|
+
│ │ base64 PAYMENT-SIGNATURE │ │
|
|
350
|
+
│ │ ─────────────────────────► │ │
|
|
351
|
+
│ │ HTTP request + │ forward + settle │
|
|
352
|
+
│ │ PAYMENT-SIGNATURE │ ───────────────────────► │
|
|
353
|
+
│ │ │ sign fee-payer, │
|
|
354
|
+
│ │ │ submit atomic group │
|
|
355
|
+
│ │ 200 + resource │ ◄─────────────────────── │
|
|
356
|
+
│ │ ◄───────────────────────── │ │
|
|
357
|
+
│ ◄─ { result, paid: {...}}│ │ │
|
|
358
|
+
│ │ │ │
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### What's different from the Coinbase/EVM version
|
|
362
|
+
|
|
363
|
+
1. **Header name is `PAYMENT-SIGNATURE`**, not `X-PAYMENT`. The header body is base64-encoded JSON with `x402Version`, `scheme`, `network` (a CAIP-2 identifier like `algorand:wGHE2Pw…` for mainnet), a `payload`, and a verbatim copy of the `accepts[]` entry the client chose.
|
|
364
|
+
2. **Payment is an atomic 2-transaction group.** Index 0 is a fee-payer transaction (sender = facilitator, amount = 0, fee = 2000 µAlgo for the whole group); index 1 is the actual USDC ASA transfer (sender = wallet, fee = 0). The wallet signs only index 1 — the facilitator signs index 0 server-side at settlement. The user's wallet pays only the USDC, not even network fees.
|
|
365
|
+
3. **Network strings are Algorand CAIP-2.** This MCP recognizes mainnet (`wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=`) and testnet (`SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=`). Endpoints that only accept Base, Solana, or other non-Algorand networks are not satisfiable here and the tool returns a clear error.
|
|
366
|
+
|
|
367
|
+
### Coinbase Wallet MCP compatibility (x402 surface)
|
|
368
|
+
|
|
369
|
+
The x402 tools — `make_http_request_with_x402` and `x402_discover_payment_requirements` — are intentionally name- and shape-compatible with the Coinbase Wallet MCP's x402 tools. The input parameters (`baseURL`, `path`, `method`, `queryParams`, `body`, `headers`, `correlationId`, `maxAmountPerRequest`, `paymentRequirements`, `preferredNetwork`, `extensions`) are the same. The output envelope (`result`, `_atomicUnitsNote`) is the same.
|
|
319
370
|
|
|
320
|
-
|
|
371
|
+
What this means in practice:
|
|
372
|
+
- **Drop-in for Algorand x402.** Agents and MCP apps written against the Coinbase Wallet MCP's x402 tools work against this server without any prompt changes — they just hit Algorand x402 endpoints instead of Base/Solana ones.
|
|
373
|
+
- **Same agent reflexes.** Models trained on tool-call traces from the Coinbase ecosystem use these tools correctly on first call. Nothing to relearn.
|
|
374
|
+
- **Compatibility is scoped to the x402 surface only.** The wallet, account, transaction-building, and DEX tools in this MCP are Algorand-specific and do not mirror Coinbase's wallet API. Only `make_http_request_with_x402` and `x402_discover_payment_requirements` are drop-in compatible.
|
|
375
|
+
|
|
376
|
+
The one parameter that necessarily differs: `preferredNetwork` accepts `mainnet | testnet | localnet` only (Algorand networks), because the wallet only signs Algorand transactions. Coinbase's enum lists `base | base-sepolia | solana | solana-devnet`. Agents that pass one of those values get a clear error indicating no Algorand-payable accepts entry exists.
|
|
377
|
+
|
|
378
|
+
### Example — paid weather API
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
# Step 1 (optional): peek at the cost
|
|
382
|
+
x402_discover_payment_requirements {
|
|
383
|
+
"baseURL": "https://example.x402.goplausible.xyz",
|
|
384
|
+
"path": "/weather",
|
|
385
|
+
"method": "GET"
|
|
386
|
+
}
|
|
387
|
+
# returns: { result: { accepts: [{ scheme: "exact", network: "algorand:SGO1...",
|
|
388
|
+
# maxAmountRequired: "100", asset: "10458941",
|
|
389
|
+
# payTo: "AAAA...", extra: { feePayer: "BBBB..." } }] } }
|
|
390
|
+
|
|
391
|
+
# Step 2: pay and fetch in one call
|
|
392
|
+
make_http_request_with_x402 {
|
|
393
|
+
"baseURL": "https://example.x402.goplausible.xyz",
|
|
394
|
+
"path": "/weather",
|
|
395
|
+
"method": "GET",
|
|
396
|
+
"maxAmountPerRequest": 10000,
|
|
397
|
+
"preferredNetwork": "testnet"
|
|
398
|
+
}
|
|
399
|
+
# returns: { result: <weather payload>, paid: { network: "testnet",
|
|
400
|
+
# asset: "10458941",
|
|
401
|
+
# amount: "100", payTo: "AAAA..." },
|
|
402
|
+
# paymentResponse: <decoded X-PAYMENT-RESPONSE> }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The active wallet account must be opted into the target ASA (e.g. USDC) and hold enough balance to cover `maxAmountRequired`. If it isn't opted in, the payment fails at settlement — opt in first with `wallet_optin_asset`.
|
|
406
|
+
|
|
407
|
+
### Prerequisites
|
|
408
|
+
|
|
409
|
+
- An active wallet account exists (`wallet_get_info` to verify)
|
|
410
|
+
- That account is opted into the payment asset (USDC mainnet ASA `31566704`, testnet ASA `10458941`)
|
|
411
|
+
- The account has enough of the payment asset for `maxAmountRequired`
|
|
412
|
+
- The endpoint's `accepts[]` includes at least one entry with an Algorand network the MCP recognizes
|
|
321
413
|
|
|
322
414
|
## Optional Environment Variables
|
|
323
415
|
|
|
@@ -361,13 +453,22 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
361
453
|
| `wallet_remove_account` | Remove an account from the wallet by nickname or index |
|
|
362
454
|
| `wallet_list_accounts` | List all accounts with nicknames and addresses |
|
|
363
455
|
| `wallet_switch_account` | Switch the active account by nickname or index |
|
|
364
|
-
| `wallet_get_info` | Get info for the **active account this MCP server owns** (
|
|
456
|
+
| `wallet_get_info` | Get info for the **active account this MCP server owns** (DB-backed): address, public key, balance, opted-in counts. For arbitrary on-chain accounts use `api_algod_get_account_info`. |
|
|
365
457
|
| `wallet_get_assets` | Get all ASA holdings for the **active account this MCP server owns**. For arbitrary on-chain accounts use `api_algod_get_account_info` or `api_algod_get_account_asset_info`. |
|
|
366
458
|
| `wallet_sign_transaction` | Sign a single transaction with the active account |
|
|
367
459
|
| `wallet_sign_transaction_group` | Sign a group of transactions with the active account (auto-assigns group ID) |
|
|
368
460
|
| `wallet_sign_data` | Sign arbitrary hex data with raw Ed25519 (noble, no SDK prefix) |
|
|
369
461
|
| `wallet_optin_asset` | Opt the active account into an asset (creates, signs, and submits) |
|
|
370
462
|
|
|
463
|
+
### x402 HTTP Payment Tools (2 tools)
|
|
464
|
+
|
|
465
|
+
See [x402 HTTP Payments](#x402-http-payments) for the full protocol explanation.
|
|
466
|
+
|
|
467
|
+
| Tool | Description |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `x402_discover_payment_requirements` | Probe an x402-protected endpoint and return its `accepts[]` array (cost, asset, network, payTo) without paying. Read-only. |
|
|
470
|
+
| `make_http_request_with_x402` | Call an x402-protected endpoint with automatic USDC/ALGO payment from the active wallet. Discovers internally if `paymentRequirements` is not supplied, builds the atomic fee-payer + payment group, signs, and retries with the `PAYMENT-SIGNATURE` header. |
|
|
471
|
+
|
|
371
472
|
### Account Management (8 tools)
|
|
372
473
|
|
|
373
474
|
| Tool | Description |
|
|
@@ -587,7 +688,7 @@ algorand-mcp/
|
|
|
587
688
|
│ │ └── wallet/ # Wallet resources
|
|
588
689
|
│ ├── tools/ # MCP Tools
|
|
589
690
|
│ │ ├── commonParams.ts # Network + pagination schema fragments
|
|
590
|
-
│ │ ├── walletManager.ts #
|
|
691
|
+
│ │ ├── walletManager.ts # Agent wallet (SQLite-backed)
|
|
591
692
|
│ │ ├── accountManager.ts # Account operations
|
|
592
693
|
│ │ ├── utilityManager.ts # Utility functions
|
|
593
694
|
│ │ ├── algodManager.ts # TEAL compile, simulate, submit
|
|
@@ -867,7 +968,7 @@ describeIf(testConfig.isCategoryEnabled('your-category'))('Your Tools (E2E)', ()
|
|
|
867
968
|
|
|
868
969
|
- [algosdk](https://github.com/algorand/js-algorand-sdk) v3 — Algorand JavaScript SDK
|
|
869
970
|
- [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) — MCP TypeScript SDK
|
|
870
|
-
- [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager)
|
|
971
|
+
- [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager). Used as a backward-compatibility read fallback for accounts created by pre-DB-migration installs; new mnemonics are written only to `wallet.db`.
|
|
871
972
|
- [sql.js](https://github.com/sql-js/sql.js) — Embedded SQLite (WASM) for wallet metadata persistence
|
|
872
973
|
- [@noble/curves](https://github.com/paulmillr/noble-curves) — Pure JS Ed25519 for raw data signing (`wallet_sign_data`)
|
|
873
974
|
- [@tinymanorg/tinyman-js-sdk](https://github.com/tinymanorg/tinyman-js-sdk) — Tinyman AMM SDK
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ BigInt.prototype.toJSON = function () { return Number(this); };
|
|
|
4
4
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, PingRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
-
import { AccountManager, WalletManager, UtilityManager, TransactionManager, AlgodManager, transactionTools, apiManager, handleApiManager, arc26Manager, KnowledgeManager } from './tools/index.js';
|
|
7
|
+
import { AccountManager, WalletManager, X402Manager, UtilityManager, TransactionManager, AlgodManager, transactionTools, apiManager, handleApiManager, arc26Manager, KnowledgeManager } from './tools/index.js';
|
|
8
8
|
import { ResourceManager } from './resources/index.js';
|
|
9
9
|
class AlgorandMcpServer {
|
|
10
10
|
constructor(name = 'algorand-mcp-server', version = '2.7.5') {
|
|
@@ -72,6 +72,8 @@ class AlgorandMcpServer {
|
|
|
72
72
|
...AccountManager.accountTools,
|
|
73
73
|
// Wallet Tools
|
|
74
74
|
...WalletManager.walletTools,
|
|
75
|
+
// x402 Tools
|
|
76
|
+
...X402Manager.x402Tools,
|
|
75
77
|
// Utility Tools
|
|
76
78
|
...UtilityManager.utilityTools,
|
|
77
79
|
// Algod Tools
|
|
@@ -93,6 +95,10 @@ class AlgorandMcpServer {
|
|
|
93
95
|
if (name.startsWith('wallet_')) {
|
|
94
96
|
return WalletManager.handleTool(name, args);
|
|
95
97
|
}
|
|
98
|
+
// Handle x402 tools
|
|
99
|
+
if (name === 'make_http_request_with_x402' || name === 'x402_discover_payment_requirements') {
|
|
100
|
+
return X402Manager.handleTool(name, args);
|
|
101
|
+
}
|
|
96
102
|
// Handle account tools
|
|
97
103
|
if (name.startsWith('create_account') ||
|
|
98
104
|
name.startsWith('rekey_account') ||
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AccountManager } from './accountManager.js';
|
|
2
2
|
export { WalletManager } from './walletManager.js';
|
|
3
|
+
export { X402Manager } from './x402Manager.js';
|
|
3
4
|
export { KnowledgeManager } from './knowledgeManager.js';
|
|
4
5
|
export { arc26Manager } from './arc26Manager.js';
|
|
5
6
|
export { UtilityManager } from './utilityManager.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AccountManager } from './accountManager.js';
|
|
2
2
|
export { WalletManager } from './walletManager.js';
|
|
3
|
+
export { X402Manager } from './x402Manager.js';
|
|
3
4
|
export { KnowledgeManager } from './knowledgeManager.js';
|
|
4
5
|
export { arc26Manager } from './arc26Manager.js';
|
|
5
6
|
export { UtilityManager } from './utilityManager.js';
|
|
@@ -86,7 +86,7 @@ export class UtilityManager {
|
|
|
86
86
|
type: 'text',
|
|
87
87
|
text: JSON.stringify({
|
|
88
88
|
name: 'Algorand MCP Server',
|
|
89
|
-
version: '4.2.
|
|
89
|
+
version: '4.2.8',
|
|
90
90
|
builder: 'GoPlausible',
|
|
91
91
|
description: 'A Model Context Protocol (MCP) server providing comprehensive access to the Algorand blockchain. Supports account management, transaction building and signing, smart contract interaction, asset operations, ARC-26 URI generation, and deep integration with Algorand ecosystem services including NFDomains, Tinyman, Vestige, and Ultrade.',
|
|
92
92
|
blockchain: 'Algorand — a carbon-negative, pure proof-of-stake Layer 1 blockchain delivering instant finality, low fees, and advanced smart contract capabilities via AVM (Algorand Virtual Machine).',
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class X402Manager {
|
|
2
|
+
static readonly x402Tools: {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: any;
|
|
6
|
+
}[];
|
|
7
|
+
static handleTool(name: string, args: Record<string, unknown>): Promise<{
|
|
8
|
+
content: {
|
|
9
|
+
type: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
}>;
|
|
13
|
+
private static handleDiscover;
|
|
14
|
+
private static handlePaidRequest;
|
|
15
|
+
private static discover;
|
|
16
|
+
private static selectRequirement;
|
|
17
|
+
private static buildPaymentHeader;
|
|
18
|
+
private static httpRequest;
|
|
19
|
+
private static buildUrl;
|
|
20
|
+
private static requireHttpArgs;
|
|
21
|
+
private static tryParseJson;
|
|
22
|
+
private static decodeBase64Json;
|
|
23
|
+
private static snippet;
|
|
24
|
+
private static textResponse;
|
|
25
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import algosdk from 'algosdk';
|
|
2
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { withCommonParams } from './commonParams.js';
|
|
4
|
+
import { getAlgodClient, extractNetwork } from '../algorand-client.js';
|
|
5
|
+
import { WalletManager } from './walletManager.js';
|
|
6
|
+
// ── CAIP-2 ↔ MCP network mapping ─────────────────────────────────────────────
|
|
7
|
+
const CAIP2_GENESIS_TO_NETWORK = {
|
|
8
|
+
'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=': 'mainnet',
|
|
9
|
+
'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=': 'testnet',
|
|
10
|
+
};
|
|
11
|
+
const NETWORK_TO_CAIP2 = {
|
|
12
|
+
mainnet: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=',
|
|
13
|
+
testnet: 'algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=',
|
|
14
|
+
localnet: 'algorand:localnet',
|
|
15
|
+
};
|
|
16
|
+
function caip2ToNetwork(caip2) {
|
|
17
|
+
if (!caip2.startsWith('algorand:'))
|
|
18
|
+
return null;
|
|
19
|
+
const hash = caip2.slice('algorand:'.length);
|
|
20
|
+
return CAIP2_GENESIS_TO_NETWORK[hash] ?? null;
|
|
21
|
+
}
|
|
22
|
+
const ATOMIC_UNITS_NOTE = 'USDC amounts are expressed in atomic units. 1,000,000 atomic units = $1.00 USDC (USDC has 6 decimals).';
|
|
23
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
24
|
+
// ── Tool schemas ─────────────────────────────────────────────────────────────
|
|
25
|
+
const x402ToolSchemas = {
|
|
26
|
+
discoverPaymentRequirements: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
baseURL: { type: 'string', description: 'Base URL of the x402-protected endpoint, e.g. https://example.x402.goplausible.xyz' },
|
|
30
|
+
path: { type: 'string', description: 'Path under baseURL, e.g. /weather' },
|
|
31
|
+
method: { type: 'string', enum: HTTP_METHODS, description: 'HTTP method to probe' },
|
|
32
|
+
queryParams: { type: 'object', description: 'Optional query string parameters', additionalProperties: { type: 'string' } },
|
|
33
|
+
body: { description: 'Optional request body. Strings are sent verbatim; objects are JSON-encoded.' },
|
|
34
|
+
},
|
|
35
|
+
required: ['baseURL', 'path', 'method'],
|
|
36
|
+
},
|
|
37
|
+
makeHttpRequestWithX402: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
baseURL: { type: 'string', description: 'Base URL of the x402-protected endpoint' },
|
|
41
|
+
path: { type: 'string', description: 'Path under baseURL' },
|
|
42
|
+
method: { type: 'string', enum: HTTP_METHODS, description: 'HTTP method' },
|
|
43
|
+
queryParams: { type: 'object', description: 'Optional query string parameters', additionalProperties: { type: 'string' } },
|
|
44
|
+
body: { description: 'Optional request body. Strings are sent verbatim; objects are JSON-encoded.' },
|
|
45
|
+
headers: { type: 'object', description: 'Optional additional request headers', additionalProperties: { type: 'string' } },
|
|
46
|
+
correlationId: { type: 'string', description: 'Optional correlation id; forwarded as X-Correlation-ID header' },
|
|
47
|
+
maxAmountPerRequest: { type: 'integer', description: 'Maximum payment in USDC atomic units (1,000,000 = $1.00). If a payment requirement exceeds this, the call is refused.' },
|
|
48
|
+
paymentRequirements: { type: 'array', description: 'Pre-fetched accepts[] array from a prior x402_discover_payment_requirements call. If omitted, this tool discovers internally.' },
|
|
49
|
+
preferredNetwork: { type: 'string', enum: ['mainnet', 'testnet', 'localnet'], description: 'Preferred Algorand network. When multiple accepts entries match, this network wins. If omitted, the active wallet network is used.' },
|
|
50
|
+
extensions: { type: 'object', description: 'Optional pre-fetched PaymentRequired extensions (e.g. Bazaar resource details). Passed through to the response under `extensions`.' },
|
|
51
|
+
},
|
|
52
|
+
required: ['baseURL', 'path', 'method'],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
// ── X402Manager ──────────────────────────────────────────────────────────────
|
|
56
|
+
export class X402Manager {
|
|
57
|
+
// ── Public dispatch ────────────────────────────────────────────────────────
|
|
58
|
+
static async handleTool(name, args) {
|
|
59
|
+
try {
|
|
60
|
+
switch (name) {
|
|
61
|
+
case 'x402_discover_payment_requirements':
|
|
62
|
+
return await X402Manager.handleDiscover(args);
|
|
63
|
+
case 'make_http_request_with_x402':
|
|
64
|
+
return await X402Manager.handlePaidRequest(args);
|
|
65
|
+
default:
|
|
66
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown x402 tool: ${name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof McpError)
|
|
71
|
+
throw error;
|
|
72
|
+
throw new McpError(ErrorCode.InternalError, `x402 operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ── Tool handlers ──────────────────────────────────────────────────────────
|
|
76
|
+
static async handleDiscover(args) {
|
|
77
|
+
const { baseURL, path, method, queryParams, body } = X402Manager.requireHttpArgs(args);
|
|
78
|
+
const result = await X402Manager.discover({ baseURL, path, method, queryParams, body });
|
|
79
|
+
return X402Manager.textResponse({ result });
|
|
80
|
+
}
|
|
81
|
+
static async handlePaidRequest(args) {
|
|
82
|
+
const { baseURL, path, method, queryParams, body } = X402Manager.requireHttpArgs(args);
|
|
83
|
+
const headers = args.headers ?? {};
|
|
84
|
+
const correlationId = typeof args.correlationId === 'string' ? args.correlationId : undefined;
|
|
85
|
+
const maxAmountPerRequest = typeof args.maxAmountPerRequest === 'number' ? args.maxAmountPerRequest : undefined;
|
|
86
|
+
const preferredNetwork = typeof args.preferredNetwork === 'string' ? args.preferredNetwork : undefined;
|
|
87
|
+
const extensions = args.extensions ?? undefined;
|
|
88
|
+
let accepts = Array.isArray(args.paymentRequirements) ? args.paymentRequirements : undefined;
|
|
89
|
+
// 1. Discover if needed
|
|
90
|
+
if (!accepts || accepts.length === 0) {
|
|
91
|
+
const discovered = await X402Manager.discover({ baseURL, path, method, queryParams, body });
|
|
92
|
+
if (!discovered.x402 || !Array.isArray(discovered.accepts) || discovered.accepts.length === 0) {
|
|
93
|
+
throw new McpError(ErrorCode.InvalidRequest, `Endpoint did not return an x402 payment requirement (status ${discovered.status}). Use a non-x402 HTTP tool, or pass paymentRequirements explicitly.`);
|
|
94
|
+
}
|
|
95
|
+
accepts = discovered.accepts;
|
|
96
|
+
}
|
|
97
|
+
// 2. Select a requirement we can satisfy on Algorand
|
|
98
|
+
const { requirement, mcpNetwork } = X402Manager.selectRequirement(accepts, preferredNetwork, maxAmountPerRequest);
|
|
99
|
+
// 3. Build and sign the payment group
|
|
100
|
+
const paymentHeader = await X402Manager.buildPaymentHeader(requirement, mcpNetwork);
|
|
101
|
+
// 4. Retry with PAYMENT-SIGNATURE
|
|
102
|
+
const paidHeaders = { ...headers, 'PAYMENT-SIGNATURE': paymentHeader };
|
|
103
|
+
if (correlationId)
|
|
104
|
+
paidHeaders['X-Correlation-ID'] = correlationId;
|
|
105
|
+
const response = await X402Manager.httpRequest({ baseURL, path, method, queryParams, body, headers: paidHeaders });
|
|
106
|
+
if (response.status === 402) {
|
|
107
|
+
// Surface the server's rejection rather than retrying.
|
|
108
|
+
throw new McpError(ErrorCode.InvalidRequest, `Payment rejected by server: ${X402Manager.snippet(response.bodyText)}`);
|
|
109
|
+
}
|
|
110
|
+
const result = X402Manager.tryParseJson(response.bodyText) ?? response.bodyText;
|
|
111
|
+
const paymentResponseHeader = response.headers['x-payment-response'] ?? response.headers['X-PAYMENT-RESPONSE'];
|
|
112
|
+
return X402Manager.textResponse({
|
|
113
|
+
result,
|
|
114
|
+
status: response.status,
|
|
115
|
+
paymentResponse: paymentResponseHeader ? X402Manager.decodeBase64Json(paymentResponseHeader) : undefined,
|
|
116
|
+
paid: {
|
|
117
|
+
network: mcpNetwork,
|
|
118
|
+
asset: requirement.asset,
|
|
119
|
+
amount: requirement.maxAmountRequired,
|
|
120
|
+
payTo: requirement.payTo,
|
|
121
|
+
},
|
|
122
|
+
extensions,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// ── x402 protocol helpers ──────────────────────────────────────────────────
|
|
126
|
+
static async discover(opts) {
|
|
127
|
+
const response = await X402Manager.httpRequest(opts);
|
|
128
|
+
if (response.status !== 402) {
|
|
129
|
+
return { status: response.status, x402: false, rawBody: X402Manager.tryParseJson(response.bodyText) ?? response.bodyText };
|
|
130
|
+
}
|
|
131
|
+
const parsed = X402Manager.tryParseJson(response.bodyText);
|
|
132
|
+
if (!parsed ||
|
|
133
|
+
typeof parsed !== 'object' ||
|
|
134
|
+
typeof parsed.x402Version !== 'number' ||
|
|
135
|
+
!Array.isArray(parsed.accepts)) {
|
|
136
|
+
return { status: 402, x402: false, rawBody: parsed ?? response.bodyText };
|
|
137
|
+
}
|
|
138
|
+
const body = parsed;
|
|
139
|
+
return { status: 402, x402: true, accepts: body.accepts, x402Version: body.x402Version };
|
|
140
|
+
}
|
|
141
|
+
static selectRequirement(accepts, preferredNetwork, maxAmountPerRequest) {
|
|
142
|
+
// Map each accepts entry to its MCP network (or null if non-Algorand).
|
|
143
|
+
const ranked = accepts
|
|
144
|
+
.map(req => ({ req, mcpNetwork: caip2ToNetwork(req.network) }))
|
|
145
|
+
.filter((e) => e.mcpNetwork !== null);
|
|
146
|
+
if (ranked.length === 0) {
|
|
147
|
+
throw new McpError(ErrorCode.InvalidRequest, `No payment requirement is satisfiable on Algorand. Endpoint accepts networks: [${accepts.map(r => r.network).join(', ')}]`);
|
|
148
|
+
}
|
|
149
|
+
const inBudget = (req) => maxAmountPerRequest === undefined || Number(req.maxAmountRequired) <= maxAmountPerRequest;
|
|
150
|
+
const affordable = ranked.filter(e => inBudget(e.req));
|
|
151
|
+
if (affordable.length === 0) {
|
|
152
|
+
throw new McpError(ErrorCode.InvalidRequest, `All Algorand payment requirements exceed maxAmountPerRequest=${maxAmountPerRequest}. Cheapest: ${Math.min(...ranked.map(e => Number(e.req.maxAmountRequired)))}.`);
|
|
153
|
+
}
|
|
154
|
+
if (preferredNetwork) {
|
|
155
|
+
const preferred = affordable.find(e => e.mcpNetwork === preferredNetwork);
|
|
156
|
+
if (preferred)
|
|
157
|
+
return { requirement: preferred.req, mcpNetwork: preferred.mcpNetwork };
|
|
158
|
+
}
|
|
159
|
+
// No preference (or preference not matched): pick the cheapest affordable.
|
|
160
|
+
const chosen = affordable.reduce((a, b) => Number(a.req.maxAmountRequired) <= Number(b.req.maxAmountRequired) ? a : b);
|
|
161
|
+
return { requirement: chosen.req, mcpNetwork: chosen.mcpNetwork };
|
|
162
|
+
}
|
|
163
|
+
static async buildPaymentHeader(requirement, mcpNetwork) {
|
|
164
|
+
const feePayer = requirement.extra?.feePayer;
|
|
165
|
+
if (!feePayer || typeof feePayer !== 'string') {
|
|
166
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Payment requirement is missing `extra.feePayer` (facilitator address).');
|
|
167
|
+
}
|
|
168
|
+
const amount = Number(requirement.maxAmountRequired);
|
|
169
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
170
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid maxAmountRequired: ${requirement.maxAmountRequired}`);
|
|
171
|
+
}
|
|
172
|
+
const account = await WalletManager.getActiveWalletAccount();
|
|
173
|
+
const sk = await WalletManager.getActiveWalletSecretKey();
|
|
174
|
+
const algodClient = getAlgodClient(mcpNetwork);
|
|
175
|
+
const suggestedParams = await algodClient.getTransactionParams().do();
|
|
176
|
+
// Fee payer covers fees for both txns in the group.
|
|
177
|
+
const feePayerParams = { ...suggestedParams, fee: BigInt(2000), flatFee: true };
|
|
178
|
+
const innerParams = { ...suggestedParams, fee: BigInt(0), flatFee: true };
|
|
179
|
+
const feePayerTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
|
|
180
|
+
sender: feePayer,
|
|
181
|
+
receiver: feePayer,
|
|
182
|
+
amount: 0,
|
|
183
|
+
suggestedParams: feePayerParams,
|
|
184
|
+
});
|
|
185
|
+
const isAlgo = requirement.asset === '0' || requirement.asset === '';
|
|
186
|
+
const paymentTxn = isAlgo
|
|
187
|
+
? algosdk.makePaymentTxnWithSuggestedParamsFromObject({
|
|
188
|
+
sender: account.address,
|
|
189
|
+
receiver: requirement.payTo,
|
|
190
|
+
amount,
|
|
191
|
+
suggestedParams: innerParams,
|
|
192
|
+
})
|
|
193
|
+
: algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
|
|
194
|
+
sender: account.address,
|
|
195
|
+
receiver: requirement.payTo,
|
|
196
|
+
assetIndex: Number(requirement.asset),
|
|
197
|
+
amount,
|
|
198
|
+
suggestedParams: innerParams,
|
|
199
|
+
});
|
|
200
|
+
const grouped = algosdk.assignGroupID([feePayerTxn, paymentTxn]);
|
|
201
|
+
const groupedFeePayer = grouped[0];
|
|
202
|
+
const groupedPayment = grouped[1];
|
|
203
|
+
const signedPayment = algosdk.signTransaction(groupedPayment, sk);
|
|
204
|
+
const unsignedFeePayer = algosdk.encodeUnsignedTransaction(groupedFeePayer);
|
|
205
|
+
const headerBody = {
|
|
206
|
+
x402Version: 2,
|
|
207
|
+
scheme: requirement.scheme,
|
|
208
|
+
network: requirement.network,
|
|
209
|
+
payload: {
|
|
210
|
+
paymentGroup: [
|
|
211
|
+
algosdk.bytesToBase64(unsignedFeePayer),
|
|
212
|
+
algosdk.bytesToBase64(signedPayment.blob),
|
|
213
|
+
],
|
|
214
|
+
paymentIndex: 1,
|
|
215
|
+
},
|
|
216
|
+
accepted: requirement,
|
|
217
|
+
};
|
|
218
|
+
return Buffer.from(JSON.stringify(headerBody), 'utf8').toString('base64');
|
|
219
|
+
}
|
|
220
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────────
|
|
221
|
+
static async httpRequest(opts) {
|
|
222
|
+
var _a;
|
|
223
|
+
const url = X402Manager.buildUrl(opts.baseURL, opts.path, opts.queryParams);
|
|
224
|
+
const init = { method: opts.method, headers: { ...(opts.headers ?? {}) } };
|
|
225
|
+
if (opts.body !== undefined && opts.method !== 'GET') {
|
|
226
|
+
if (typeof opts.body === 'string') {
|
|
227
|
+
init.body = opts.body;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
init.body = JSON.stringify(opts.body);
|
|
231
|
+
(_a = init.headers)['content-type'] ?? (_a['content-type'] = 'application/json');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const response = await fetch(url, init);
|
|
235
|
+
const bodyText = await response.text();
|
|
236
|
+
const headers = {};
|
|
237
|
+
response.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
|
|
238
|
+
return { status: response.status, bodyText, headers };
|
|
239
|
+
}
|
|
240
|
+
static buildUrl(baseURL, path, queryParams) {
|
|
241
|
+
const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
242
|
+
const tail = path.startsWith('/') ? path : `/${path}`;
|
|
243
|
+
const url = new URL(base + tail);
|
|
244
|
+
if (queryParams) {
|
|
245
|
+
for (const [k, v] of Object.entries(queryParams))
|
|
246
|
+
url.searchParams.set(k, String(v));
|
|
247
|
+
}
|
|
248
|
+
return url.toString();
|
|
249
|
+
}
|
|
250
|
+
// ── Misc helpers ───────────────────────────────────────────────────────────
|
|
251
|
+
static requireHttpArgs(args) {
|
|
252
|
+
const baseURL = typeof args.baseURL === 'string' ? args.baseURL : null;
|
|
253
|
+
const path = typeof args.path === 'string' ? args.path : null;
|
|
254
|
+
const method = typeof args.method === 'string' ? args.method.toUpperCase() : null;
|
|
255
|
+
if (!baseURL)
|
|
256
|
+
throw new McpError(ErrorCode.InvalidParams, 'baseURL (string) is required');
|
|
257
|
+
if (!path)
|
|
258
|
+
throw new McpError(ErrorCode.InvalidParams, 'path (string) is required');
|
|
259
|
+
if (!method || !HTTP_METHODS.includes(method)) {
|
|
260
|
+
throw new McpError(ErrorCode.InvalidParams, `method must be one of: ${HTTP_METHODS.join(', ')}`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
new URL(baseURL);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new McpError(ErrorCode.InvalidParams, `baseURL is not a valid URL: ${baseURL}`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
baseURL,
|
|
270
|
+
path,
|
|
271
|
+
method: method,
|
|
272
|
+
queryParams: args.queryParams,
|
|
273
|
+
body: args.body,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
static tryParseJson(text) {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(text);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
static decodeBase64Json(b64) {
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(Buffer.from(b64, 'base64').toString('utf8'));
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return b64;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
static snippet(text, max = 300) {
|
|
293
|
+
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
294
|
+
}
|
|
295
|
+
static textResponse(payload) {
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: 'text',
|
|
300
|
+
text: JSON.stringify({ ...payload, _atomicUnitsNote: ATOMIC_UNITS_NOTE }, null, 2),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
X402Manager.x402Tools = [
|
|
307
|
+
{
|
|
308
|
+
name: 'x402_discover_payment_requirements',
|
|
309
|
+
description: 'Probe an x402-protected HTTP endpoint and return its payment requirements (the `accepts` array from the 402 response) without paying. Use this to inspect cost and supported networks/assets before calling `make_http_request_with_x402`.',
|
|
310
|
+
inputSchema: withCommonParams(x402ToolSchemas.discoverPaymentRequirements),
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'make_http_request_with_x402',
|
|
314
|
+
description: 'Call an x402-protected HTTP endpoint with automatic USDC/ALGO payment from the active wallet. If `paymentRequirements` is not supplied, this tool runs discovery internally (one extra request). Builds an atomic 2-transaction group (facilitator fee-payer + wallet payment), signs the payment leg with the wallet, and resends with the `PAYMENT-SIGNATURE` header.',
|
|
315
|
+
inputSchema: withCommonParams(x402ToolSchemas.makeHttpRequestWithX402),
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
// extractNetwork is imported above but the x402 tools do not currently use the
|
|
319
|
+
// generic `network` arg — they derive the working network from the chosen
|
|
320
|
+
// payment requirement's CAIP-2 identifier instead. The import remains for
|
|
321
|
+
// future use (e.g. localnet discovery against a private facilitator).
|
|
322
|
+
void extractNetwork;
|