agentwallet-mcp 1.10.9 → 1.12.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 +28 -10
- package/build/index.js +316 -112
- package/build/local-wallet.d.ts +26 -0
- package/build/local-wallet.js +43 -0
- package/build/token-risk.d.ts +19 -0
- package/build/token-risk.js +127 -0
- package/build/x402-eip3009.d.ts +171 -0
- package/build/x402-eip3009.js +207 -0
- package/build/x402-permit2.d.ts +124 -0
- package/build/x402-permit2.js +93 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,7 +172,10 @@ Add to your settings:
|
|
|
172
172
|
| `get_allowance` | Check ERC-20 token allowance |
|
|
173
173
|
| `wrap_eth` | Wrap native tokens to WETH/WAVAX/etc. |
|
|
174
174
|
| `unwrap_eth` | Unwrap WETH back to native tokens |
|
|
175
|
-
| `pay_x402` | Pay x402 invoices
|
|
175
|
+
| `pay_x402` | Pay x402 invoices (exact via EIP-3009, upto via Permit2), with approvals above the cap |
|
|
176
|
+
| `check_approval` | Status of a human approval created by an over-cap `pay_x402` |
|
|
177
|
+
| `approve_permit2` | One-time token approval to Permit2 for `upto` endpoints |
|
|
178
|
+
| `check_token_risk` | Honeypot, tax, owner-power and holder-concentration flags for a token |
|
|
176
179
|
| `create_paywall` | Create an x402 paywall to charge for a resource |
|
|
177
180
|
| `list_paywalls` | List all your x402 paywalls |
|
|
178
181
|
| `get_paywall` | Get paywall details by ID |
|
|
@@ -219,15 +222,15 @@ All on-chain. All through MCP. No frontend needed.
|
|
|
219
222
|
|
|
220
223
|
## x402 Payments
|
|
221
224
|
|
|
222
|
-
AgentWallet
|
|
225
|
+
AgentWallet speaks the [x402 open payment standard](https://x402.org) the way the spec defines it. When your Ai agent hits an API that answers HTTP 402, `pay_x402` does the whole flow:
|
|
223
226
|
|
|
224
|
-
1. Fetches the URL and
|
|
225
|
-
2.
|
|
226
|
-
3.
|
|
227
|
-
4. Retries the request with
|
|
228
|
-
5. Returns the
|
|
227
|
+
1. Fetches the URL and reads the payment requirements, from the v2 `PAYMENT-REQUIRED` header or the v1 JSON body.
|
|
228
|
+
2. Picks an `exact` option (you can steer it with `prefer_chain`).
|
|
229
|
+
3. Signs an **EIP-3009 `TransferWithAuthorization`** for exactly that amount. Nothing is broadcast and no gas is paid by the payer; the endpoint's facilitator settles it on-chain.
|
|
230
|
+
4. Retries the request with the payment header (`PAYMENT-SIGNATURE` for v2, `X-PAYMENT` for v1).
|
|
231
|
+
5. Returns the response, the settlement receipt (`PAYMENT-RESPONSE` / `X-PAYMENT-RESPONSE`) and, if the endpoint declined, its stated reason in `retry_error`.
|
|
229
232
|
|
|
230
|
-
|
|
233
|
+
Verified against Coinbase's public facilitator (`isValid: true` for v1 and v2 payloads) and live x402 servers on Base. Works in both custody modes: the local key signs in-process; hosted wallets sign through `POST /wallets/{id}/x402/authorize`, a narrow endpoint that only ever signs this one struct and runs the same pause and token-cap checks as a transfer.
|
|
231
234
|
|
|
232
235
|
```
|
|
233
236
|
pay_x402(
|
|
@@ -237,9 +240,24 @@ pay_x402(
|
|
|
237
240
|
)
|
|
238
241
|
```
|
|
239
242
|
|
|
240
|
-
`max_payment` is
|
|
243
|
+
`max_payment` is a hard per-payment cap. If you omit it, `pay_x402` falls back to `AGENTWALLET_MAX_AUTOPAY` (default `1`), so a malicious or compromised 402 endpoint can never authorize an unbounded payment. In local mode `AGENTWALLET_MAX_TX_TOKEN` applies to authorizations too, because an authorization is a transfer somebody else executes.
|
|
241
244
|
|
|
242
|
-
|
|
245
|
+
**Schemes.** `exact` (EIP-3009, gasless for the payer) is preferred and works on every EVM chain AgentWallet knows. `upto` (a Permit2 maximum the seller settles at actual usage) is signed as a `PermitWitnessTransferFrom` bound to the endpoint's facilitator; it needs a one-time `approve_permit2` per token (or `AGENTWALLET_PERMIT2_AUTO_APPROVE=1`). An `upto` option without `extra.facilitatorAddress` is refused, never approximated with an upfront transfer. Native-asset and Solana requests, and AgentWallet's own paywalls, are paid by an on-chain transfer proved by transaction hash, which is what those servers verify.
|
|
246
|
+
|
|
247
|
+
**Approvals above the cap.** On a hosted wallet, a payment above the cap does not have to fail. With `request_approval` (default on; `AGENTWALLET_APPROVALS=0` turns it off) the wallet owner gets an email with approve and deny links, `pay_x402` returns an `approval_id`, and the agent retries with it once `check_approval` says approved. An approval is bound to one wallet, chain, asset, recipient and maximum, expires in 24 hours, and is consumed by one signature. Local mode has no approval channel: the caps in your environment are the policy.
|
|
248
|
+
|
|
249
|
+
**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.
|
|
250
|
+
|
|
251
|
+
**Proof.** Real settlements are listed in [PAYMENTS.md](PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas.
|
|
252
|
+
|
|
253
|
+
## Pay any x402 API from Claude in three steps
|
|
254
|
+
|
|
255
|
+
1. Add the server (one of the snippets under Quick Start). Self-custody: set `AGENTWALLET_PRIVATE_KEY` to a key that holds USDC on Base. Hosted: `AGENTWALLET_USER` and `AGENTWALLET_PASS` from hifriendbot.com/wallet.
|
|
256
|
+
2. Ask: *"Use pay_x402 to POST https://api.ozdreamtools.de/api/holidays with body {"year":2026,"state":"BY"} and a max_payment of 0.05."*
|
|
257
|
+
3. Read the result: `payment_made: true`, the amount, `payment_method`, and `settlement.transaction`, the on-chain hash. No ETH needed for `exact`: the endpoint's facilitator pays the gas.
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
**Token domains.** The EIP-712 domain comes from the endpoint's `extra.name` / `extra.version`, then a short registry of USDC deployments, then the token contract's own `name()` / `version()`. If none of those answer, the payment is refused rather than signed with a guessed domain.
|
|
243
261
|
|
|
244
262
|
## x402 Acceptance
|
|
245
263
|
|
package/build/index.js
CHANGED
|
@@ -15,6 +15,10 @@ import { z } from 'zod';
|
|
|
15
15
|
import { assertPublicUrl, safeFetch } from './ssrf-guard.js';
|
|
16
16
|
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, } from './x402-payment.js';
|
|
17
17
|
import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
|
|
18
|
+
import { localSignAuthorization, localEthCall, localSignPermit2Upto } from './local-wallet.js';
|
|
19
|
+
import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, PERMIT2_ADDRESS, } from './x402-permit2.js';
|
|
20
|
+
import { assessTokenRisk } from './token-risk.js';
|
|
21
|
+
import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, } from './x402-eip3009.js';
|
|
18
22
|
import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, } from './local-solana.js';
|
|
19
23
|
/** True when either chain family is running non-custodially. */
|
|
20
24
|
function anyLocalMode() {
|
|
@@ -90,6 +94,40 @@ async function routeLocally(path, method, body) {
|
|
|
90
94
|
const hasData = data && data !== '0x';
|
|
91
95
|
return localSend(chainId, to, value, hasData ? data : undefined);
|
|
92
96
|
}
|
|
97
|
+
// POST /wallets/{id}/x402/authorize (x402 "exact": EIP-3009 authorization, signed here, broadcast by nobody)
|
|
98
|
+
if (method === 'POST' && /^\/wallets\/[^/]*\/x402\/authorize$/.test(rawPath)) {
|
|
99
|
+
if (!isLocalMode())
|
|
100
|
+
refuseMissingLocalKey('evm');
|
|
101
|
+
const chainId = localChainId(body, qs);
|
|
102
|
+
return localSignAuthorization(chainId, String(body?.asset || ''), { name: String(body?.name || ''), version: String(body?.version || '') }, {
|
|
103
|
+
from: getLocalAddress(),
|
|
104
|
+
to: String(body?.to || ''),
|
|
105
|
+
value: String(body?.value ?? '0'),
|
|
106
|
+
validAfter: String(body?.valid_after ?? '0'),
|
|
107
|
+
validBefore: String(body?.valid_before ?? '0'),
|
|
108
|
+
nonce: String(body?.nonce || ''),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// POST /wallets/{id}/x402/permit2 (x402 "upto": Permit2 max-authorization, signed here)
|
|
112
|
+
if (method === 'POST' && /^\/wallets\/[^/]*\/x402\/permit2$/.test(rawPath)) {
|
|
113
|
+
if (!isLocalMode())
|
|
114
|
+
refuseMissingLocalKey('evm');
|
|
115
|
+
const chainId = localChainId(body, qs);
|
|
116
|
+
return localSignPermit2Upto(chainId, {
|
|
117
|
+
from: getLocalAddress(),
|
|
118
|
+
permitted: { token: String(body?.asset || ''), amount: String(body?.amount ?? '0') },
|
|
119
|
+
spender: String(body?.spender || ''),
|
|
120
|
+
nonce: String(body?.nonce ?? '0'),
|
|
121
|
+
deadline: String(body?.deadline ?? '0'),
|
|
122
|
+
witness: { to: String(body?.to || ''), facilitator: String(body?.facilitator || ''), validAfter: String(body?.valid_after ?? '0') },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// POST /eth-call (read-only; answered from the local RPC so pure self-custody needs no hosted credentials)
|
|
126
|
+
if (method === 'POST' && rawPath === '/eth-call') {
|
|
127
|
+
if (!isLocalMode())
|
|
128
|
+
return NOT_HANDLED;
|
|
129
|
+
return localEthCall(localChainId(body, qs), String(body?.to || ''), String(body?.data || '0x'));
|
|
130
|
+
}
|
|
93
131
|
// GET /wallets/{id}/balance
|
|
94
132
|
if (method === 'GET' && /^\/wallets\/[^/]*\/balance$/.test(rawPath)) {
|
|
95
133
|
const chainId = localChainId(body, qs);
|
|
@@ -423,7 +461,7 @@ function resolveChainId(network) {
|
|
|
423
461
|
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');
|
|
424
462
|
const server = new McpServer({
|
|
425
463
|
name: 'agentwallet',
|
|
426
|
-
version: '1.
|
|
464
|
+
version: '1.12.0',
|
|
427
465
|
}, {
|
|
428
466
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. Private keys are encrypted server-side and never exposed — agents sign and broadcast transactions without ever touching raw keys.
|
|
429
467
|
|
|
@@ -765,6 +803,17 @@ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 toke
|
|
|
765
803
|
rawAmount = parseUnits(amount, decimals);
|
|
766
804
|
}
|
|
767
805
|
const calldata = '0x095ea7b3' + padAddress(spender) + encodeUint256(rawAmount);
|
|
806
|
+
// Asset risk rides along with the approval (issue #13). It never blocks; it is the
|
|
807
|
+
// caller's call. Skipped for Permit2, which only moves what a signature allows.
|
|
808
|
+
let risk = null;
|
|
809
|
+
if (spender.toLowerCase() !== PERMIT2_ADDRESS.toLowerCase()) {
|
|
810
|
+
try {
|
|
811
|
+
risk = await assessTokenRisk(chain_id, token, (to, data) => ethCallHex(chain_id, to, data));
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
risk = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
768
817
|
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
769
818
|
to: token,
|
|
770
819
|
value: '0',
|
|
@@ -776,6 +825,7 @@ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 toke
|
|
|
776
825
|
token,
|
|
777
826
|
spender,
|
|
778
827
|
amount: amount.toLowerCase() === 'max' ? 'unlimited' : amount,
|
|
828
|
+
risk,
|
|
779
829
|
});
|
|
780
830
|
});
|
|
781
831
|
// ─── Tool: get_allowance ────────────────────────────────────────
|
|
@@ -935,11 +985,76 @@ server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20
|
|
|
935
985
|
decimals,
|
|
936
986
|
});
|
|
937
987
|
});
|
|
988
|
+
// ─── x402 "exact" helpers ────────────────────────────────────────
|
|
989
|
+
/** The paying address: the local key in self-custody mode, else the hosted wallet record. */
|
|
990
|
+
async function payerAddress(walletId) {
|
|
991
|
+
const w = (await api(`/wallets/${walletId}`, 'GET', undefined, { 'X-AGW-SKIP-X402': 'true' }));
|
|
992
|
+
const a = String(w?.address || w?.wallet_address || '');
|
|
993
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(a))
|
|
994
|
+
throw new Error(`x402: could not resolve the EVM address of wallet ${walletId} (got "${a}").`);
|
|
995
|
+
return a;
|
|
996
|
+
}
|
|
997
|
+
async function ethCallHex(chainId, to, data) {
|
|
998
|
+
const r = (await api('/eth-call', 'POST', { chain_id: chainId, to, data }, { 'X-AGW-SKIP-X402': 'true' }));
|
|
999
|
+
return String(r?.result || '');
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* The token's EIP-712 domain. Order: what the 402 declared (extra.name/version,
|
|
1003
|
+
* which is what the reference client requires), then a short list of USDC
|
|
1004
|
+
* deployments we know, then the contract's own name()/version(). A wrong
|
|
1005
|
+
* domain produces a signature that recovers to a stranger, so we refuse
|
|
1006
|
+
* rather than guess.
|
|
1007
|
+
*/
|
|
1008
|
+
async function resolveTokenDomain(chainId, asset, extra) {
|
|
1009
|
+
if (extra?.name && extra?.version)
|
|
1010
|
+
return { name: String(extra.name), version: String(extra.version) };
|
|
1011
|
+
const known = knownTokenDomain(chainId, asset);
|
|
1012
|
+
if (known)
|
|
1013
|
+
return known;
|
|
1014
|
+
let name = '', version = '';
|
|
1015
|
+
try {
|
|
1016
|
+
name = decodeAbiString(await ethCallHex(chainId, asset, '0x06fdde03')); // name()
|
|
1017
|
+
version = decodeAbiString(await ethCallHex(chainId, asset, '0x54fd4d50')); // version()
|
|
1018
|
+
}
|
|
1019
|
+
catch { /* refused below */ }
|
|
1020
|
+
if (!name || !version) {
|
|
1021
|
+
throw new Error(`x402: cannot determine the EIP-712 domain for token ${asset} on chain ${chainId}: the endpoint sent no ` +
|
|
1022
|
+
`extra.name/extra.version and the contract did not answer name()/version(). Refused rather than signed with a guessed domain.`);
|
|
1023
|
+
}
|
|
1024
|
+
return { name, version };
|
|
1025
|
+
}
|
|
1026
|
+
/** Sign the authorization with whichever custody mode is active (local key, or the hosted signer). */
|
|
1027
|
+
async function signAuthorization(walletId, chainId, asset, domain, auth, approvalId) {
|
|
1028
|
+
const r = (await api(`/wallets/${walletId}/x402/authorize`, 'POST', {
|
|
1029
|
+
chain_id: chainId, asset, name: domain.name, version: domain.version,
|
|
1030
|
+
to: auth.to, value: auth.value, valid_after: auth.validAfter, valid_before: auth.validBefore, nonce: auth.nonce,
|
|
1031
|
+
...(approvalId ? { approval_id: approvalId } : {}),
|
|
1032
|
+
}, { 'X-AGW-SKIP-X402': 'true' }));
|
|
1033
|
+
const sig = String(r?.signature || '');
|
|
1034
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(sig)) {
|
|
1035
|
+
throw new Error(`x402: the wallet did not return a valid authorization signature${r?.error ? ` (${r.error})` : ''}.`);
|
|
1036
|
+
}
|
|
1037
|
+
return sig;
|
|
1038
|
+
}
|
|
1039
|
+
/** Sign a Permit2 upto authorization with whichever custody mode is active. */
|
|
1040
|
+
async function signPermit2(walletId, chainId, auth, approvalId) {
|
|
1041
|
+
const r = (await api(`/wallets/${walletId}/x402/permit2`, 'POST', {
|
|
1042
|
+
chain_id: chainId, asset: auth.permitted.token, amount: auth.permitted.amount, spender: auth.spender, permit2: PERMIT2_ADDRESS,
|
|
1043
|
+
nonce: auth.nonce, deadline: auth.deadline, to: auth.witness.to, facilitator: auth.witness.facilitator, valid_after: auth.witness.validAfter,
|
|
1044
|
+
...(approvalId ? { approval_id: approvalId } : {}),
|
|
1045
|
+
}, { 'X-AGW-SKIP-X402': 'true' }));
|
|
1046
|
+
const sig = String(r?.signature || '');
|
|
1047
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(sig))
|
|
1048
|
+
throw new Error(`x402 upto: the wallet did not return a valid Permit2 signature${r?.error ? ` (${r.error})` : ''}.`);
|
|
1049
|
+
return sig;
|
|
1050
|
+
}
|
|
938
1051
|
// ─── Tool: pay_x402 ─────────────────────────────────────────────
|
|
939
1052
|
server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server returns HTTP 402 Payment Required, ' +
|
|
940
|
-
'parses the payment requirements
|
|
941
|
-
'
|
|
942
|
-
'
|
|
1053
|
+
'parses the payment requirements (v1 body or v2 PAYMENT-REQUIRED header), signs an EIP-3009 ' +
|
|
1054
|
+
'TransferWithAuthorization for the "exact" scheme (no gas, nothing broadcast by the payer), and retries ' +
|
|
1055
|
+
'the request with the payment header. AgentWallet paywalls are paid by on-chain transfer instead. ' +
|
|
1056
|
+
'The "upto" scheme is refused, never approximated. Returns the final response and the settlement ' +
|
|
1057
|
+
'receipt. Supports the x402 open payment standard (https://x402.org). Set max_payment to cap spend.', {
|
|
943
1058
|
url: z.string().url().describe('The URL to access (will handle 402 payment if required)'),
|
|
944
1059
|
wallet_id: z.number().int().describe('Wallet ID to pay from'),
|
|
945
1060
|
method: z.string().default('GET').describe('HTTP method (GET, POST, PUT, DELETE)'),
|
|
@@ -954,7 +1069,10 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
954
1069
|
'to allow a larger single payment.'),
|
|
955
1070
|
prefer_chain: z.number().int().optional().describe('Preferred chain ID if the server accepts payment on multiple chains ' +
|
|
956
1071
|
'(e.g. 8453 for Base, 1 for Ethereum)'),
|
|
957
|
-
|
|
1072
|
+
request_approval: z.boolean().optional().describe('Hosted wallets only. When the payment exceeds the cap, email the wallet owner an approve/deny link ' +
|
|
1073
|
+
'instead of refusing, and return an approval_id to retry with. Default true unless AGENTWALLET_APPROVALS=0.'),
|
|
1074
|
+
approval_id: z.string().optional().describe('An approval id from an earlier over-cap attempt. Once the owner has approved it, pass it here to make that one payment.'),
|
|
1075
|
+
}, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain, request_approval, approval_id }) => {
|
|
958
1076
|
// Build request headers
|
|
959
1077
|
const reqHeaders = { Accept: 'application/json' };
|
|
960
1078
|
if (headersJson) {
|
|
@@ -999,125 +1117,171 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
999
1117
|
response: parsed,
|
|
1000
1118
|
});
|
|
1001
1119
|
}
|
|
1002
|
-
// Step 2:
|
|
1003
|
-
|
|
1120
|
+
// Step 2: Requirements come from the PAYMENT-REQUIRED header (v2) or the JSON body (v1).
|
|
1121
|
+
const initialText = await initialRes.text();
|
|
1122
|
+
let initialBody = null;
|
|
1004
1123
|
try {
|
|
1005
|
-
|
|
1124
|
+
initialBody = JSON.parse(initialText);
|
|
1006
1125
|
}
|
|
1007
1126
|
catch {
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1021
|
-
// Resolve the chain
|
|
1022
|
-
const chainId = resolveChainId(option.network);
|
|
1023
|
-
if (!chainId) {
|
|
1024
|
-
throw new Error(`Unsupported x402 network: "${option.network}". ` +
|
|
1025
|
-
`Supported: ${Object.keys(X402_NETWORKS).join(', ')}, or any CAIP-2 / numeric chain ID.`);
|
|
1026
|
-
}
|
|
1027
|
-
// Decimals come from the registry or the token contract, NEVER from the 402
|
|
1028
|
-
// body. The endpoint controls requiredDecimals, so trusting it lets a
|
|
1029
|
-
// hostile server inflate the cap AND misreport the amount shown to the
|
|
1030
|
-
// agent. The declared value is only used to detect a mismatch and refuse.
|
|
1031
|
-
const assetForDecimals = option.asset || option.extra?.token || '';
|
|
1032
|
-
const trustedDecimals = assetForDecimals
|
|
1033
|
-
? await resolveTrustedDecimals(chainId, assetForDecimals)
|
|
1034
|
-
: nativeDecimals(chainId); // 18 on EVM (wei), 9 on Solana (lamports)
|
|
1035
|
-
assertDeclaredDecimals(option.requiredDecimals, trustedDecimals);
|
|
1036
|
-
// Human-readable amount, computed with trusted decimals so what the agent
|
|
1037
|
-
// is told matches what will actually leave the wallet.
|
|
1038
|
-
const amount = formatUnits(option.maxAmountRequired, trustedDecimals);
|
|
1039
|
-
// Step 4: Enforce a hard per-payment cap. ALWAYS applied — if the caller
|
|
1040
|
-
// omits max_payment we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"),
|
|
1041
|
-
// mirroring the internal auto-pay path (deriveX402Payment). Without this, an
|
|
1042
|
-
// untrusted or compromised x402 endpoint could return an arbitrarily large
|
|
1043
|
-
// maxAmountRequired and drain the wallet. The cap is never silently skipped.
|
|
1044
|
-
const capSource = max_payment ? 'max_payment' : 'AGENTWALLET_MAX_AUTOPAY';
|
|
1045
|
-
const effectiveMax = max_payment || process.env.AGENTWALLET_MAX_AUTOPAY || '1';
|
|
1046
|
-
if (!isWithinCap(option.maxAmountRequired, trustedDecimals, effectiveMax)) {
|
|
1127
|
+
initialBody = null;
|
|
1128
|
+
}
|
|
1129
|
+
const paymentInfo = parsePaymentRequired(n => initialRes.headers.get(n), initialBody);
|
|
1130
|
+
if (!paymentInfo || !Array.isArray(paymentInfo.accepts) || paymentInfo.accepts.length === 0) {
|
|
1131
|
+
throw new Error('402 response carried no x402 payment requirements (no PAYMENT-REQUIRED header and no "accepts" array in the body). ' +
|
|
1132
|
+
'This server may not support x402.');
|
|
1133
|
+
}
|
|
1134
|
+
const x402Version = Number(paymentInfo.x402Version) || 1;
|
|
1135
|
+
const accepts = paymentInfo.accepts;
|
|
1136
|
+
// Step 3: Pick an option this client can sign: exact (EIP-3009) first, then a signable upto (Permit2).
|
|
1137
|
+
const picked = pickOption(accepts, resolveChainId, prefer_chain);
|
|
1138
|
+
if (!picked.option || !picked.chainId) {
|
|
1047
1139
|
return jsonResponse({
|
|
1048
1140
|
status: 402,
|
|
1049
1141
|
payment_required: true,
|
|
1050
1142
|
payment_made: false,
|
|
1051
|
-
error:
|
|
1052
|
-
|
|
1053
|
-
(max_payment ? '' : ' No max_payment was provided, so the default cap was applied. ' +
|
|
1054
|
-
'Pass max_payment (or raise AGENTWALLET_MAX_AUTOPAY) to allow a larger payment.'),
|
|
1055
|
-
required_amount: amount,
|
|
1056
|
-
max_allowed: effectiveMax,
|
|
1057
|
-
cap_source: capSource,
|
|
1058
|
-
token: option.extra?.name || 'native',
|
|
1059
|
-
network: option.network,
|
|
1060
|
-
chain_id: chainId,
|
|
1061
|
-
pay_to: option.payTo,
|
|
1062
|
-
description: option.description,
|
|
1143
|
+
error: picked.reason,
|
|
1144
|
+
offered: accepts.map(a => ({ scheme: a.scheme, network: a.network, amount: requiredAmount(a), asset: a.asset ?? a.extra?.token ?? null })),
|
|
1063
1145
|
});
|
|
1064
1146
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1147
|
+
const option = picked.option;
|
|
1148
|
+
const chainId = picked.chainId;
|
|
1149
|
+
const isUpto = option.scheme === 'upto';
|
|
1150
|
+
const rawAmount = requiredAmount(option);
|
|
1151
|
+
if (!/^\d+$/.test(rawAmount)) {
|
|
1152
|
+
throw new Error(`x402: invalid amount "${rawAmount}" in the payment requirements (expected integer base units).`);
|
|
1153
|
+
}
|
|
1154
|
+
// Decimals come from the registry or the token contract, NEVER from the 402
|
|
1155
|
+
// body. The endpoint controls what it declares, so trusting it would let a
|
|
1156
|
+
// hostile server inflate the cap and misreport the amount shown to the agent.
|
|
1067
1157
|
const tokenAddress = option.asset || option.extra?.token || '';
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1158
|
+
const trustedDecimals = tokenAddress
|
|
1159
|
+
? await resolveTrustedDecimals(chainId, tokenAddress)
|
|
1160
|
+
: nativeDecimals(chainId); // 18 on EVM (wei), 9 on Solana (lamports)
|
|
1161
|
+
if (typeof option.requiredDecimals === 'number')
|
|
1162
|
+
assertDeclaredDecimals(option.requiredDecimals, trustedDecimals);
|
|
1163
|
+
const amount = formatUnits(rawAmount, trustedDecimals);
|
|
1164
|
+
const tokenLabel = option.extra?.name || (tokenAddress ? 'tokens' : 'native');
|
|
1165
|
+
const skip = { 'X-AGW-SKIP-X402': 'true' };
|
|
1166
|
+
// Step 4: Hard per-payment cap, ALWAYS applied. If the caller omits max_payment
|
|
1167
|
+
// we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"). Above the cap a hosted
|
|
1168
|
+
// wallet can ask its owner instead of refusing: an approval is created, the
|
|
1169
|
+
// owner gets approve/deny links by email, and the agent retries with the id.
|
|
1170
|
+
// For upto the cap applies to the MAXIMUM the seller may settle.
|
|
1171
|
+
const capSource = max_payment ? 'max_payment' : 'AGENTWALLET_MAX_AUTOPAY';
|
|
1172
|
+
const effectiveMax = max_payment || process.env.AGENTWALLET_MAX_AUTOPAY || '1';
|
|
1173
|
+
let approvalUsed = null;
|
|
1174
|
+
if (!isWithinCap(rawAmount, trustedDecimals, effectiveMax)) {
|
|
1175
|
+
const overCap = {
|
|
1176
|
+
status: 402, payment_required: true, payment_made: false,
|
|
1177
|
+
required_amount: amount, max_allowed: effectiveMax, cap_source: capSource, token: tokenLabel,
|
|
1178
|
+
network: option.network, chain_id: chainId, pay_to: option.payTo, description: option.description,
|
|
1179
|
+
};
|
|
1180
|
+
const wantApproval = request_approval ?? (process.env.AGENTWALLET_APPROVALS !== '0');
|
|
1181
|
+
if (approval_id) {
|
|
1182
|
+
const a = (await api(`/approvals/${encodeURIComponent(approval_id)}`, 'GET', undefined, skip));
|
|
1183
|
+
if (a?.status !== 'approved') {
|
|
1184
|
+
return jsonResponse({ ...overCap, approval_id, approval_status: a?.status ?? null, error: `Approval ${approval_id} is ${a?.status ?? 'unknown'}, not approved.${a?.error ? ' ' + a.error : ''}` });
|
|
1185
|
+
}
|
|
1186
|
+
if (String(a.pay_to || '').toLowerCase() !== option.payTo.toLowerCase() || Number(a.chain_id) !== chainId || BigInt(String(a.value || '0')) < BigInt(rawAmount)) {
|
|
1187
|
+
return jsonResponse({ ...overCap, approval_id, error: 'Approval does not cover this payment (recipient, chain or amount differ).' });
|
|
1188
|
+
}
|
|
1189
|
+
approvalUsed = String(approval_id);
|
|
1190
|
+
}
|
|
1191
|
+
else if (wantApproval && !anyLocalMode()) {
|
|
1192
|
+
const created = (await api('/approvals', 'POST', {
|
|
1193
|
+
wallet_id, chain_id: chainId, scheme: option.scheme, asset: tokenAddress, pay_to: option.payTo, value: rawAmount,
|
|
1194
|
+
amount_human: amount, token_name: tokenLabel, url,
|
|
1195
|
+
}, skip));
|
|
1196
|
+
if (!created?.id) {
|
|
1197
|
+
return jsonResponse({ ...overCap, error: `Payment of ${amount} ${tokenLabel} exceeds the ${effectiveMax} cap and the approval request failed: ${created?.error || 'unknown error'}.` });
|
|
1198
|
+
}
|
|
1199
|
+
return jsonResponse({
|
|
1200
|
+
...overCap, approval_pending: true, approval_id: String(created.id), expires_at: created.expires_at ?? null,
|
|
1201
|
+
error: `Payment of ${amount} ${tokenLabel} exceeds the ${effectiveMax} cap (from ${capSource}). The wallet owner has been emailed approve/deny links ` +
|
|
1202
|
+
`(approval ${created.id}, valid 24 hours). Check it with check_approval, then call pay_x402 again with approval_id "${created.id}".`,
|
|
1203
|
+
});
|
|
1081
1204
|
}
|
|
1082
1205
|
else {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1206
|
+
return jsonResponse({
|
|
1207
|
+
...overCap,
|
|
1208
|
+
error: `Payment of ${amount} ${tokenLabel} exceeds the ${effectiveMax} cap (from ${capSource}).` +
|
|
1209
|
+
(max_payment ? '' : ' No max_payment was provided, so the default cap was applied.') +
|
|
1210
|
+
(anyLocalMode() ? ' Local mode has no approval channel: raise max_payment, AGENTWALLET_MAX_AUTOPAY or AGENTWALLET_MAX_TX_TOKEN yourself.'
|
|
1211
|
+
: ' Pass request_approval=true to email the wallet owner for a one-time approval, or raise the cap.'),
|
|
1212
|
+
});
|
|
1089
1213
|
}
|
|
1090
1214
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1215
|
+
// Step 5: Pay. Standard x402 "exact" on EVM is an EIP-3009 authorization the
|
|
1216
|
+
// facilitator settles; "upto" is a Permit2 max-authorization settled at actual
|
|
1217
|
+
// usage; the payer broadcasts nothing and pays no gas for either. AgentWallet's
|
|
1218
|
+
// own paywalls (verified by receipt), native-asset requests and Solana still use
|
|
1219
|
+
// a broadcast transfer proved by hash.
|
|
1220
|
+
const standardExact = !isUpto && !isSolanaChain(chainId) && Boolean(tokenAddress) && !isLegacyAgentWalletAccept(option);
|
|
1221
|
+
let headerName = 'X-PAYMENT';
|
|
1222
|
+
let paymentHeader = '';
|
|
1223
|
+
let txHash = null;
|
|
1224
|
+
let authorization = null;
|
|
1225
|
+
let uptoAuth = null;
|
|
1226
|
+
let payer = null;
|
|
1227
|
+
let permit2ApprovalTx = null;
|
|
1228
|
+
if (isUpto) {
|
|
1229
|
+
payer = await payerAddress(wallet_id);
|
|
1230
|
+
const allowance = decodeUint(await ethCallHex(chainId, tokenAddress, permit2AllowanceCalldata(payer)));
|
|
1231
|
+
if (allowance < BigInt(rawAmount)) {
|
|
1232
|
+
if (process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1') {
|
|
1233
|
+
const r = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: permit2ApproveCalldata(), chain_id: chainId }, skip));
|
|
1234
|
+
permit2ApprovalTx = String(r?.tx_hash || '');
|
|
1235
|
+
await new Promise(res => setTimeout(res, 4000));
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
return jsonResponse({
|
|
1239
|
+
status: 402, payment_required: true, payment_made: false, permit2_approval_needed: true,
|
|
1240
|
+
token: tokenLabel, token_address: tokenAddress, chain_id: chainId, payer, permit2: PERMIT2_ADDRESS,
|
|
1241
|
+
error: `This endpoint uses the x402 "upto" scheme, which settles through Permit2. Wallet ${payer} has not approved ` +
|
|
1242
|
+
`${tokenLabel} to Permit2 on chain ${chainId}. Call approve_permit2 once for this token (a normal transaction that needs gas), ` +
|
|
1243
|
+
`or set AGENTWALLET_PERMIT2_AUTO_APPROVE=1, then call pay_x402 again.`,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
uptoAuth = buildUptoAuthorization(payer, option);
|
|
1248
|
+
const signature = await signPermit2(wallet_id, chainId, uptoAuth, approvalUsed);
|
|
1249
|
+
const payload = buildPaymentPayloadRaw(x402Version, option, uptoPayload(uptoAuth, signature), paymentInfo.resource, paymentInfo.extensions);
|
|
1250
|
+
const h = paymentHeaderFor(x402Version, payload);
|
|
1251
|
+
headerName = h.name;
|
|
1252
|
+
paymentHeader = h.value;
|
|
1253
|
+
}
|
|
1254
|
+
else if (standardExact) {
|
|
1255
|
+
payer = await payerAddress(wallet_id);
|
|
1256
|
+
const domain = await resolveTokenDomain(chainId, tokenAddress, option.extra);
|
|
1257
|
+
authorization = buildAuthorization(payer, option);
|
|
1258
|
+
const signature = await signAuthorization(wallet_id, chainId, tokenAddress, domain, authorization, approvalUsed);
|
|
1259
|
+
const payload = buildPaymentPayload(x402Version, option, authorization, signature, paymentInfo.resource, paymentInfo.extensions);
|
|
1260
|
+
const h = paymentHeaderFor(x402Version, payload);
|
|
1261
|
+
headerName = h.name;
|
|
1262
|
+
paymentHeader = h.value;
|
|
1100
1263
|
}
|
|
1101
1264
|
else {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1265
|
+
let txResult;
|
|
1266
|
+
if (isSolanaChain(chainId)) {
|
|
1267
|
+
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', tokenAddress
|
|
1268
|
+
? { to: option.payTo, value: rawAmount, token_mint: tokenAddress, token_decimals: trustedDecimals, chain_id: chainId }
|
|
1269
|
+
: { to: option.payTo, value: rawAmount, chain_id: chainId }));
|
|
1270
|
+
}
|
|
1271
|
+
else if (tokenAddress) {
|
|
1272
|
+
const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(rawAmount);
|
|
1273
|
+
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: calldata, chain_id: chainId }));
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: option.payTo, value: rawAmount, data: '', chain_id: chainId }));
|
|
1277
|
+
}
|
|
1278
|
+
txHash = String(txResult.tx_hash || txResult.signature || '');
|
|
1279
|
+
if (!txHash)
|
|
1280
|
+
throw new Error('x402: the payment transaction returned no hash.');
|
|
1281
|
+
paymentHeader = Buffer.from(JSON.stringify({ x402Version, scheme: option.scheme, network: option.network, payload: { txHash } })).toString('base64');
|
|
1282
|
+
}
|
|
1283
|
+
// Step 6: Retry with the payment header and read the settlement receipt.
|
|
1284
|
+
const retryHeaders = { ...reqHeaders, [headerName]: paymentHeader };
|
|
1121
1285
|
const retryOptions = {
|
|
1122
1286
|
method,
|
|
1123
1287
|
headers: retryHeaders,
|
|
@@ -1135,21 +1299,61 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1135
1299
|
catch {
|
|
1136
1300
|
retryParsed = retryText;
|
|
1137
1301
|
}
|
|
1302
|
+
const settlement = parseSettlement(n => retryRes.headers.get(n));
|
|
1303
|
+
// A second 402 means the facilitator declined (unfunded payer, expired window, bad domain...). v2 servers
|
|
1304
|
+
// put the reason in a fresh PAYMENT-REQUIRED header, v1 servers in the body; surface it instead of {}.
|
|
1305
|
+
let retryError = null;
|
|
1306
|
+
if (retryRes.status === 402) {
|
|
1307
|
+
const again = parsePaymentRequired(n => retryRes.headers.get(n), retryParsed);
|
|
1308
|
+
retryError = String(again?.error || retryParsed?.error || 'Payment was not accepted (no reason given).');
|
|
1309
|
+
}
|
|
1138
1310
|
return jsonResponse({
|
|
1139
1311
|
status: retryRes.status,
|
|
1140
1312
|
payment_required: true,
|
|
1141
|
-
payment_made:
|
|
1313
|
+
payment_made: retryRes.status !== 402,
|
|
1314
|
+
retry_error: retryError,
|
|
1315
|
+
payment_method: isUpto ? 'permit2-upto-authorization' : (standardExact ? 'eip3009-authorization' : 'onchain-transfer'),
|
|
1316
|
+
approval_id: approvalUsed,
|
|
1317
|
+
permit2_approval_tx: permit2ApprovalTx,
|
|
1318
|
+
x402_version: x402Version,
|
|
1142
1319
|
amount,
|
|
1143
|
-
token:
|
|
1144
|
-
token_address:
|
|
1320
|
+
token: tokenLabel,
|
|
1321
|
+
token_address: tokenAddress || null,
|
|
1145
1322
|
network: option.network,
|
|
1146
1323
|
chain_id: chainId,
|
|
1147
1324
|
pay_to: option.payTo,
|
|
1148
|
-
|
|
1325
|
+
payer,
|
|
1326
|
+
tx_hash: txHash ?? settlement?.transaction ?? null,
|
|
1327
|
+
settlement,
|
|
1328
|
+
authorization: authorization ? { nonce: authorization.nonce, valid_before: authorization.validBefore }
|
|
1329
|
+
: (uptoAuth ? { scheme: 'upto', max_amount: amount, nonce: uptoAuth.nonce, deadline: uptoAuth.deadline, facilitator: uptoAuth.witness.facilitator } : null),
|
|
1149
1330
|
description: option.description,
|
|
1150
1331
|
response: retryParsed,
|
|
1151
1332
|
});
|
|
1152
1333
|
});
|
|
1334
|
+
// ─── Tool: check_approval ───────────────────────────────────────
|
|
1335
|
+
server.tool('check_approval', 'Status of a payment approval created when pay_x402 exceeded the cap on a hosted wallet. ' +
|
|
1336
|
+
'Returns pending, approved, denied or expired. Once approved, call pay_x402 again with approval_id.', { approval_id: z.string().describe('Approval id returned by pay_x402') }, async ({ approval_id }) => jsonResponse(await api(`/approvals/${encodeURIComponent(approval_id)}`, 'GET', undefined, { 'X-AGW-SKIP-X402': 'true' })));
|
|
1337
|
+
// ─── Tool: approve_permit2 ──────────────────────────────────────
|
|
1338
|
+
server.tool('approve_permit2', 'One-time ERC-20 approval of a token to the Permit2 contract, needed before paying x402 "upto" endpoints with that token. ' +
|
|
1339
|
+
'A normal on-chain transaction (needs gas). Approves the maximum so it never has to be repeated; Permit2 itself only moves ' +
|
|
1340
|
+
'what each signed authorization allows.', {
|
|
1341
|
+
wallet_id: z.number().int().describe('Wallet ID'),
|
|
1342
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token address (e.g. USDC on Base)'),
|
|
1343
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
1344
|
+
}, async ({ wallet_id, token, chain_id }) => {
|
|
1345
|
+
if (isSolanaChain(chain_id))
|
|
1346
|
+
throw new Error('Permit2 is an EVM contract; there is nothing to approve on Solana.');
|
|
1347
|
+
const result = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: token, value: '0', data: permit2ApproveCalldata(), chain_id }));
|
|
1348
|
+
return jsonResponse({ ...result, token, spender: PERMIT2_ADDRESS, amount: 'unlimited', note: 'Permit2 only transfers what a signed authorization allows; the allowance itself moves nothing.' });
|
|
1349
|
+
});
|
|
1350
|
+
// ─── Tool: check_token_risk ─────────────────────────────────────
|
|
1351
|
+
server.tool('check_token_risk', 'Assess an ERC-20 token before approving or swapping it: honeypot, taxes, owner powers, verified source, ' +
|
|
1352
|
+
'holder concentration, DEX liquidity. Uses GoPlus Security (free, no key) with an on-chain fallback. ' +
|
|
1353
|
+
'A warning for the caller to weigh, never a block.', {
|
|
1354
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
1355
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
1356
|
+
}, async ({ token, chain_id }) => jsonResponse({ token, chain_id, risk: await assessTokenRisk(chain_id, token, (to, data) => ethCallHex(chain_id, to, data)) }));
|
|
1153
1357
|
// ─── Tool: get_usage ─────────────────────────────────────────────
|
|
1154
1358
|
server.tool('get_usage', 'Get the current month\'s usage statistics. ' +
|
|
1155
1359
|
'Returns operations count, tier info, remaining quota, and fees.', {}, async () => {
|
package/build/local-wallet.d.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* on the custodial path and say so explicitly rather than silently downgrading.
|
|
16
16
|
*/
|
|
17
17
|
import { type Address, type Hex } from 'viem';
|
|
18
|
+
import { type Eip3009Authorization } from './x402-eip3009.js';
|
|
19
|
+
import { type UptoPermit2Authorization } from './x402-permit2.js';
|
|
18
20
|
/** True when the server is running non-custodially. */
|
|
19
21
|
export declare function isLocalMode(): boolean;
|
|
20
22
|
export declare function getLocalAccount(): {
|
|
@@ -88,3 +90,27 @@ export declare function localWalletRecord(): {
|
|
|
88
90
|
custody: string;
|
|
89
91
|
note: string;
|
|
90
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Sign a TransferWithAuthorization for an x402 payment. Nothing is broadcast;
|
|
95
|
+
* the resource server's facilitator settles it on-chain. The amount is put
|
|
96
|
+
* through the same AGENTWALLET_MAX_TX_TOKEN guard as a transfer, because that
|
|
97
|
+
* is exactly what the authorization lets someone else execute.
|
|
98
|
+
*/
|
|
99
|
+
export declare function localSignAuthorization(chainId: number, asset: Address, domain: {
|
|
100
|
+
name: string;
|
|
101
|
+
version: string;
|
|
102
|
+
}, auth: Eip3009Authorization): Promise<{
|
|
103
|
+
signature: `0x${string}`;
|
|
104
|
+
from: Address;
|
|
105
|
+
mode: 'local';
|
|
106
|
+
}>;
|
|
107
|
+
/** Read-only eth_call against the local RPC, same shape as the hosted /eth-call route. */
|
|
108
|
+
export declare function localEthCall(chainId: number, to: Address, data: Hex): Promise<{
|
|
109
|
+
result: string;
|
|
110
|
+
}>;
|
|
111
|
+
/** x402 "upto": sign a Permit2 max-authorization in-process. The maximum goes through the token cap like a transfer would. */
|
|
112
|
+
export declare function localSignPermit2Upto(chainId: number, auth: UptoPermit2Authorization): Promise<{
|
|
113
|
+
signature: `0x${string}`;
|
|
114
|
+
from: Address;
|
|
115
|
+
mode: 'local';
|
|
116
|
+
}>;
|