@axonfi/sdk 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Giving bots funded wallets is risky: scattered keys, no spending controls, one c
14
14
  - **Bounded risk** — per-tx caps, daily limits, velocity windows, destination whitelists. Bots can only operate within the policies you set.
15
15
  - **AI verification** — 3-agent LLM consensus (safety, behavioral, reasoning) for flagged transactions. 2/3 consensus required.
16
16
  - **Gasless bots** — bots sign EIP-712 intents off-chain. Axon's relayer handles gas, simulation, and on-chain execution.
17
- - **Multi-chain** — Base, Arbitrum, Optimism, Polygon. USDC as base asset.
17
+ - **Multi-chain** — Base, Arbitrum. USDC as base asset.
18
18
 
19
19
  Your agents pay. You stay in control.
20
20
 
@@ -185,6 +185,28 @@ Payments resolve through one of three paths:
185
185
  | **AI Scan** | Exceeds AI threshold | ~30s | `status: "approved"` or routes to review |
186
186
  | **Human Review** | No AI consensus | Async | `status: "pending_review"`, poll for result |
187
187
 
188
+ ## HTTP 402 Paywalls (x402)
189
+
190
+ The SDK handles [x402](https://www.x402.org/) paywalls — APIs that charge per-request via HTTP 402 Payment Required. When a bot hits a paywall, the SDK parses the payment requirements, funds the bot from the vault, signs a token authorization, and returns a header for the retry.
191
+
192
+ ```typescript
193
+ const response = await fetch('https://api.example.com/data');
194
+
195
+ if (response.status === 402) {
196
+ // SDK handles everything: parse header, fund bot from vault, sign authorization
197
+ const result = await axon.x402.handlePaymentRequired(response.headers);
198
+
199
+ // Retry with the payment signature
200
+ const data = await fetch('https://api.example.com/data', {
201
+ headers: { 'PAYMENT-SIGNATURE': result.paymentSignature },
202
+ });
203
+ }
204
+ ```
205
+
206
+ The full pipeline applies — spending limits, AI verification, human review — even for 402 payments. Vault owners see every paywall payment in the dashboard with the resource URL, merchant address, and amount.
207
+
208
+ Supports EIP-3009 (USDC, gasless) and Permit2 (any ERC-20) settlement schemes.
209
+
188
210
  ## Security Model
189
211
 
190
212
  - **Owners** control everything: bot whitelist, spending limits, withdrawal. Hardware wallet recommended.
@@ -199,8 +221,7 @@ Payments resolve through one of three paths:
199
221
  | Base Sepolia | 84532 | Live |
200
222
  | Base | 8453 | Coming soon |
201
223
  | Arbitrum One | 42161 | Coming soon |
202
- | Optimism | 10 | Coming soon |
203
- | Polygon PoS | 137 | Coming soon |
224
+ | Arbitrum Sepolia | 421614 | Live |
204
225
 
205
226
  ## Links
206
227
 
package/dist/index.cjs CHANGED
@@ -3032,14 +3032,371 @@ function generateUuid() {
3032
3032
  const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3033
3033
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3034
3034
  }
3035
+
3036
+ // src/x402.ts
3037
+ function parsePaymentRequired(headerValue) {
3038
+ let decoded;
3039
+ try {
3040
+ decoded = atob(headerValue);
3041
+ } catch {
3042
+ decoded = headerValue;
3043
+ }
3044
+ const parsed = JSON.parse(decoded);
3045
+ if (!parsed.accepts || !Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
3046
+ throw new Error("x402: no payment options in PAYMENT-REQUIRED header");
3047
+ }
3048
+ if (!parsed.resource) {
3049
+ throw new Error("x402: missing resource in PAYMENT-REQUIRED header");
3050
+ }
3051
+ return parsed;
3052
+ }
3053
+ function parseChainId(network) {
3054
+ const parts = network.split(":");
3055
+ if (parts.length !== 2 || parts[0] !== "eip155") {
3056
+ throw new Error(`x402: unsupported network format "${network}" (expected "eip155:<chainId>")`);
3057
+ }
3058
+ const chainId = parseInt(parts[1], 10);
3059
+ if (isNaN(chainId)) {
3060
+ throw new Error(`x402: invalid chain ID in network "${network}"`);
3061
+ }
3062
+ return chainId;
3063
+ }
3064
+ function findMatchingOption(accepts, chainId) {
3065
+ const matchingOptions = [];
3066
+ for (const option of accepts) {
3067
+ try {
3068
+ const optionChainId = parseChainId(option.network);
3069
+ if (optionChainId === chainId) {
3070
+ matchingOptions.push(option);
3071
+ }
3072
+ } catch {
3073
+ continue;
3074
+ }
3075
+ }
3076
+ if (matchingOptions.length === 0) return null;
3077
+ const usdcAddress = USDC[chainId]?.toLowerCase();
3078
+ if (usdcAddress) {
3079
+ const usdcOption = matchingOptions.find((opt) => opt.asset.toLowerCase() === usdcAddress);
3080
+ if (usdcOption) return usdcOption;
3081
+ }
3082
+ return matchingOptions[0] ?? null;
3083
+ }
3084
+ function extractX402Metadata(parsed, selectedOption) {
3085
+ const metadata = {};
3086
+ if (parsed.x402Version !== void 0) {
3087
+ metadata.x402_version = String(parsed.x402Version);
3088
+ }
3089
+ if (selectedOption.scheme) {
3090
+ metadata.x402_scheme = selectedOption.scheme;
3091
+ }
3092
+ if (parsed.resource.mimeType) {
3093
+ metadata.x402_mime_type = parsed.resource.mimeType;
3094
+ }
3095
+ if (selectedOption.payTo) {
3096
+ metadata.x402_merchant = selectedOption.payTo;
3097
+ }
3098
+ if (parsed.resource.description) {
3099
+ metadata.x402_resource_description = parsed.resource.description;
3100
+ }
3101
+ return {
3102
+ resourceUrl: parsed.resource.url,
3103
+ memo: parsed.resource.description ?? null,
3104
+ recipientLabel: selectedOption.payTo ? `${selectedOption.payTo.slice(0, 6)}...${selectedOption.payTo.slice(-4)}` : null,
3105
+ metadata
3106
+ };
3107
+ }
3108
+ function formatPaymentSignature(payload) {
3109
+ const json = JSON.stringify(payload);
3110
+ return btoa(json);
3111
+ }
3112
+ var USDC_EIP712_DOMAIN = {
3113
+ // Base mainnet
3114
+ 8453: { name: "USD Coin", version: "2" },
3115
+ // Base Sepolia
3116
+ 84532: { name: "USDC", version: "2" },
3117
+ // Arbitrum One
3118
+ 42161: { name: "USD Coin", version: "2" },
3119
+ // Arbitrum Sepolia (same as mainnet convention)
3120
+ 421614: { name: "USDC", version: "2" }
3121
+ };
3122
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
3123
+ TransferWithAuthorization: [
3124
+ { name: "from", type: "address" },
3125
+ { name: "to", type: "address" },
3126
+ { name: "value", type: "uint256" },
3127
+ { name: "validAfter", type: "uint256" },
3128
+ { name: "validBefore", type: "uint256" },
3129
+ { name: "nonce", type: "bytes32" }
3130
+ ]
3131
+ };
3132
+ function randomNonce() {
3133
+ const bytes = new Uint8Array(32);
3134
+ crypto.getRandomValues(bytes);
3135
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
3136
+ }
3137
+ async function signTransferWithAuthorization(privateKey, chainId, auth) {
3138
+ const domainConfig = USDC_EIP712_DOMAIN[chainId];
3139
+ if (!domainConfig) {
3140
+ throw new Error(`EIP-3009 not configured for chain ${chainId}`);
3141
+ }
3142
+ const usdcAddress = USDC[chainId];
3143
+ if (!usdcAddress) {
3144
+ throw new Error(`USDC address not known for chain ${chainId}`);
3145
+ }
3146
+ const account = accounts.privateKeyToAccount(privateKey);
3147
+ return account.signTypedData({
3148
+ domain: {
3149
+ name: domainConfig.name,
3150
+ version: domainConfig.version,
3151
+ chainId,
3152
+ verifyingContract: usdcAddress
3153
+ },
3154
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
3155
+ primaryType: "TransferWithAuthorization",
3156
+ message: {
3157
+ from: auth.from,
3158
+ to: auth.to,
3159
+ value: auth.value,
3160
+ validAfter: auth.validAfter,
3161
+ validBefore: auth.validBefore,
3162
+ nonce: auth.nonce
3163
+ }
3164
+ });
3165
+ }
3166
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
3167
+ var X402_PROXY_ADDRESS = "0x4020CD856C882D5fb903D99CE35316A085Bb0001";
3168
+ var WITNESS_TYPE_STRING = "TransferDetails witness)TokenPermissions(address token,uint256 amount)TransferDetails(address to,uint256 requestedAmount)";
3169
+ var PERMIT_WITNESS_TRANSFER_FROM_TYPES = {
3170
+ PermitWitnessTransferFrom: [
3171
+ { name: "permitted", type: "TokenPermissions" },
3172
+ { name: "spender", type: "address" },
3173
+ { name: "nonce", type: "uint256" },
3174
+ { name: "deadline", type: "uint256" },
3175
+ { name: "witness", type: "TransferDetails" }
3176
+ ],
3177
+ TokenPermissions: [
3178
+ { name: "token", type: "address" },
3179
+ { name: "amount", type: "uint256" }
3180
+ ],
3181
+ TransferDetails: [
3182
+ { name: "to", type: "address" },
3183
+ { name: "requestedAmount", type: "uint256" }
3184
+ ]
3185
+ };
3186
+ function randomPermit2Nonce() {
3187
+ const bytes = new Uint8Array(32);
3188
+ crypto.getRandomValues(bytes);
3189
+ let n = 0n;
3190
+ for (const b of bytes) {
3191
+ n = n << 8n | BigInt(b);
3192
+ }
3193
+ return n;
3194
+ }
3195
+ async function signPermit2WitnessTransfer(privateKey, chainId, permit) {
3196
+ const account = accounts.privateKeyToAccount(privateKey);
3197
+ return account.signTypedData({
3198
+ domain: {
3199
+ name: "Permit2",
3200
+ chainId,
3201
+ verifyingContract: PERMIT2_ADDRESS
3202
+ },
3203
+ types: PERMIT_WITNESS_TRANSFER_FROM_TYPES,
3204
+ primaryType: "PermitWitnessTransferFrom",
3205
+ message: {
3206
+ permitted: {
3207
+ token: permit.token,
3208
+ amount: permit.amount
3209
+ },
3210
+ spender: permit.spender,
3211
+ nonce: permit.nonce,
3212
+ deadline: permit.deadline,
3213
+ witness: {
3214
+ to: permit.witnessTo,
3215
+ requestedAmount: permit.witnessRequestedAmount
3216
+ }
3217
+ }
3218
+ });
3219
+ }
3220
+
3221
+ // src/client.ts
3035
3222
  var AxonClient = class {
3036
3223
  constructor(config) {
3224
+ // ============================================================================
3225
+ // x402 — HTTP 402 Payment Required
3226
+ // ============================================================================
3227
+ /**
3228
+ * x402 utilities for handling HTTP 402 Payment Required responses.
3229
+ *
3230
+ * The x402 flow:
3231
+ * 1. Bot hits an API that returns HTTP 402 + PAYMENT-REQUIRED header
3232
+ * 2. SDK parses the header, finds a matching payment option
3233
+ * 3. SDK funds the bot's EOA from the vault (full Axon pipeline applies)
3234
+ * 4. Bot signs an EIP-3009 or Permit2 authorization
3235
+ * 5. SDK returns a PAYMENT-SIGNATURE header for the bot to retry with
3236
+ *
3237
+ * @example
3238
+ * ```ts
3239
+ * const response = await fetch('https://api.example.com/data');
3240
+ * if (response.status === 402) {
3241
+ * const result = await client.x402.handlePaymentRequired(response.headers);
3242
+ * const data = await fetch('https://api.example.com/data', {
3243
+ * headers: { 'PAYMENT-SIGNATURE': result.paymentSignature },
3244
+ * });
3245
+ * }
3246
+ * ```
3247
+ */
3248
+ this.x402 = {
3249
+ /**
3250
+ * Fund the bot's EOA from the vault for x402 settlement.
3251
+ *
3252
+ * This is a regular Axon payment (to = bot's own address) that goes through
3253
+ * the full pipeline: policy engine, AI scan, human review if needed.
3254
+ *
3255
+ * @param amount - Amount in token base units
3256
+ * @param token - Token address (defaults to USDC on this chain)
3257
+ * @param metadata - Optional metadata for the payment record
3258
+ */
3259
+ fund: async (amount, token, metadata) => {
3260
+ const tokenAddress = token ?? USDC[this.chainId];
3261
+ if (!tokenAddress) {
3262
+ throw new Error(`No default USDC address for chain ${this.chainId}`);
3263
+ }
3264
+ return this.pay({
3265
+ to: this.botAddress,
3266
+ token: tokenAddress,
3267
+ amount,
3268
+ x402Funding: true,
3269
+ ...metadata
3270
+ });
3271
+ },
3272
+ /**
3273
+ * Handle a full x402 flow: parse header, fund bot, sign authorization, return header.
3274
+ *
3275
+ * Supports both EIP-3009 (USDC) and Permit2 (any ERC-20) settlement.
3276
+ * The bot's EOA is funded from the vault first (full Axon pipeline applies).
3277
+ *
3278
+ * @param headers - Response headers from the 402 response (must contain PAYMENT-REQUIRED)
3279
+ * @param maxTimeoutMs - Maximum time to wait for pending_review resolution (default: 120s)
3280
+ * @param pollIntervalMs - Polling interval for pending_review (default: 5s)
3281
+ * @returns Payment signature header value + funding details
3282
+ */
3283
+ handlePaymentRequired: async (headers, maxTimeoutMs = 12e4, pollIntervalMs = 5e3) => {
3284
+ const headerValue = headers instanceof Headers ? headers.get("payment-required") ?? headers.get("PAYMENT-REQUIRED") : headers["payment-required"] ?? headers["PAYMENT-REQUIRED"];
3285
+ if (!headerValue) {
3286
+ throw new Error("x402: no PAYMENT-REQUIRED header found");
3287
+ }
3288
+ const parsed = parsePaymentRequired(headerValue);
3289
+ const option = findMatchingOption(parsed.accepts, this.chainId);
3290
+ if (!option) {
3291
+ throw new Error(
3292
+ `x402: no payment option matches chain ${this.chainId}. Available: ${parsed.accepts.map((a) => a.network).join(", ")}`
3293
+ );
3294
+ }
3295
+ const x402Meta = extractX402Metadata(parsed, option);
3296
+ const amount = BigInt(option.amount);
3297
+ const tokenAddress = option.asset;
3298
+ const payInput = {
3299
+ to: this.botAddress,
3300
+ token: tokenAddress,
3301
+ amount,
3302
+ x402Funding: true,
3303
+ resourceUrl: x402Meta.resourceUrl,
3304
+ metadata: x402Meta.metadata
3305
+ };
3306
+ if (x402Meta.memo) payInput.memo = x402Meta.memo;
3307
+ if (x402Meta.recipientLabel) payInput.recipientLabel = x402Meta.recipientLabel;
3308
+ let fundingResult = await this.pay(payInput);
3309
+ if (fundingResult.status === "pending_review") {
3310
+ const deadline = Date.now() + maxTimeoutMs;
3311
+ while (fundingResult.status === "pending_review" && Date.now() < deadline) {
3312
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
3313
+ fundingResult = await this.poll(fundingResult.requestId);
3314
+ }
3315
+ if (fundingResult.status === "pending_review") {
3316
+ throw new Error(`x402: funding timed out after ${maxTimeoutMs}ms (still pending_review)`);
3317
+ }
3318
+ }
3319
+ if (fundingResult.status === "rejected") {
3320
+ throw new Error(`x402: funding rejected \u2014 ${fundingResult.reason ?? "unknown reason"}`);
3321
+ }
3322
+ const botPrivateKey = this.botPrivateKey;
3323
+ const payTo = option.payTo;
3324
+ const usdcAddress = USDC[this.chainId]?.toLowerCase();
3325
+ const isUsdc = tokenAddress.toLowerCase() === usdcAddress;
3326
+ let signaturePayload;
3327
+ if (isUsdc && USDC_EIP712_DOMAIN[this.chainId]) {
3328
+ const nonce = randomNonce();
3329
+ const validAfter = 0n;
3330
+ const validBefore = BigInt(Math.floor(Date.now() / 1e3) + 300);
3331
+ const sig = await signTransferWithAuthorization(botPrivateKey, this.chainId, {
3332
+ from: this.botAddress,
3333
+ to: payTo,
3334
+ value: amount,
3335
+ validAfter,
3336
+ validBefore,
3337
+ nonce
3338
+ });
3339
+ signaturePayload = {
3340
+ scheme: "exact",
3341
+ signature: sig,
3342
+ authorization: {
3343
+ from: this.botAddress,
3344
+ to: payTo,
3345
+ value: amount.toString(),
3346
+ validAfter: validAfter.toString(),
3347
+ validBefore: validBefore.toString(),
3348
+ nonce
3349
+ }
3350
+ };
3351
+ } else {
3352
+ const nonce = randomPermit2Nonce();
3353
+ const deadline = BigInt(Math.floor(Date.now() / 1e3) + 300);
3354
+ const sig = await signPermit2WitnessTransfer(botPrivateKey, this.chainId, {
3355
+ token: tokenAddress,
3356
+ amount,
3357
+ spender: X402_PROXY_ADDRESS,
3358
+ nonce,
3359
+ deadline,
3360
+ witnessTo: payTo,
3361
+ witnessRequestedAmount: amount
3362
+ });
3363
+ signaturePayload = {
3364
+ scheme: "permit2",
3365
+ signature: sig,
3366
+ permit: {
3367
+ permitted: { token: tokenAddress, amount: amount.toString() },
3368
+ spender: X402_PROXY_ADDRESS,
3369
+ nonce: nonce.toString(),
3370
+ deadline: deadline.toString()
3371
+ },
3372
+ witness: {
3373
+ to: payTo,
3374
+ requestedAmount: amount.toString()
3375
+ }
3376
+ };
3377
+ }
3378
+ const paymentSignature = formatPaymentSignature(signaturePayload);
3379
+ const handleResult = {
3380
+ paymentSignature,
3381
+ selectedOption: option,
3382
+ fundingResult: {
3383
+ requestId: fundingResult.requestId,
3384
+ status: fundingResult.status
3385
+ }
3386
+ };
3387
+ if (fundingResult.txHash) {
3388
+ handleResult.fundingResult.txHash = fundingResult.txHash;
3389
+ }
3390
+ return handleResult;
3391
+ }
3392
+ };
3037
3393
  this.vaultAddress = config.vaultAddress;
3038
3394
  this.chainId = config.chainId;
3039
- this.relayerUrl = "https://relay.axonfi.xyz";
3395
+ this.relayerUrl = config.relayerUrl ?? "https://relay.axonfi.xyz";
3040
3396
  if (!config.botPrivateKey) {
3041
3397
  throw new Error("botPrivateKey is required in AxonClientConfig");
3042
3398
  }
3399
+ this.botPrivateKey = config.botPrivateKey;
3043
3400
  this.walletClient = createAxonWalletClient(config.botPrivateKey, config.chainId);
3044
3401
  }
3045
3402
  // ============================================================================
@@ -3342,7 +3699,8 @@ Timestamp: ${timestamp}`;
3342
3699
  ...input.invoiceId !== void 0 && { invoiceId: input.invoiceId },
3343
3700
  ...input.orderId !== void 0 && { orderId: input.orderId },
3344
3701
  ...input.recipientLabel !== void 0 && { recipientLabel: input.recipientLabel },
3345
- ...input.metadata !== void 0 && { metadata: input.metadata }
3702
+ ...input.metadata !== void 0 && { metadata: input.metadata },
3703
+ ...input.x402Funding !== void 0 && { x402Funding: input.x402Funding }
3346
3704
  };
3347
3705
  return this._post(RELAYER_API.PAYMENTS, idempotencyKey, body);
3348
3706
  }
@@ -3916,19 +4274,26 @@ exports.EXPLORER_TX = EXPLORER_TX;
3916
4274
  exports.KNOWN_TOKENS = KNOWN_TOKENS;
3917
4275
  exports.NATIVE_ETH = NATIVE_ETH;
3918
4276
  exports.PAYMENT_INTENT_TYPEHASH = PAYMENT_INTENT_TYPEHASH;
4277
+ exports.PERMIT2_ADDRESS = PERMIT2_ADDRESS;
3919
4278
  exports.PaymentErrorCode = PaymentErrorCode;
3920
4279
  exports.RELAYER_API = RELAYER_API;
3921
4280
  exports.SUPPORTED_CHAIN_IDS = SUPPORTED_CHAIN_IDS;
3922
4281
  exports.SWAP_INTENT_TYPEHASH = SWAP_INTENT_TYPEHASH;
3923
4282
  exports.Token = Token;
3924
4283
  exports.USDC = USDC;
4284
+ exports.USDC_EIP712_DOMAIN = USDC_EIP712_DOMAIN;
3925
4285
  exports.WINDOW = WINDOW;
4286
+ exports.WITNESS_TYPE_STRING = WITNESS_TYPE_STRING;
4287
+ exports.X402_PROXY_ADDRESS = X402_PROXY_ADDRESS;
3926
4288
  exports.createAxonPublicClient = createAxonPublicClient;
3927
4289
  exports.createAxonWalletClient = createAxonWalletClient;
3928
4290
  exports.decryptKeystore = decryptKeystore;
3929
4291
  exports.deployVault = deployVault;
3930
4292
  exports.encodeRef = encodeRef;
3931
4293
  exports.encryptKeystore = encryptKeystore;
4294
+ exports.extractX402Metadata = extractX402Metadata;
4295
+ exports.findMatchingOption = findMatchingOption;
4296
+ exports.formatPaymentSignature = formatPaymentSignature;
3932
4297
  exports.getBotConfig = getBotConfig;
3933
4298
  exports.getChain = getChain;
3934
4299
  exports.getDomainSeparator = getDomainSeparator;
@@ -3945,10 +4310,16 @@ exports.isRebalanceTokenWhitelisted = isRebalanceTokenWhitelisted;
3945
4310
  exports.isVaultPaused = isVaultPaused;
3946
4311
  exports.operatorMaxDrainPerDay = operatorMaxDrainPerDay;
3947
4312
  exports.parseAmount = parseAmount;
4313
+ exports.parseChainId = parseChainId;
4314
+ exports.parsePaymentRequired = parsePaymentRequired;
4315
+ exports.randomNonce = randomNonce;
4316
+ exports.randomPermit2Nonce = randomPermit2Nonce;
3948
4317
  exports.resolveToken = resolveToken;
3949
4318
  exports.resolveTokenDecimals = resolveTokenDecimals;
3950
4319
  exports.signExecuteIntent = signExecuteIntent;
3951
4320
  exports.signPayment = signPayment;
4321
+ exports.signPermit2WitnessTransfer = signPermit2WitnessTransfer;
3952
4322
  exports.signSwapIntent = signSwapIntent;
4323
+ exports.signTransferWithAuthorization = signTransferWithAuthorization;
3953
4324
  //# sourceMappingURL=index.cjs.map
3954
4325
  //# sourceMappingURL=index.cjs.map