agentwallet-mcp 1.14.6 → 1.14.7

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 CHANGED
@@ -256,7 +256,7 @@ pay_x402(
256
256
 
257
257
  **Asset risk.** `check_token_risk` (and every `approve_token` result) reports honeypot, tax, owner-power, verified-source, holder-concentration and liquidity flags from GoPlus Security, with an on-chain fallback. A warning for the caller to weigh, never a block.
258
258
 
259
- **Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas.
259
+ **Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas. The same list, rendered with Basescan links, is at [hifriendbot.com/wallet/payments](https://hifriendbot.com/wallet/payments/), and a five-minute walkthrough from install to a real settlement is at [hifriendbot.com/wallet/quickstart](https://hifriendbot.com/wallet/quickstart/). Security policy, fixed disclosures and the release history: [hifriendbot.com/security](https://hifriendbot.com/security/) and [hifriendbot.com/changelog](https://hifriendbot.com/changelog/).
260
260
 
261
261
  ## Pay any x402 API from Claude in three steps
262
262
 
@@ -374,7 +374,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
374
374
  ```
375
375
 
376
376
  - **`budget`**: rolling dollar budgets (last 24 hours, last 30 days, lifetime) across every chain, counting x402 payments, stablecoin transfers and stablecoin approvals in registry stablecoins. A spend the budget cannot price (a token outside the registry) is refused while a budget is set, unless `"unpriced_spends": "allow"`. An SPL send is priced at the mint's own decimals; a `token_decimals` hint that disagrees with the mint is refused before any request is made.
377
- - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract).
377
+ - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract) and, in local Solana mode, the rent an SPL send pays to create the recipient's token account (the recipient owns that account and can close it for the lamports; the ledger records it as its own line, method `rent`, to the recipient, and `assets.allow` does not need to list `native` for it).
378
378
  - **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, or the same calldata through `send_transaction`) is what the spender will take later, so it is priced, checked and reserved on the ledger when it is granted, the spender is checked against the recipient lists, and an unlimited approval (2^128 base units or more) is refused outright, since no budget can count it; grant an exact amount. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted.
379
379
  - **`hosts`**, **`per_host`**: which endpoints `pay_x402` may pay, and how much per host per day, per month and per payment. A denied host is never contacted, not even for a quote. A host is matched by its canonical DNS name (lowercase, the root label's trailing dot removed), so `api.example.com.` and its percent-encoded spelling are `api.example.com` to every list and to the ledger's per-host sums.
380
380
  - **`recipients`**: where funds may go (transfers, x402 `payTo`, approval spenders, contracts that receive native value). The allow list covers those funds destinations; the contract a zero-value call is aimed at (a token being approved, a router) is checked against the deny list and the chain list.
package/build/index.js CHANGED
@@ -30,7 +30,7 @@ import { WRAPPED_NATIVE } from './wrapped-native.js';
30
30
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
31
31
  import { assessTokenRisk } from './token-risk.js';
32
32
  import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, decodeAbiString as decodeDomainString, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, sanitizeRequired, } from './x402-eip3009.js';
33
- import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, } from './local-solana.js';
33
+ import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, splAccountRent, } from './local-solana.js';
34
34
  import { localSimulate } from './local-wallet.js';
35
35
  import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
36
36
  import { Ledger } from './ledger.js';
@@ -92,7 +92,10 @@ function reserveSpend(input) {
92
92
  const chain = chainAllowed(p, input.chainId);
93
93
  if (!chain.ok)
94
94
  throw new Error(chain.reason + ' Nothing was signed.');
95
- const asset = assetAllowed(p, input.chainId, input.asset);
95
+ // Rent is SOL by nature, not by choice: the operator allowed the token, and
96
+ // the account that holds it is paid for in the chain's coin, so assets.allow
97
+ // does not have to list native for the rent line. native_budget counts it.
98
+ const asset = input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset);
96
99
  if (!asset.ok)
97
100
  throw new Error(asset.reason + ' Nothing was signed.');
98
101
  const to = recipientAllowed(p, input.to);
@@ -105,7 +108,7 @@ function reserveSpend(input) {
105
108
  /** The same checks with nothing recorded, for quotes and simulations. */
106
109
  function spendVerdict(input) {
107
110
  const p = POL();
108
- for (const r of [chainAllowed(p, input.chainId), assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
111
+ for (const r of [chainAllowed(p, input.chainId), input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
109
112
  if (!r.ok)
110
113
  return r;
111
114
  const limits = limitsFor(input.host);
@@ -141,7 +144,19 @@ async function describeSend(body, chainId) {
141
144
  // reached the custodian (reported privately 2026-10-24, 1.14.5). The chain
142
145
  // would have refused the TransferChecked, but the ledger was already wrong.
143
146
  const given = Number.isInteger(d) && d >= 0 && d <= 36 ? d : undefined;
144
- return { kind: 'spl', to: String(body.to || ''), asset: mint, raw: String(body.value ?? '0'), decimals: await verifiedDecimals(chainId, mint, given) };
147
+ const to = String(body.to || '');
148
+ const decimals = await verifiedDecimals(chainId, mint, given);
149
+ // In local mode this process is the payer: when the recipient has no token
150
+ // account yet, the send creates one and the rent is native value leaving
151
+ // the wallet, reserved here from the same reading the signer will make.
152
+ // The hosted custodian pays its own rent and bounds it on its side.
153
+ let carried;
154
+ if (isSolanaLocalMode() && to) {
155
+ const rent = await splAccountRent(mint, to, chainId);
156
+ if (rent > 0n)
157
+ carried = { raw: rent.toString(), to, rent: true };
158
+ }
159
+ return { kind: 'spl', to, asset: mint, raw: String(body.value ?? '0'), decimals, ...(carried ? { carried } : {}) };
145
160
  }
146
161
  const data = String(body.data || '');
147
162
  const to = String(body.to || '');
@@ -221,7 +236,9 @@ function assertSendTarget(d, chainId) {
221
236
  * recipients.allow and .deny bind who may be allowed to pull; the native
222
237
  * value of a contract call is a spend to that contract; the native value
223
238
  * carried on a token or approval call is a second spend, native, to the
224
- * called contract (1.14.4). Throws for an unbounded approval under a policy.
239
+ * called contract (1.14.4); the rent a local SPL send pays for the recipient's
240
+ * token account is a native spend to the recipient, method 'rent' (1.14.7).
241
+ * Throws for an unbounded approval under a policy.
225
242
  */
226
243
  function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
227
244
  if (d.kind === 'approval' && d.unbounded && POLICY.path) {
@@ -238,7 +255,7 @@ function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
238
255
  });
239
256
  }
240
257
  if (d.carried) {
241
- out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: 'native' });
258
+ out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: d.carried.rent ? 'rent' : 'native' });
242
259
  }
243
260
  return out;
244
261
  }
@@ -1175,7 +1192,7 @@ function resolveChainId(network) {
1175
1192
  const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
1176
1193
  const server = new McpServer({
1177
1194
  name: 'agentwallet',
1178
- version: '1.14.6',
1195
+ version: '1.14.7',
1179
1196
  }, {
1180
1197
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1181
1198
  ? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
package/build/ledger.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type Spent } from './policy.js';
2
- /** 'approval' is an ERC-20 allowance granted (approve, increaseAllowance, Permit2.approve): a spend the spender executes later, counted when granted. */
3
- export type SpendMethod = 'eip3009' | 'permit2' | 'transfer' | 'native' | 'spl' | 'autopay' | 'approval';
2
+ /**
3
+ * 'approval' is an ERC-20 allowance granted (approve, increaseAllowance, Permit2.approve): a spend the spender executes later, counted when granted.
4
+ * 'rent' is the SOL an SPL send pays to create the recipient's token account (1.14.7): native, to the recipient, who owns the account and can close it for the lamports.
5
+ */
6
+ export type SpendMethod = 'eip3009' | 'permit2' | 'transfer' | 'native' | 'spl' | 'autopay' | 'approval' | 'rent';
4
7
  export interface SpendEntry {
5
8
  seq: number;
6
9
  ts: string;
@@ -76,6 +76,16 @@ export declare function localSolTransfer(to: string, lamports: string, chainId?:
76
76
  * not exist, using the idempotent instruction so a race cannot fail the send.
77
77
  */
78
78
  export declare function localSplTransfer(mintStr: string, to: string, rawAmount: string, decimals: number, chainId?: number): Promise<LocalSolResult>;
79
+ /**
80
+ * The SOL an SPL send to `to` spends on creating the recipient's token
81
+ * account, in lamports; 0n when the account exists. The recipient owns that
82
+ * account and can close it to keep the lamports, so it is native value leaving
83
+ * the wallet and the spend policy reserves it against native_budget before the
84
+ * send is built (reported privately 2026-10-26: it was checked against the
85
+ * per-transaction SOL cap only, and fresh recipients took SOL past the daily
86
+ * budget while the ledger held token lines alone).
87
+ */
88
+ export declare function splAccountRent(mintStr: string, to: string, chainId?: number): Promise<bigint>;
79
89
  export declare function solanaWalletRecord(): {
80
90
  id: string;
81
91
  address: string;
@@ -448,31 +448,15 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
448
448
  const mint = new PublicKey(mintStr);
449
449
  const recipient = new PublicKey(to);
450
450
  await assertRecipientIsWallet(conn, recipient);
451
- const { tokenProgram, ataSize } = await mintLayout(conn, mint);
451
+ const { tokenProgram, destAta, rent } = await destinationAccount(conn, mint, recipient);
452
452
  const sourceAta = associatedTokenAddress(payer.publicKey, mint, tokenProgram);
453
- const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
454
453
  const tx = new Transaction();
455
- const destInfo = await conn.getAccountInfo(destAta);
456
- // The associated-account program creates into an address that already holds
457
- // plain SOL (system-owned, no data): it tops up the rent, allocates and
458
- // assigns. Only an address holding something else cannot become the token
459
- // account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
460
- // block a recipient for the price of the rent-exempt minimum).
461
- const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
462
- if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
463
- throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
464
- }
465
- if (!destInfo || prefunded) {
454
+ if (rent > 0n) {
466
455
  // Creating the recipient's token account costs the payer rent in SOL, an
467
456
  // outflow the SOL cap must see (0.002 SOL a time adds up over many sends).
468
- // Rent follows the account's real size, which for Token-2022 is above 165;
469
- // SOL already sitting at the address reduces what the payer tops up.
470
- // The node's rent figure is floored at the cluster constant and the SOL the
471
- // node says already sits at the address is not credited: both are numbers a
472
- // lying RPC used to zero so the create went out under the cap (round 4).
473
- const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
474
- const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
475
- assertWithinSolCap(BigInt(rent));
457
+ // The policy reserved the same figure against native_budget before this
458
+ // call (1.14.7); the cap is the per-transaction floor under it.
459
+ assertWithinSolCap(rent);
476
460
  tx.add(createAtaIdempotentIx(payer.publicKey, destAta, recipient, mint, tokenProgram));
477
461
  }
478
462
  tx.add(transferCheckedIx(tokenProgram, sourceAta, mint, destAta, payer.publicKey, BigInt(rawAmount), decimals));
@@ -487,6 +471,52 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
487
471
  signed_locally: true,
488
472
  };
489
473
  }
474
+ /**
475
+ * The recipient's associated account for a mint, and the rent the payer funds
476
+ * when it has to be created: 0 when it already exists. One reading shared by
477
+ * the send and by the policy's reservation, so the ledger line and the
478
+ * instruction carry the same figure.
479
+ *
480
+ * The associated-account program creates into an address that already holds
481
+ * plain SOL (system-owned, no data): it tops up the rent, allocates and
482
+ * assigns. Only an address holding something else cannot become the token
483
+ * account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
484
+ * block a recipient for the price of the rent-exempt minimum).
485
+ *
486
+ * Rent follows the account's real size, which for Token-2022 is above 165;
487
+ * SOL already sitting at the address reduces what the payer tops up. The
488
+ * node's rent figure is floored at the cluster constant and the SOL the node
489
+ * says already sits at the address is not credited: both are numbers a lying
490
+ * RPC used to zero so the create went out under the cap (round 4).
491
+ */
492
+ async function destinationAccount(conn, mint, recipient) {
493
+ const { tokenProgram, ataSize } = await mintLayout(conn, mint);
494
+ const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
495
+ const destInfo = await conn.getAccountInfo(destAta);
496
+ const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
497
+ if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
498
+ throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
499
+ }
500
+ if (destInfo && !prefunded)
501
+ return { tokenProgram, destAta, rent: 0n };
502
+ const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
503
+ const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
504
+ return { tokenProgram, destAta, rent: BigInt(rent) };
505
+ }
506
+ /**
507
+ * The SOL an SPL send to `to` spends on creating the recipient's token
508
+ * account, in lamports; 0n when the account exists. The recipient owns that
509
+ * account and can close it to keep the lamports, so it is native value leaving
510
+ * the wallet and the spend policy reserves it against native_budget before the
511
+ * send is built (reported privately 2026-10-26: it was checked against the
512
+ * per-transaction SOL cap only, and fresh recipients took SOL past the daily
513
+ * budget while the ledger held token lines alone).
514
+ */
515
+ export async function splAccountRent(mintStr, to, chainId = 900) {
516
+ const conn = connection(chainId);
517
+ await assertCluster(conn, chainId);
518
+ return (await destinationAccount(conn, new PublicKey(mintStr), new PublicKey(to))).rent;
519
+ }
490
520
  const TOKEN_ACCOUNT_SIZE = 165;
491
521
  const MINT_SIZE = 82;
492
522
  /** Token-2022 pads a mint to the token-account size and tags the next byte: 1 = mint, 2 = token account. */
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/hifriendbot/agentwallet-mcp.git"
7
7
  },
8
- "version": "1.14.6",
8
+ "version": "1.14.7",
9
9
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
10
10
  "type": "module",
11
11
  "main": "build/index.js",