@haven_ai/sdk 0.1.26-alpha.0 → 0.1.28-alpha.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/dist/index.cjs CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var async_hooks = require('async_hooks');
4
- var schemes = require('x402/schemes');
5
3
  var viem = require('viem');
6
- var accounts = require('viem/accounts');
7
4
  var ethers = require('ethers');
8
5
  var crypto = require('crypto');
6
+ var async_hooks = require('async_hooks');
7
+ var schemes = require('x402/schemes');
8
+ var accounts = require('viem/accounts');
9
9
 
10
10
  // src/client.ts
11
11
 
@@ -394,24 +394,6 @@ function verifySignature(hash, signature, expectedAddress) {
394
394
  return false;
395
395
  }
396
396
  }
397
- var RECEIPT_VERSION = "haven-receipt-1";
398
- function defaultRecover(hash, signature) {
399
- return ethers.ethers.recoverAddress(hash, signature);
400
- }
401
- function verifyPaymentReceipt(receipt, recover = defaultRecover) {
402
- const { delegate, signHash: signHash2, signature } = receipt.authorization;
403
- if (!signature) return { verified: false, reason: "missing_signature" };
404
- let recovered;
405
- try {
406
- recovered = recover(signHash2, signature);
407
- } catch {
408
- return { verified: false, reason: "bad_signature" };
409
- }
410
- if (recovered.toLowerCase() !== delegate.toLowerCase()) {
411
- return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
412
- }
413
- return { verified: true, recoveredSigner: recovered };
414
- }
415
397
 
416
398
  // src/base64.ts
417
399
  function normalizeBase64(value) {
@@ -958,61 +940,173 @@ function stableStringify2(value) {
958
940
  const object = value;
959
941
  return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
960
942
  }
961
- function createJsonRpcProvider(url) {
962
- return new ethers.ethers.JsonRpcProvider(url);
963
- }
964
- function createWallet(privateKey, provider) {
965
- return new ethers.ethers.Wallet(privateKey, provider);
966
- }
967
- function createErc20Contract(address, abi, runner) {
968
- return new ethers.ethers.Contract(address, abi, runner);
969
- }
970
-
971
- // src/client.ts
972
943
  var DEFAULT_BASE_URL = "http://localhost:3001";
973
- var CHAIN_EXPLORER_TX = {
974
- 100: "https://gnosisscan.io/tx",
975
- 8453: "https://basescan.org/tx"
976
- };
977
- var CHAIN_USDC = {
978
- 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
979
- 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
944
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
945
+ var HavenApiTransport = class {
946
+ apiKey;
947
+ baseUrl;
948
+ requestTimeout;
949
+ defaultHeaders;
950
+ requestContext = new async_hooks.AsyncLocalStorage();
951
+ constructor(config) {
952
+ this.apiKey = config.apiKey;
953
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
954
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
955
+ this.defaultHeaders = { ...config.defaultHeaders ?? {} };
956
+ }
957
+ /** Run `fn` with extra headers scoped to its asynchronous Haven API work. */
958
+ withRequestContext(headers, fn) {
959
+ return this.requestContext.run({ headers: { ...headers } }, fn);
960
+ }
961
+ async post(path, body) {
962
+ return this.request("POST", path, body);
963
+ }
964
+ async get(path) {
965
+ return this.request("GET", path);
966
+ }
967
+ async request(method, path, body) {
968
+ const url = `${this.baseUrl}${path}`;
969
+ const controller = new AbortController();
970
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
971
+ try {
972
+ const contextHeaders = this.requestContext.getStore()?.headers ?? {};
973
+ const res = await fetch(url, {
974
+ method,
975
+ headers: {
976
+ "Content-Type": "application/json",
977
+ "Authorization": `Bearer ${this.apiKey}`,
978
+ ...this.defaultHeaders,
979
+ ...contextHeaders
980
+ },
981
+ body: body ? JSON.stringify(body) : void 0,
982
+ signal: controller.signal
983
+ });
984
+ const data = await res.json();
985
+ if (!res.ok) {
986
+ const record = data;
987
+ const errorText = typeof record.error === "string" ? record.error : void 0;
988
+ const rawDetails = record.details ?? record.detail;
989
+ const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
990
+ const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? "API request failed";
991
+ throw new HavenApiError(message, res.status, data);
992
+ }
993
+ return data;
994
+ } catch (err) {
995
+ if (err instanceof HavenApiError) throw err;
996
+ if (err instanceof Error && err.name === "AbortError") {
997
+ throw new HavenApiError(`Request to ${path} timed out`, 408);
998
+ }
999
+ throw new HavenApiError(
1000
+ `Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
1001
+ 0
1002
+ );
1003
+ } finally {
1004
+ clearTimeout(timeout);
1005
+ }
1006
+ }
980
1007
  };
981
- function buildExplorerUrl(chainId, txHash) {
982
- const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
983
- return `${base}/${txHash}`;
984
- }
985
- function explorerUrlOrEmpty(chainId, txHash) {
986
- return txHash ? buildExplorerUrl(chainId, txHash) : "";
1008
+
1009
+ // src/payment-mappers.ts
1010
+ function mapPaymentResult(raw, buildExplorerUrl2) {
1011
+ return {
1012
+ paymentId: raw.payment_id,
1013
+ status: raw.status,
1014
+ token: raw.token,
1015
+ amount: raw.amount,
1016
+ to: raw.to,
1017
+ txHash: raw.tx_hash,
1018
+ errorMessage: raw.error_message,
1019
+ explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl2(raw.chain_id, raw.tx_hash) : null),
1020
+ fee: raw.fee ? {
1021
+ amount: raw.fee.amount,
1022
+ token: raw.fee.token,
1023
+ basisPoints: raw.fee.basis_points,
1024
+ applied: raw.fee.applied
1025
+ } : null,
1026
+ createdAt: raw.created_at,
1027
+ signedAt: raw.signed_at,
1028
+ submittedAt: raw.submitted_at,
1029
+ confirmedAt: raw.confirmed_at,
1030
+ expiresAt: raw.expires_at
1031
+ };
987
1032
  }
988
- var DEFAULT_REQUEST_TIMEOUT = 3e4;
989
- var DEFAULT_MERCHANT_TIMEOUT = 3e5;
990
- var NOTIFY_TIMEOUT = 1e4;
991
- var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
992
- var DEFAULT_POLLING_INTERVAL = 3e3;
993
- function formatAtomicAmount(atomic, decimals) {
994
- if (atomic < 0n) return "0.0";
995
- const s = atomic.toString().padStart(decimals + 1, "0");
996
- const intPart = s.slice(0, s.length - decimals) || "0";
997
- const fracPart = s.slice(s.length - decimals).replace(/0+$/, "") || "0";
998
- return `${intPart}.${fracPart}`;
1033
+ function mapPaymentStatusResult(raw) {
1034
+ return {
1035
+ paymentId: raw.payment_id,
1036
+ kind: raw.kind,
1037
+ rail: raw.rail,
1038
+ status: raw.status,
1039
+ phase: raw.phase,
1040
+ nextAction: raw.next_action,
1041
+ amount: raw.amount,
1042
+ token: raw.token,
1043
+ resourceUrl: raw.resource_url,
1044
+ merchantAddress: raw.merchant_address,
1045
+ payerAddress: raw.payer_address ?? null,
1046
+ txHash: raw.tx_hash,
1047
+ expiresAt: raw.expires_at,
1048
+ chainId: raw.chain_id,
1049
+ message: raw.message,
1050
+ fee: raw.fee ? {
1051
+ amount: raw.fee.amount,
1052
+ token: raw.fee.token,
1053
+ basisPoints: raw.fee.basis_points,
1054
+ applied: raw.fee.applied
1055
+ } : null,
1056
+ amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
1057
+ asset: raw.asset ?? raw.x402?.asset ?? null,
1058
+ network: raw.network ?? raw.x402?.network ?? null,
1059
+ description: raw.description ?? raw.x402?.description ?? null,
1060
+ idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
1061
+ x402: raw.x402 ? {
1062
+ amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
1063
+ asset: raw.x402.asset ?? raw.asset ?? null,
1064
+ network: raw.x402.network ?? raw.network ?? null,
1065
+ resourceUrl: raw.x402.resource_url ?? raw.resource_url,
1066
+ merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
1067
+ description: raw.x402.description ?? raw.description ?? null,
1068
+ idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
1069
+ } : void 0
1070
+ };
999
1071
  }
1000
- function safeBigInt(value) {
1001
- try {
1002
- return BigInt(value);
1003
- } catch {
1004
- return 0n;
1072
+ function mapPaymentReceipt(raw) {
1073
+ const receipt = {
1074
+ id: raw.id,
1075
+ paymentId: raw.payment_id,
1076
+ rail: raw.rail,
1077
+ proofStatus: raw.proof_status,
1078
+ txHash: raw.tx_hash,
1079
+ chainId: raw.chain_id,
1080
+ resourceUrl: raw.resource_url,
1081
+ merchantAddress: raw.merchant_address,
1082
+ payerAddress: raw.payer_address,
1083
+ settlementAddress: raw.settlement_address,
1084
+ tokenSymbol: raw.token_symbol,
1085
+ tokenAddress: raw.token_address,
1086
+ amountRaw: raw.amount_raw,
1087
+ amount: raw.amount_human,
1088
+ challengeId: raw.challenge_id,
1089
+ idempotencyKey: raw.idempotency_key,
1090
+ challengePayload: raw.challenge_payload,
1091
+ selectedPayment: raw.selected_payment,
1092
+ paymentProofHeaderName: raw.payment_proof_header_name,
1093
+ protocolReceiptHeaderName: raw.protocol_receipt_header_name,
1094
+ protocolReceiptPayload: raw.protocol_receipt_payload,
1095
+ merchantStatus: raw.merchant_status,
1096
+ confirmedAt: raw.confirmed_at,
1097
+ createdAt: raw.created_at,
1098
+ updatedAt: raw.updated_at
1099
+ };
1100
+ if ("payment_intent_id" in raw) {
1101
+ receipt.paymentIntentId = raw.payment_intent_id ?? null;
1005
1102
  }
1103
+ if ("approval_request_id" in raw) {
1104
+ receipt.approvalRequestId = raw.approval_request_id ?? null;
1105
+ }
1106
+ return receipt;
1006
1107
  }
1007
- function deriveReadiness(status, allowances) {
1008
- if (status !== "active") return "revoked";
1009
- const hasSpendable = allowances.some((a) => safeBigInt(a.remainingAtomic) > 0n);
1010
- return hasSpendable ? "ready" : "needs_approval";
1011
- }
1012
- var MCP_PROTOCOL_VERSION = "2025-06-18";
1013
- var MCP_ACCEPT = "application/json, text/event-stream";
1014
- var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
1015
- var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
1108
+
1109
+ // src/payment-state.ts
1016
1110
  var PAYMENT_STATE_STATUS_CODES = {
1017
1111
  pending: 202,
1018
1112
  pending_approval: 202,
@@ -1025,15 +1119,8 @@ var PAYMENT_STATE_STATUS_CODES = {
1025
1119
  failed: 502,
1026
1120
  rejected: 409
1027
1121
  };
1028
- function chainIdFromNetwork(network) {
1029
- if (network === "base") return 8453;
1030
- if (network === "base-sepolia") return 84532;
1031
- if (!network?.startsWith("eip155:")) return void 0;
1032
- const chainId = Number(network.slice("eip155:".length));
1033
- return Number.isFinite(chainId) ? chainId : void 0;
1034
- }
1035
- function chainIdOrNull(network) {
1036
- return chainIdFromNetwork(network) ?? null;
1122
+ function paymentStateStatusCode(status, fallback = 502) {
1123
+ return PAYMENT_STATE_STATUS_CODES[status] ?? fallback;
1037
1124
  }
1038
1125
  function phaseForStatus(status) {
1039
1126
  if (status === "pending_signature") return AgentPaymentPhase.AgentSignatureRequired;
@@ -1076,69 +1163,1484 @@ function messageForState(label, status, paymentId, nextAction) {
1076
1163
  }
1077
1164
  return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
1078
1165
  }
1079
- function sameAddress3(a, b) {
1080
- return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
1081
- }
1082
- function decimalFromUsdcAtomic(value) {
1083
- const amount = BigInt(value);
1084
- const whole = amount / 1000000n;
1085
- const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
1086
- return fraction ? `${whole}.${fraction}` : whole.toString();
1087
- }
1088
- function normalizeDecimal(value) {
1089
- if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
1090
- const [whole, fraction = ""] = value.split(".");
1091
- const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
1092
- const normalizedFraction = fraction.replace(/0+$/, "");
1093
- return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
1094
- }
1095
- function parseMerchantSettlement(header) {
1096
- if (!header) return {};
1097
- const parsed = parseProtocolReceiptHeader(header);
1098
- const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
1099
- return { settlementTxHash: tx };
1166
+ function paymentStateFromRaw(label, raw) {
1167
+ if (!raw.payment_id || !raw.status) return null;
1168
+ const phase = raw.phase ?? phaseForStatus(raw.status);
1169
+ const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
1170
+ if (!phase || !nextAction) return null;
1171
+ const amount = raw.amount ?? raw.requested ?? "";
1172
+ const token = raw.token ?? "";
1173
+ const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
1174
+ return {
1175
+ paymentId: raw.payment_id,
1176
+ kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
1177
+ rail: raw.rail ?? "direct",
1178
+ status: raw.status === "pending" ? "pending_approval" : raw.status,
1179
+ phase,
1180
+ nextAction,
1181
+ amount,
1182
+ token,
1183
+ resourceUrl: raw.resource_url ?? null,
1184
+ merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
1185
+ txHash: raw.tx_hash ?? null,
1186
+ expiresAt: raw.expires_at ?? "",
1187
+ chainId: raw.chain_id ?? 0,
1188
+ message,
1189
+ amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
1190
+ asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
1191
+ network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
1192
+ description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
1193
+ idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
1194
+ x402: raw.x402 ? {
1195
+ amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
1196
+ asset: raw.x402.asset ?? raw.asset ?? null,
1197
+ network: raw.x402.network ?? raw.network ?? null,
1198
+ resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
1199
+ merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
1200
+ description: raw.x402.description ?? raw.description ?? null,
1201
+ idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
1202
+ } : void 0,
1203
+ mpp: raw.mpp ? {
1204
+ amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
1205
+ asset: raw.mpp.asset ?? raw.asset ?? null,
1206
+ network: raw.mpp.network ?? raw.network ?? null,
1207
+ resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
1208
+ merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
1209
+ description: raw.mpp.description ?? raw.description ?? null,
1210
+ idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
1211
+ challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
1212
+ } : void 0
1213
+ };
1100
1214
  }
1101
- function isMcpUrl(url) {
1102
- try {
1103
- return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
1104
- } catch {
1105
- return /\/mcp(?:[/?#]|$)/.test(url);
1215
+ function throwPaymentStateError(label, raw) {
1216
+ const statusCode = paymentStateStatusCode(raw.status);
1217
+ const state = paymentStateFromRaw(label, raw);
1218
+ if (state) {
1219
+ throw new HavenPaymentStateError(state.message, statusCode, state, raw);
1106
1220
  }
1107
- }
1108
- async function responseHasBazaarExtension(response) {
1109
- try {
1110
- const body = await response.clone().json();
1111
- return body?.extensions?.bazaar != null;
1112
- } catch {
1113
- return false;
1221
+ if (raw.status === "pending_approval") {
1222
+ throw new HavenApiError(
1223
+ `${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
1224
+ statusCode,
1225
+ raw
1226
+ );
1227
+ }
1228
+ if (raw.status === "expired") {
1229
+ throw new HavenApiError(
1230
+ `${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
1231
+ statusCode,
1232
+ raw
1233
+ );
1114
1234
  }
1235
+ const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
1236
+ const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
1237
+ throw new HavenApiError(message, statusCode, raw);
1115
1238
  }
1116
- function parseSseJsonRpcMessages(text) {
1117
- const messages = [];
1118
- let dataLines = [];
1119
- const flush = () => {
1120
- if (dataLines.length === 0) return;
1239
+
1240
+ // src/mcp-merchant-transport.ts
1241
+ var DEFAULT_MERCHANT_TIMEOUT = 3e5;
1242
+ var MCP_NOTIFICATION_TIMEOUT = 1e4;
1243
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
1244
+ var MCP_ACCEPT = "application/json, text/event-stream";
1245
+ var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
1246
+ var McpMerchantTransport = class {
1247
+ merchantTimeout;
1248
+ fetchImpl;
1249
+ requestId = 0;
1250
+ constructor(options = {}) {
1251
+ this.merchantTimeout = options.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
1252
+ this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
1253
+ }
1254
+ /** Fetch a merchant with a settlement-sized timeout and caller cancellation. */
1255
+ async fetch(url, init = {}, timeoutMs = this.merchantTimeout) {
1256
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
1257
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
1121
1258
  try {
1122
- messages.push(JSON.parse(dataLines.join("\n")));
1259
+ return await this.fetchImpl(url, { ...init, signal });
1260
+ } catch (err) {
1261
+ if (timeoutSignal.aborted) {
1262
+ throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
1263
+ }
1264
+ throw err;
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Establish an MCP Streamable-HTTP session. Any handshake failure degrades
1269
+ * to `undefined`, allowing the caller to fall back to plain x402.
1270
+ */
1271
+ async initialize(url, init, wallet) {
1272
+ try {
1273
+ const headers = new Headers(init?.headers);
1274
+ headers.set("Content-Type", "application/json");
1275
+ headers.set("Accept", MCP_ACCEPT);
1276
+ if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1277
+ const response = await this.fetch(url, {
1278
+ method: "POST",
1279
+ headers,
1280
+ body: JSON.stringify({
1281
+ jsonrpc: "2.0",
1282
+ id: ++this.requestId,
1283
+ method: "initialize",
1284
+ params: {
1285
+ protocolVersion: MCP_PROTOCOL_VERSION,
1286
+ capabilities: {},
1287
+ clientInfo: MCP_CLIENT_INFO
1288
+ }
1289
+ })
1290
+ });
1291
+ if (!response.ok) return void 0;
1292
+ const sessionId = response.headers.get("mcp-session-id");
1293
+ if (!sessionId) return void 0;
1294
+ const message = await this.readMessage(response);
1295
+ if (message && "error" in message) return void 0;
1296
+ await this.notifyInitialized(url, init, sessionId, wallet);
1297
+ return sessionId;
1123
1298
  } catch {
1299
+ return void 0;
1124
1300
  }
1125
- dataLines = [];
1126
- };
1127
- for (const line of text.split(/\r?\n/)) {
1128
- if (line === "") {
1129
- flush();
1130
- continue;
1301
+ }
1302
+ /** Add the MCP session and response-content negotiation headers. */
1303
+ withSessionHeaders(init, sessionId) {
1304
+ const headers = new Headers(init?.headers);
1305
+ headers.set("mcp-session-id", sessionId);
1306
+ headers.set("Accept", MCP_ACCEPT);
1307
+ return { ...init, headers };
1308
+ }
1309
+ /** Read one JSON-RPC message from a JSON or SSE response without consuming it. */
1310
+ async readMessage(response) {
1311
+ let text;
1312
+ try {
1313
+ text = await response.clone().text();
1314
+ } catch {
1315
+ return void 0;
1131
1316
  }
1132
- if (line.startsWith("data:")) {
1133
- dataLines.push(line.slice(5).replace(/^ /, ""));
1317
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1318
+ return selectJsonRpcResult(parseSseJsonRpcMessages(text));
1319
+ }
1320
+ try {
1321
+ return JSON.parse(text);
1322
+ } catch {
1323
+ return void 0;
1134
1324
  }
1135
1325
  }
1136
- flush();
1137
- return messages;
1138
- }
1139
- function selectJsonRpcResult(messages) {
1140
- return messages.find((m) => "result" in m || "error" in m) ?? messages[messages.length - 1];
1141
- }
1326
+ /**
1327
+ * Collapse an MCP SSE response to the JSON-RPC result. Non-SSE and
1328
+ * unparseable responses pass through with their original body untouched.
1329
+ */
1330
+ async surfaceResult(response) {
1331
+ if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1332
+ return response;
1333
+ }
1334
+ let text;
1335
+ try {
1336
+ text = await response.clone().text();
1337
+ } catch {
1338
+ return response;
1339
+ }
1340
+ const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
1341
+ if (!message) return response;
1342
+ const body = "result" in message ? message.result : message;
1343
+ const headers = new Headers(response.headers);
1344
+ headers.set("content-type", "application/json");
1345
+ headers.delete("content-length");
1346
+ headers.delete("mcp-session-id");
1347
+ return new Response(JSON.stringify(body), {
1348
+ status: response.status,
1349
+ statusText: response.statusText,
1350
+ headers
1351
+ });
1352
+ }
1353
+ /** Detect MCP transport from the URL or Coinbase Bazaar extension. */
1354
+ async detect(url, paymentRequired, response) {
1355
+ if (isMcpUrl(url)) return { handshakeRequired: true, source: "path" };
1356
+ if (paymentRequired.extensions?.bazaar != null) {
1357
+ return { handshakeRequired: true, source: "bazaar" };
1358
+ }
1359
+ if (await responseHasBazaarExtension(response)) {
1360
+ return { handshakeRequired: true, source: "bazaar" };
1361
+ }
1362
+ return void 0;
1363
+ }
1364
+ /** Identify the conventional Streamable-HTTP MCP path without probing it. */
1365
+ isMcpUrl(url) {
1366
+ return isMcpUrl(url);
1367
+ }
1368
+ /** Detect Bazaar metadata without consuming the merchant response. */
1369
+ hasBazaarExtension(response) {
1370
+ return responseHasBazaarExtension(response);
1371
+ }
1372
+ /** Deliver an already-signed x402 header without changing the caller body. */
1373
+ async deliverPayment(url, init, paymentHeader) {
1374
+ const headers = new Headers(init?.headers);
1375
+ headers.set("X-PAYMENT", paymentHeader);
1376
+ return this.fetch(url, { ...init, headers });
1377
+ }
1378
+ async notifyInitialized(url, init, sessionId, wallet) {
1379
+ try {
1380
+ const headers = new Headers(init?.headers);
1381
+ headers.set("Content-Type", "application/json");
1382
+ headers.set("Accept", MCP_ACCEPT);
1383
+ headers.set("mcp-session-id", sessionId);
1384
+ if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
1385
+ await this.fetch(
1386
+ url,
1387
+ {
1388
+ method: "POST",
1389
+ headers,
1390
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1391
+ },
1392
+ MCP_NOTIFICATION_TIMEOUT
1393
+ );
1394
+ } catch {
1395
+ }
1396
+ }
1397
+ };
1398
+ async function captureMerchantResponse(response) {
1399
+ const merchant_body = await response.text().catch(() => "");
1400
+ return {
1401
+ merchant_status: response.status,
1402
+ merchant_status_text: response.statusText,
1403
+ merchant_headers: Object.fromEntries(response.headers.entries()),
1404
+ merchant_body
1405
+ };
1406
+ }
1407
+ function isMcpUrl(url) {
1408
+ try {
1409
+ return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
1410
+ } catch {
1411
+ return /\/mcp(?:[/?#]|$)/.test(url);
1412
+ }
1413
+ }
1414
+ async function responseHasBazaarExtension(response) {
1415
+ try {
1416
+ const body = await response.clone().json();
1417
+ return body?.extensions?.bazaar != null;
1418
+ } catch {
1419
+ return false;
1420
+ }
1421
+ }
1422
+ function parseSseJsonRpcMessages(text) {
1423
+ const messages = [];
1424
+ let dataLines = [];
1425
+ const flush = () => {
1426
+ if (dataLines.length === 0) return;
1427
+ try {
1428
+ messages.push(JSON.parse(dataLines.join("\n")));
1429
+ } catch {
1430
+ }
1431
+ dataLines = [];
1432
+ };
1433
+ for (const line of text.split(/\r?\n/)) {
1434
+ if (line === "") {
1435
+ flush();
1436
+ continue;
1437
+ }
1438
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
1439
+ }
1440
+ flush();
1441
+ return messages;
1442
+ }
1443
+ function selectJsonRpcResult(messages) {
1444
+ return messages.find((message) => "result" in message || "error" in message) ?? messages.at(-1);
1445
+ }
1446
+ var RECEIPT_VERSION = "haven-receipt-1";
1447
+ function defaultRecover(hash, signature) {
1448
+ return ethers.ethers.recoverAddress(hash, signature);
1449
+ }
1450
+ function verifyPaymentReceipt(receipt, recover = defaultRecover) {
1451
+ const { delegate, signHash: signHash2, signature } = receipt.authorization;
1452
+ if (!signature) return { verified: false, reason: "missing_signature" };
1453
+ let recovered;
1454
+ try {
1455
+ recovered = recover(signHash2, signature);
1456
+ } catch {
1457
+ return { verified: false, reason: "bad_signature" };
1458
+ }
1459
+ if (recovered.toLowerCase() !== delegate.toLowerCase()) {
1460
+ return { verified: false, reason: "signer_mismatch", recoveredSigner: recovered };
1461
+ }
1462
+ return { verified: true, recoveredSigner: recovered };
1463
+ }
1464
+
1465
+ // src/account-reads.ts
1466
+ function safeBigInt(value) {
1467
+ try {
1468
+ return BigInt(value);
1469
+ } catch {
1470
+ return 0n;
1471
+ }
1472
+ }
1473
+ function formatAtomicAmount(atomic, decimals) {
1474
+ if (atomic < 0n) return "0.0";
1475
+ const value = atomic.toString().padStart(decimals + 1, "0");
1476
+ const whole = value.slice(0, value.length - decimals) || "0";
1477
+ const fraction = value.slice(value.length - decimals).replace(/0+$/, "") || "0";
1478
+ return `${whole}.${fraction}`;
1479
+ }
1480
+ function deriveReadiness(status, allowances) {
1481
+ if (status !== "active") return "revoked";
1482
+ return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
1483
+ }
1484
+ var AccountReads = class {
1485
+ transport;
1486
+ getPaymentStatus;
1487
+ agentInFlight = null;
1488
+ constructor(options) {
1489
+ this.transport = options.transport;
1490
+ this.getPaymentStatus = options.getPaymentStatus;
1491
+ }
1492
+ async getAgent() {
1493
+ if (this.agentInFlight) return this.agentInFlight;
1494
+ const request = this.fetchAgent();
1495
+ this.agentInFlight = request;
1496
+ request.finally(() => {
1497
+ this.agentInFlight = null;
1498
+ }).catch(() => {
1499
+ });
1500
+ return request;
1501
+ }
1502
+ async getAgentSummary() {
1503
+ const [agent, allowanceSummary] = await Promise.all([this.getAgent(), this.getAllowances()]);
1504
+ const allowances = allowanceSummary.allowances.map((allowance) => {
1505
+ const token = resolveTokenFromAddress(allowance.tokenAddress);
1506
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(allowance.onchain.remaining), token.decimals)} ${allowance.tokenSymbol}` : `${allowance.onchain.remaining} ${allowance.tokenSymbol} (atomic; unknown decimals)`;
1507
+ return {
1508
+ tokenSymbol: allowance.tokenSymbol,
1509
+ remainingAtomic: allowance.onchain.remaining,
1510
+ remainingDisplay,
1511
+ configuredAmount: allowance.configuredAmount,
1512
+ resetPeriodMin: allowance.resetPeriodMin,
1513
+ isResetPending: allowance.onchain.isResetPending
1514
+ };
1515
+ });
1516
+ const readiness = deriveReadiness(agent.status, allowances);
1517
+ return { ...agent, readiness, spend_authority_readiness: readiness, allowances };
1518
+ }
1519
+ async getAllowances() {
1520
+ const raw = await this.transport.get("/machine-payments/allowances");
1521
+ return {
1522
+ agentId: raw.agent_id,
1523
+ safeAddress: raw.safe_address,
1524
+ delegateAddress: raw.delegate_address,
1525
+ chainId: raw.chain_id,
1526
+ allowances: raw.allowances.map((allowance) => ({
1527
+ id: allowance.id,
1528
+ tokenAddress: allowance.token_address,
1529
+ tokenSymbol: allowance.token_symbol,
1530
+ configuredAmount: allowance.configured_amount,
1531
+ resetPeriodMin: allowance.reset_period_min,
1532
+ onchain: {
1533
+ amount: allowance.onchain.amount,
1534
+ spent: allowance.onchain.spent,
1535
+ remaining: allowance.onchain.remaining,
1536
+ effectiveSpent: allowance.onchain.effective_spent,
1537
+ resetTimeMin: allowance.onchain.reset_time_min,
1538
+ lastResetMin: allowance.onchain.last_reset_min,
1539
+ nonce: allowance.onchain.nonce,
1540
+ isResetPending: allowance.onchain.is_reset_pending,
1541
+ remainingIsFromChain: allowance.onchain.remaining_is_from_chain
1542
+ }
1543
+ }))
1544
+ };
1545
+ }
1546
+ async getPostPurchaseAllowanceSummary(paymentId) {
1547
+ const unavailable = (detail, payment2 = null) => ({
1548
+ payment: payment2,
1549
+ allowance: null,
1550
+ warnings: [{
1551
+ code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
1552
+ message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
1553
+ }]
1554
+ });
1555
+ const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
1556
+ this.getPaymentStatus(paymentId),
1557
+ this.getAgent(),
1558
+ this.getAllowances()
1559
+ ]);
1560
+ if (statusResult.status === "rejected") {
1561
+ return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
1562
+ }
1563
+ const payment = statusResult.value;
1564
+ if (agentResult.status === "rejected") {
1565
+ return unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason), payment);
1566
+ }
1567
+ if (allowanceResult.status === "rejected") {
1568
+ return unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason), payment);
1569
+ }
1570
+ try {
1571
+ const tokenAddress = payment.asset ?? payment.x402?.asset ?? null;
1572
+ if (!tokenAddress) return unavailable("the settled payment does not carry a resolvable token address", payment);
1573
+ const match = allowanceResult.value.allowances.find(
1574
+ (allowance) => allowance.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1575
+ );
1576
+ if (!match) return unavailable("no allowance/budget row matches the settled token", payment);
1577
+ const token = resolveTokenFromAddress(match.tokenAddress);
1578
+ const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1579
+ const rail = agentResult.value.executionRail;
1580
+ return {
1581
+ payment,
1582
+ allowance: {
1583
+ rail,
1584
+ remaining_atomic: match.onchain.remaining,
1585
+ ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1586
+ token_symbol: match.tokenSymbol,
1587
+ token_address: match.tokenAddress,
1588
+ reset_period: match.resetPeriodMin,
1589
+ source: rail === "delegation" ? "active_delegations" : "allowance_module"
1590
+ },
1591
+ warnings: []
1592
+ };
1593
+ } catch (error) {
1594
+ return unavailable(error instanceof Error ? error.message : String(error));
1595
+ }
1596
+ }
1597
+ async listReceipts(options = {}) {
1598
+ const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
1599
+ const raw = await this.transport.get(`/machine-payments/receipts${query}`);
1600
+ return raw.receipts.map(mapPaymentReceipt);
1601
+ }
1602
+ async getReceipt(paymentId) {
1603
+ const { receipt } = await this.transport.get(`/payments/${paymentId}/receipt`);
1604
+ return { receipt, verification: verifyPaymentReceipt(receipt) };
1605
+ }
1606
+ async fetchAgent() {
1607
+ const raw = await this.transport.get("/machine-payments/agent");
1608
+ return {
1609
+ id: raw.id,
1610
+ name: raw.name,
1611
+ status: raw.status,
1612
+ safeAddress: raw.safe_address,
1613
+ delegateAddress: raw.delegate_address,
1614
+ chainId: raw.chain_id,
1615
+ executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1616
+ };
1617
+ }
1618
+ };
1619
+ function createJsonRpcProvider(url) {
1620
+ return new ethers.ethers.JsonRpcProvider(url);
1621
+ }
1622
+ function createWallet(privateKey, provider) {
1623
+ return new ethers.ethers.Wallet(privateKey, provider);
1624
+ }
1625
+ function createErc20Contract(address, abi, runner) {
1626
+ return new ethers.ethers.Contract(address, abi, runner);
1627
+ }
1628
+
1629
+ // src/delegate-sweep.ts
1630
+ var DelegateSweepApi = class {
1631
+ constructor(options) {
1632
+ this.options = options;
1633
+ }
1634
+ options;
1635
+ async sweepDelegate() {
1636
+ if (!this.options.delegateKey) throw new HavenSigningError("delegateKey is required for sweepDelegate.");
1637
+ const agent = await this.options.getAgent();
1638
+ if (!agent.delegateAddress) throw new HavenApiError("Agent has no delegate address.", 422);
1639
+ const rpcUrl = this.options.chainRpcs[agent.chainId];
1640
+ if (!rpcUrl) throw new HavenApiError(`chainRpcs[${agent.chainId}] must be configured to sweep the delegate wallet.`, 422);
1641
+ const provider = createJsonRpcProvider(rpcUrl);
1642
+ const wallet = createWallet(this.options.delegateKey, provider);
1643
+ const transfers = [];
1644
+ if (isSweepableChain(agent.chainId)) {
1645
+ const contract = createErc20Contract(sweepUsdcAddress(agent.chainId), ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"], wallet);
1646
+ const balance2 = await contract.balanceOf(agent.delegateAddress);
1647
+ if (balance2 > 0n) {
1648
+ const tx = await contract.transfer(agent.safeAddress, balance2);
1649
+ const receipt = await tx.wait(1);
1650
+ const txHash = receipt?.hash ?? tx.hash;
1651
+ transfers.push({ asset: "USDC", amount: format(balance2, 6), amountAtomic: balance2.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash) });
1652
+ }
1653
+ }
1654
+ const balance = await provider.getBalance(agent.delegateAddress);
1655
+ if (balance > 0n) {
1656
+ const fee = await provider.getFeeData();
1657
+ const send = balance - (fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n) * 21000n * 2n;
1658
+ if (send > 0n) {
1659
+ const tx = await wallet.sendTransaction({ to: agent.safeAddress, value: send });
1660
+ const receipt = await tx.wait(1);
1661
+ const txHash = receipt?.hash ?? tx.hash;
1662
+ transfers.push({ asset: "ETH", amount: format(send, 18), amountAtomic: send.toString(), txHash, explorerUrl: this.options.buildExplorerUrl(agent.chainId, txHash) });
1663
+ }
1664
+ }
1665
+ return { fromAddress: agent.delegateAddress, toAddress: agent.safeAddress, chainId: agent.chainId, transfers };
1666
+ }
1667
+ prepareSweep() {
1668
+ return this.options.transport.post("/machine-payments/sweep/prepare", {});
1669
+ }
1670
+ submitSweep(authorization, signature) {
1671
+ return this.options.transport.post("/machine-payments/sweep/submit", { authorization, signature });
1672
+ }
1673
+ };
1674
+ function format(value, decimals) {
1675
+ const raw = value.toString().padStart(decimals + 1, "0");
1676
+ return `${raw.slice(0, -decimals) || "0"}.${raw.slice(-decimals).replace(/0+$/, "") || "0"}`;
1677
+ }
1678
+
1679
+ // src/x402-protocol.ts
1680
+ var CHAIN_EXPLORER_TX = {
1681
+ 100: "https://gnosisscan.io/tx",
1682
+ 8453: "https://basescan.org/tx"
1683
+ };
1684
+ function buildExplorerUrl(chainId, txHash) {
1685
+ const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
1686
+ return `${base}/${txHash}`;
1687
+ }
1688
+ function explorerUrlOrEmpty(chainId, txHash) {
1689
+ return txHash ? buildExplorerUrl(chainId, txHash) : "";
1690
+ }
1691
+ function chainIdFromNetwork(network) {
1692
+ if (network === "base") return 8453;
1693
+ if (network === "base-sepolia") return 84532;
1694
+ if (!network?.startsWith("eip155:")) return void 0;
1695
+ const chainId = Number(network.slice("eip155:".length));
1696
+ return Number.isFinite(chainId) ? chainId : void 0;
1697
+ }
1698
+ function chainIdOrNull(network) {
1699
+ return chainIdFromNetwork(network) ?? null;
1700
+ }
1701
+ function sameAddress3(a, b) {
1702
+ return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
1703
+ }
1704
+ function decimalFromUsdcAtomic(value) {
1705
+ const amount = BigInt(value);
1706
+ const whole = amount / 1000000n;
1707
+ const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
1708
+ return fraction ? `${whole}.${fraction}` : whole.toString();
1709
+ }
1710
+ function normalizeDecimal(value) {
1711
+ if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
1712
+ const [whole, fraction = ""] = value.split(".");
1713
+ const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
1714
+ const normalizedFraction = fraction.replace(/0+$/, "");
1715
+ return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
1716
+ }
1717
+ function x402PayerAddress(delegateAddress, x402Wallet) {
1718
+ return delegateAddress ?? x402Wallet;
1719
+ }
1720
+ function withX402Wallet(init, wallet) {
1721
+ if (!wallet) return init;
1722
+ const headers = new Headers(init?.headers);
1723
+ if (!headers.has("x402-wallet")) {
1724
+ headers.set("x402-wallet", wallet);
1725
+ }
1726
+ return {
1727
+ ...init,
1728
+ headers
1729
+ };
1730
+ }
1731
+ function snapshotRequestBody(body) {
1732
+ if (body == null) return void 0;
1733
+ if (typeof body === "string") return body;
1734
+ if (body instanceof URLSearchParams) return body.toString();
1735
+ throw new HavenApiError(
1736
+ "Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
1737
+ 400
1738
+ );
1739
+ }
1740
+ function snapshotX402Request(url, init) {
1741
+ return {
1742
+ url,
1743
+ method: init?.method ?? "GET",
1744
+ headers: Array.from(new Headers(init?.headers).entries()),
1745
+ body: snapshotRequestBody(init?.body)
1746
+ };
1747
+ }
1748
+ function requestInitFromSnapshot(request) {
1749
+ return {
1750
+ method: request.method,
1751
+ headers: request.headers,
1752
+ body: request.body
1753
+ };
1754
+ }
1755
+ function buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
1756
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
1757
+ if (!option) {
1758
+ throw new HavenApiError(
1759
+ "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
1760
+ 400
1761
+ );
1762
+ }
1763
+ const token = resolveTokenFromAddress(option.asset, option.network);
1764
+ return {
1765
+ rail: "x402",
1766
+ idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
1767
+ paymentRequired,
1768
+ accepted: option,
1769
+ request,
1770
+ ...mcpTransport ? { mcpTransport } : {},
1771
+ resourceUrl: paymentRequired.resource.url,
1772
+ description: paymentRequired.resource.description ?? option.description ?? null,
1773
+ mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
1774
+ amountAtomic: x402AuthorizationAmount(option),
1775
+ amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
1776
+ token: token?.symbol ?? "USDC",
1777
+ // #1351: null when the asset is unrecognised on this network — the
1778
+ // `token` fallback above is a LABEL, not evidence of 6 decimals, and a
1779
+ // human-denominated cap must fail closed rather than convert against a
1780
+ // guess. Same resolution as `token`, so the two never disagree.
1781
+ decimals: token?.decimals ?? null,
1782
+ asset: option.asset,
1783
+ network: option.network,
1784
+ chainId: chainIdOrNull(option.network),
1785
+ merchantAddress: option.payTo,
1786
+ maxTimeoutSeconds: option.maxTimeoutSeconds
1787
+ };
1788
+ }
1789
+ function buildX402Receipt(input) {
1790
+ const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
1791
+ return {
1792
+ success: true,
1793
+ paymentId: input.paymentId,
1794
+ txHash: input.txHash,
1795
+ token: input.token,
1796
+ amount: input.amount,
1797
+ to: input.to,
1798
+ resourceUrl: input.resourceUrl,
1799
+ explorerUrl: input.explorerUrl,
1800
+ accepted: input.accepted,
1801
+ paymentHeader: input.paymentHeader,
1802
+ merchantTo: input.merchantTo ?? input.accepted.payTo,
1803
+ payer: input.payer,
1804
+ chainId: input.chainId,
1805
+ haven: {
1806
+ paymentId: input.paymentId,
1807
+ fundingTxHash: input.txHash,
1808
+ fundingExplorerUrl
1809
+ },
1810
+ merchant: {
1811
+ payTo: input.merchantTo ?? input.accepted.payTo
1812
+ },
1813
+ x402: {
1814
+ amount: x402AuthorizationAmount(input.accepted),
1815
+ token: input.token,
1816
+ network: input.accepted.network,
1817
+ asset: input.accepted.asset,
1818
+ resource: input.accepted.resource ?? input.resourceUrl
1819
+ }
1820
+ };
1821
+ }
1822
+ function buildX402ResumeState(input) {
1823
+ const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
1824
+ return {
1825
+ rail: "x402",
1826
+ paymentId: input.paymentId,
1827
+ idempotencyKey: input.idempotencyKey,
1828
+ paymentRequired: input.paymentRequired,
1829
+ accepted: input.accepted,
1830
+ url: input.request?.url ?? input.paymentRequired.resource.url,
1831
+ request: input.request,
1832
+ resourceUrl: input.paymentRequired.resource.url,
1833
+ description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
1834
+ amountAtomic: x402AuthorizationAmount(input.accepted),
1835
+ amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
1836
+ token: token?.symbol ?? "USDC",
1837
+ asset: input.accepted.asset,
1838
+ network: input.accepted.network,
1839
+ chainId: chainIdOrNull(input.accepted.network),
1840
+ merchantAddress: input.accepted.payTo
1841
+ };
1842
+ }
1843
+ function attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
1844
+ if (!(err instanceof HavenPaymentStateError)) return;
1845
+ if (err.state.rail !== "x402") return;
1846
+ err.resumeState = buildX402ResumeState({
1847
+ paymentId: err.state.paymentId,
1848
+ paymentRequired,
1849
+ accepted,
1850
+ idempotencyKey,
1851
+ request
1852
+ });
1853
+ }
1854
+ function attachResumeState(err, input) {
1855
+ attachX402ResumeState(
1856
+ err,
1857
+ input.paymentRequired,
1858
+ input.accepted,
1859
+ input.idempotencyKey,
1860
+ input.request
1861
+ );
1862
+ }
1863
+ function assertCanResumeX402(status, paymentRequired, option) {
1864
+ if (status.rail !== "x402") {
1865
+ throw new HavenPaymentStateError(
1866
+ `Payment ${status.paymentId} is ${status.rail}, not x402.`,
1867
+ 409,
1868
+ status
1869
+ );
1870
+ }
1871
+ if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
1872
+ throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
1873
+ }
1874
+ if (!status.txHash) {
1875
+ throw new HavenApiError(
1876
+ `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
1877
+ 502,
1878
+ status,
1879
+ status.paymentId
1880
+ );
1881
+ }
1882
+ if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
1883
+ throw new HavenApiError(
1884
+ "x402 resume request does not match the approved resource URL.",
1885
+ 409,
1886
+ { status, paymentRequired },
1887
+ status.paymentId
1888
+ );
1889
+ }
1890
+ if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
1891
+ throw new HavenApiError(
1892
+ "x402 resume request does not match the approved merchant.",
1893
+ 409,
1894
+ { status, selectedPayment: option },
1895
+ status.paymentId
1896
+ );
1897
+ }
1898
+ const optionChainId = chainIdFromNetwork(option.network);
1899
+ if (status.chainId && optionChainId && status.chainId !== optionChainId) {
1900
+ throw new HavenApiError(
1901
+ "x402 resume request does not match the approved network.",
1902
+ 409,
1903
+ { status, selectedPayment: option },
1904
+ status.paymentId
1905
+ );
1906
+ }
1907
+ if (status.token && status.token !== "USDC") {
1908
+ throw new HavenApiError(
1909
+ "x402 resume request does not match the approved token.",
1910
+ 409,
1911
+ { status, selectedPayment: option },
1912
+ status.paymentId
1913
+ );
1914
+ }
1915
+ const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
1916
+ const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
1917
+ if (approvedAmount && approvedAmount !== requestedAmount) {
1918
+ throw new HavenApiError(
1919
+ "x402 resume request does not match the approved amount.",
1920
+ 409,
1921
+ { status, selectedPayment: option },
1922
+ status.paymentId
1923
+ );
1924
+ }
1925
+ }
1926
+ var X402FundingLeg = class {
1927
+ delegateKey;
1928
+ delegateAddress;
1929
+ x402Wallet;
1930
+ chainRpcs;
1931
+ post;
1932
+ signForData;
1933
+ assertSignableAuthorizationState;
1934
+ /**
1935
+ * Receipts keyed by idempotency key, held only as long as the underlying
1936
+ * EIP-3009 authorization is valid. The cache belongs to this module rather
1937
+ * than to the facade because its expiry is read out of the authorization
1938
+ * header itself — a 3009 artifact.
1939
+ */
1940
+ receiptCache = /* @__PURE__ */ new Map();
1941
+ constructor(options) {
1942
+ this.delegateKey = options.delegateKey;
1943
+ this.delegateAddress = options.delegateAddress;
1944
+ this.x402Wallet = options.x402Wallet;
1945
+ this.chainRpcs = options.chainRpcs;
1946
+ this.post = options.post;
1947
+ this.signForData = options.signForData;
1948
+ this.assertSignableAuthorizationState = options.assertSignableAuthorizationState;
1949
+ }
1950
+ // ── Receipt cache ────────────────────────────────────────────────
1951
+ /** A still-valid cached receipt for this key, or undefined. */
1952
+ cachedReceipt(idempotencyKey) {
1953
+ const cached = this.receiptCache.get(idempotencyKey);
1954
+ if (cached && cached.expiresAt > Date.now()) return cached.receipt;
1955
+ return void 0;
1956
+ }
1957
+ cacheReceipt(idempotencyKey, paymentHeader, receipt) {
1958
+ const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
1959
+ if (expiresAt > Date.now()) {
1960
+ this.receiptCache.set(idempotencyKey, { expiresAt, receipt });
1961
+ }
1962
+ }
1963
+ // ── Authorization ────────────────────────────────────────────────
1964
+ async authorize(paymentRequired, option, idempotencyKey) {
1965
+ const raw = await this.post("/x402", {
1966
+ url: paymentRequired.resource.url,
1967
+ payTo: this.delegateAddress,
1968
+ merchantPayTo: option.payTo,
1969
+ amount: x402AuthorizationAmount(option),
1970
+ asset: option.asset,
1971
+ network: option.network,
1972
+ description: paymentRequired.resource.description,
1973
+ idempotencyKey,
1974
+ // #1360: same explicit funding-leg declaration as createX402Intent —
1975
+ // this local-key path derives payTo from the key (never stale), but the
1976
+ // declaration keeps both writers of the 3009 shape loud-by-default.
1977
+ settlementScheme: "eip3009"
1978
+ });
1979
+ const state = paymentStateFromRaw("x402 payment", raw);
1980
+ const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
1981
+ if (executedReplay) {
1982
+ const canFund = await this.delegateCanFund(
1983
+ raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
1984
+ option.asset,
1985
+ x402AuthorizationAmount(option)
1986
+ );
1987
+ const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
1988
+ if (refuse) {
1989
+ const settledReceipt = state && executedReplay === "approval-resume" ? this.receiptFromStatus(paymentRequired, option, void 0, state) : this.receiptFromAuthorization(paymentRequired, option, void 0, raw);
1990
+ throw new X402AlreadySettledError(
1991
+ canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
1992
+ settledReceipt,
1993
+ canFund === false ? "settled" : "unverifiable"
1994
+ );
1995
+ }
1996
+ }
1997
+ const paymentHeader = await this.createPaymentHeader(paymentRequired, option);
1998
+ if (raw.success && raw.tx_hash) {
1999
+ const receipt2 = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
2000
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
2001
+ return receipt2;
2002
+ }
2003
+ if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
2004
+ const receipt2 = this.receiptFromStatus(paymentRequired, option, paymentHeader, state);
2005
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt2);
2006
+ return receipt2;
2007
+ }
2008
+ this.assertSignableAuthorizationState("x402 payment", raw);
2009
+ if (!raw.sign_data?.hash) {
2010
+ throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
2011
+ }
2012
+ const sig = await this.signForData(raw.sign_data);
2013
+ const execResult = await this.post(
2014
+ `/payments/${raw.payment_id}/sign`,
2015
+ { signature: sig }
2016
+ );
2017
+ if (execResult.status !== "confirmed") {
2018
+ throwPaymentStateError("x402 payment", execResult);
2019
+ }
2020
+ await this.waitForFundingTx(
2021
+ execResult.tx_hash,
2022
+ execResult.chain_id ?? chainIdFromNetwork(option.network)
2023
+ );
2024
+ const receipt = this.receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
2025
+ this.cacheReceipt(idempotencyKey, paymentHeader, receipt);
2026
+ return receipt;
2027
+ }
2028
+ // ── Header minting ───────────────────────────────────────────────
2029
+ async createPaymentHeader(paymentRequired, option) {
2030
+ if (!this.delegateKey) {
2031
+ throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
2032
+ }
2033
+ const account = accounts.privateKeyToAccount(this.delegateKey);
2034
+ const requirements = toStandardPaymentRequirements(paymentRequired, option);
2035
+ const header = await schemes.exact.evm.createPaymentHeader(
2036
+ account,
2037
+ paymentRequired.x402Version,
2038
+ requirements
2039
+ );
2040
+ if (paymentRequired.x402Version < 2) return header;
2041
+ const payment = decodeBase64Json(header);
2042
+ return encodeBase64Json({
2043
+ x402Version: paymentRequired.x402Version,
2044
+ accepted: option,
2045
+ payload: payment.payload
2046
+ });
2047
+ }
2048
+ // ── Receipt mapping ──────────────────────────────────────────────
2049
+ receiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
2050
+ const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
2051
+ const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
2052
+ const token = execResult?.token ?? raw.token ?? "USDC";
2053
+ const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
2054
+ const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
2055
+ const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
2056
+ const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
2057
+ const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
2058
+ return buildX402Receipt({
2059
+ paymentId: raw.payment_id,
2060
+ txHash,
2061
+ token,
2062
+ amount,
2063
+ to,
2064
+ resourceUrl: paymentRequired.resource.url,
2065
+ explorerUrl,
2066
+ accepted: option,
2067
+ paymentHeader,
2068
+ merchantTo,
2069
+ payer,
2070
+ chainId
2071
+ });
2072
+ }
2073
+ receiptFromStatus(paymentRequired, option, paymentHeader, status) {
2074
+ if (!status.txHash) {
2075
+ throw new HavenApiError(
2076
+ `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
2077
+ 502,
2078
+ status,
2079
+ status.paymentId
2080
+ );
2081
+ }
2082
+ return buildX402Receipt({
2083
+ paymentId: status.paymentId,
2084
+ txHash: status.txHash,
2085
+ token: status.token || "USDC",
2086
+ amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
2087
+ to: this.delegateAddress ?? "",
2088
+ resourceUrl: paymentRequired.resource.url,
2089
+ explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
2090
+ accepted: option,
2091
+ paymentHeader,
2092
+ merchantTo: status.merchantAddress ?? option.payTo,
2093
+ payer: this.x402Wallet,
2094
+ chainId: status.chainId || chainIdFromNetwork(option.network)
2095
+ });
2096
+ }
2097
+ // ── On-chain reads ───────────────────────────────────────────────
2098
+ /**
2099
+ * Wait for a funding tx to be mined with ≥1 confirmation before the
2100
+ * merchant retry, eliminating the race where the merchant's
2101
+ * `balanceOf(delegate)` runs before the funding block propagates.
2102
+ *
2103
+ * Skipped when `chainRpcs` does not include the chain; in that case Haven's
2104
+ * backend has already confirmed on-chain submission and callers accept the
2105
+ * small propagation window as a trade-off for not configuring an RPC URL.
2106
+ */
2107
+ async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
2108
+ if (!txHash || !chainId) return;
2109
+ const rpcUrl = this.chainRpcs[chainId];
2110
+ if (!rpcUrl) return;
2111
+ const provider = createJsonRpcProvider(rpcUrl);
2112
+ const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2113
+ if (!onChainReceipt || onChainReceipt.status !== 1) {
2114
+ throw new HavenApiError(
2115
+ "Funding tx did not confirm on-chain within the timeout window.",
2116
+ 500,
2117
+ { txHash, chainId }
2118
+ );
2119
+ }
2120
+ }
2121
+ /**
2122
+ * Can the delegate EOA still fund an authorization for `amountAtomic`?
2123
+ *
2124
+ * #1521: the only question that separates a legitimate resume (funding
2125
+ * confirmed, merchant never paid — the delegate still holds the money) from
2126
+ * a replayed settled payment (funding confirmed, merchant paid, delegate
2127
+ * spent). The intent's own `status: 'confirmed'` is identical in both.
2128
+ *
2129
+ * The balance is asked of the CHAIN rather than of Haven's bookkeeping on
2130
+ * purpose: the merchant-settlement evidence record is written by this SDK
2131
+ * *after* the merchant call, so a client that dies between the two leaves
2132
+ * the backend believing the merchant was never paid — the exact case the
2133
+ * discriminator has to get right. The chain cannot be behind in that way.
2134
+ *
2135
+ * Returns `null` — never a guess — when `chainRpcs` has no entry for the
2136
+ * chain or the read fails. Callers must treat that as "unverifiable", not
2137
+ * as "funded".
2138
+ */
2139
+ async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
2140
+ if (!chainId || !this.delegateAddress) return null;
2141
+ const rpcUrl = this.chainRpcs[chainId];
2142
+ if (!rpcUrl) return null;
2143
+ try {
2144
+ const provider = createJsonRpcProvider(rpcUrl);
2145
+ const token = createErc20Contract(
2146
+ tokenAddress,
2147
+ ["function balanceOf(address) view returns (uint256)"],
2148
+ provider
2149
+ );
2150
+ const balance = await Promise.race([
2151
+ token.balanceOf(this.delegateAddress),
2152
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
2153
+ ]);
2154
+ if (balance === null) return null;
2155
+ return balance >= BigInt(amountAtomic);
2156
+ } catch {
2157
+ return null;
2158
+ }
2159
+ }
2160
+ };
2161
+ function getPaymentHeaderValidBefore(paymentHeader) {
2162
+ try {
2163
+ const payment = decodeBase64Json(
2164
+ paymentHeader
2165
+ );
2166
+ const payload = payment.payload;
2167
+ const validBeforeSeconds = Number(payload.authorization?.validBefore);
2168
+ if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
2169
+ } catch {
2170
+ }
2171
+ return 0;
2172
+ }
2173
+
2174
+ // src/x402-erc7710.ts
2175
+ var X402Erc7710 = class {
2176
+ delegateKey;
2177
+ post;
2178
+ signForData;
2179
+ getAgent;
2180
+ constructor(options) {
2181
+ this.delegateKey = options.delegateKey;
2182
+ this.post = options.post;
2183
+ this.signForData = options.signForData;
2184
+ this.getAgent = options.getAgent;
2185
+ }
2186
+ /**
2187
+ * Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
2188
+ *
2189
+ * The whole point of this path is what it does NOT do. There is no funding
2190
+ * leg: the merchant redeems a delegation chain and pulls from the treasury
2191
+ * directly, so the delegate EOA never holds the money, no sweep can strand
2192
+ * it, and the #713 reconciliation class does not apply. It is also why this
2193
+ * method is SMALLER than the 3009 path — the backend assembles the merchant
2194
+ * `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
2195
+ * header locally.
2196
+ *
2197
+ * authorize (payTo = the MERCHANT) → sign the child → settle → header
2198
+ *
2199
+ * The caller then retries the merchant with that header. **Nothing has
2200
+ * settled when this returns** — that is why it does not return an
2201
+ * `X402Receipt`.
2202
+ *
2203
+ * Requires a delegation-rail account. The backend enforces that
2204
+ * (`validateGenericSchemeRail`), and so does this method, before building a
2205
+ * request the backend would only reject: an error a client can explain is
2206
+ * worth more than a 400 it has to decode.
2207
+ *
2208
+ * **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
2209
+ * challenge frequently carries no `resource` object at all, so
2210
+ * `paymentRequired.resource?.url` is undefined and the backend answers
2211
+ * "Valid url is required". The QA scenario this path was ported from falls
2212
+ * back to the request URL for exactly that reason — the SDK cannot, because
2213
+ * it never saw the request. Pass it.
2214
+ */
2215
+ async settle(paymentRequired, options = {}) {
2216
+ if (!this.delegateKey) {
2217
+ throw new HavenSigningError(
2218
+ "delegateKey is required for x402 payments. Pass it in the HavenClient config."
2219
+ );
2220
+ }
2221
+ const prepared = await this.prepare(paymentRequired, options);
2222
+ const signature = await this.signForData(prepared.signData);
2223
+ const paymentHeader = await this.submit(prepared.paymentId, signature);
2224
+ return { ...prepared.settlement, paymentHeader };
2225
+ }
2226
+ /**
2227
+ * The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
2228
+ * the request, and return the child to be signed — without signing it.
2229
+ *
2230
+ * Split out because the hosted topology cannot use `settleX402Erc7710()`:
2231
+ * that method signs in-process with `delegateKey`, and hosted Haven does not
2232
+ * have one and must not. The hosted MCP server drives these two halves with
2233
+ * the LOCAL signer in between, so the key stays where it belongs and the
2234
+ * request shaping stays in one place rather than being reimplemented.
2235
+ */
2236
+ async prepare(paymentRequired, options = {}) {
2237
+ const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
2238
+ if (!delegationRail) {
2239
+ throw new HavenApiError(
2240
+ "erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
2241
+ 400
2242
+ );
2243
+ }
2244
+ const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
2245
+ if (!selection || selection.scheme !== "erc7710") {
2246
+ throw new HavenApiError(
2247
+ "This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
2248
+ 400
2249
+ );
2250
+ }
2251
+ const option = selection.option;
2252
+ const merchantPayTo = option.payTo;
2253
+ const amountAtomic = x402AuthorizationAmount(option);
2254
+ const raw = await this.post("/x402", {
2255
+ url: options.resourceUrl ?? paymentRequired.resource?.url,
2256
+ // payTo = the MERCHANT is what selects direct settlement server-side.
2257
+ // The explicit settlementScheme must AGREE with that shape (#1360) —
2258
+ // disagreement is a 400 by design, so that a stale delegate address
2259
+ // becomes a loud mismatch instead of a silent reroute to the 3009 leg.
2260
+ payTo: merchantPayTo,
2261
+ settlementScheme: "erc7710",
2262
+ amount: amountAtomic,
2263
+ asset: option.asset,
2264
+ network: option.network,
2265
+ // The v2 header echoes the accepted entry field-for-field, so the quoted
2266
+ // timeout must round-trip or the merchant rejects the echo (#1064).
2267
+ maxTimeoutSeconds: option.maxTimeoutSeconds,
2268
+ // #1058: forward the advertised facilitators verbatim — the child becomes
2269
+ // redeemable ONLY by them. `null` here means the merchant advertised none
2270
+ // (or an empty array, which the backend 400s on), so the field is OMITTED
2271
+ // rather than sent empty. See x402FacilitatorAddresses.
2272
+ ...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {},
2273
+ // #1307/#1547: persisted so the settle leg can rehydrate the merchant
2274
+ // call by payment_id on this scheme too, not only on the 3009 bridge.
2275
+ ...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
2276
+ });
2277
+ if (!raw.payment_id) {
2278
+ throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
2279
+ }
2280
+ const signData = raw.sign_data;
2281
+ if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
2282
+ throw new HavenApiError(
2283
+ `x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
2284
+ 500,
2285
+ raw
2286
+ );
2287
+ }
2288
+ return {
2289
+ paymentId: raw.payment_id,
2290
+ signData,
2291
+ settlement: {
2292
+ paymentId: raw.payment_id,
2293
+ merchantPayTo,
2294
+ amountAtomic,
2295
+ asset: option.asset,
2296
+ network: option.network,
2297
+ facilitatorAddresses: selection.facilitatorAddresses
2298
+ }
2299
+ };
2300
+ }
2301
+ /**
2302
+ * The SETTLE half (#1456): exchange the signed child for the merchant header.
2303
+ *
2304
+ * The SDK builds no header on this path — the backend assembles the MetaMask
2305
+ * erc7710 payload in `assembleSettlementPayload`. Whoever produced the
2306
+ * signature (an in-process delegate key, or the local edge signer over the
2307
+ * hosted boundary) is irrelevant here.
2308
+ */
2309
+ async submit(paymentId, signature) {
2310
+ const settled = await this.post(
2311
+ `/x402/${paymentId}/settle`,
2312
+ { signature }
2313
+ );
2314
+ if (!settled.payment_header) {
2315
+ throw new HavenApiError(
2316
+ "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
2317
+ 500,
2318
+ settled
2319
+ );
2320
+ }
2321
+ return settled.payment_header;
2322
+ }
2323
+ };
2324
+
2325
+ // src/tool-adapter.ts
2326
+ function toolX402PaymentRequired(input) {
2327
+ return {
2328
+ x402Version: 2,
2329
+ resource: { url: input.url, description: input.description },
2330
+ accepts: [
2331
+ {
2332
+ scheme: "exact",
2333
+ network: input.network,
2334
+ amount: input.amount,
2335
+ asset: input.asset,
2336
+ payTo: input.payTo,
2337
+ maxTimeoutSeconds: 30
2338
+ }
2339
+ ]
2340
+ };
2341
+ }
2342
+ function x402ToolReceipt(receipt) {
2343
+ return {
2344
+ success: true,
2345
+ payment_id: receipt.paymentId,
2346
+ tx_hash: receipt.txHash,
2347
+ token: receipt.token,
2348
+ amount: receipt.amount,
2349
+ to: receipt.to,
2350
+ resource_url: receipt.resourceUrl,
2351
+ explorer_url: receipt.explorerUrl,
2352
+ payment_header: receipt.paymentHeader,
2353
+ merchant_to: receipt.merchantTo,
2354
+ payer: receipt.payer,
2355
+ chain_id: receipt.chainId,
2356
+ haven: receipt.haven,
2357
+ merchant: receipt.merchant,
2358
+ x402: receipt.x402
2359
+ };
2360
+ }
2361
+ function toolError(err) {
2362
+ if (err instanceof HavenPaymentStateError) {
2363
+ return {
2364
+ success: false,
2365
+ payment_id: err.state.paymentId,
2366
+ kind: err.state.kind,
2367
+ rail: err.state.rail,
2368
+ status: err.state.status,
2369
+ phase: err.state.phase,
2370
+ next_action: err.state.nextAction,
2371
+ tx_hash: err.state.txHash,
2372
+ token: err.state.token,
2373
+ amount: err.state.amount,
2374
+ resource_url: err.state.resourceUrl,
2375
+ merchant_address: err.state.merchantAddress,
2376
+ amount_atomic: err.state.amountAtomic,
2377
+ asset: err.state.asset,
2378
+ network: err.state.network,
2379
+ description: err.state.description,
2380
+ idempotency_key: err.state.idempotencyKey,
2381
+ x402: err.state.x402 ? {
2382
+ amount_atomic: err.state.x402.amountAtomic,
2383
+ asset: err.state.x402.asset,
2384
+ network: err.state.x402.network,
2385
+ resource_url: err.state.x402.resourceUrl,
2386
+ merchant_address: err.state.x402.merchantAddress,
2387
+ description: err.state.x402.description,
2388
+ idempotency_key: err.state.x402.idempotencyKey
2389
+ } : void 0,
2390
+ mpp: err.state.mpp ? {
2391
+ amount_atomic: err.state.mpp.amountAtomic,
2392
+ asset: err.state.mpp.asset,
2393
+ network: err.state.mpp.network,
2394
+ resource_url: err.state.mpp.resourceUrl,
2395
+ merchant_address: err.state.mpp.merchantAddress,
2396
+ description: err.state.mpp.description,
2397
+ idempotency_key: err.state.mpp.idempotencyKey,
2398
+ challenge_id: err.state.mpp.challengeId
2399
+ } : void 0,
2400
+ resume_state: err.resumeState,
2401
+ expires_at: err.state.expiresAt,
2402
+ chain_id: err.state.chainId,
2403
+ message: err.state.message,
2404
+ error: err.message
2405
+ };
2406
+ }
2407
+ if (err instanceof HavenApiError) {
2408
+ return {
2409
+ success: false,
2410
+ status_code: err.statusCode,
2411
+ error: err.message,
2412
+ body: err.body
2413
+ };
2414
+ }
2415
+ return {
2416
+ success: false,
2417
+ error: err instanceof Error ? err.message : String(err)
2418
+ };
2419
+ }
2420
+
2421
+ // src/merchant-completion.ts
2422
+ var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
2423
+ var MerchantCompletion = class {
2424
+ post;
2425
+ merchantTransport;
2426
+ getPaymentStatus;
2427
+ getAgent;
2428
+ delegateAddress;
2429
+ x402Wallet;
2430
+ constructor(options) {
2431
+ this.post = options.post;
2432
+ this.merchantTransport = options.merchantTransport;
2433
+ this.getPaymentStatus = options.getPaymentStatus;
2434
+ this.getAgent = options.getAgent;
2435
+ this.delegateAddress = options.delegateAddress;
2436
+ this.x402Wallet = options.x402Wallet;
2437
+ }
2438
+ async retryRequest(url, initialInit, paymentRequired, receipt) {
2439
+ if (!receipt.accepted) {
2440
+ throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
2441
+ }
2442
+ if (!receipt.paymentHeader) {
2443
+ throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2444
+ }
2445
+ const retryResponse = await this.merchantTransport.deliverPayment(
2446
+ url,
2447
+ initialInit,
2448
+ receipt.paymentHeader
2449
+ );
2450
+ if (!retryResponse.ok) {
2451
+ const merchant = await captureMerchantResponse(retryResponse);
2452
+ await this.recordRetryRejected({
2453
+ rail: "x402",
2454
+ paymentId: receipt.paymentId,
2455
+ txHash: receipt.txHash,
2456
+ resourceUrl: receipt.resourceUrl,
2457
+ merchant,
2458
+ details: {
2459
+ merchant_to: receipt.merchantTo,
2460
+ delegate_to: receipt.to
2461
+ }
2462
+ });
2463
+ throw new HavenApiError(
2464
+ "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2465
+ merchant.merchant_status,
2466
+ {
2467
+ marker: "x402_retry_rejected_after_funding",
2468
+ payment_id: receipt.paymentId,
2469
+ tx_hash: receipt.txHash,
2470
+ resource_url: receipt.resourceUrl,
2471
+ merchant_to: receipt.merchantTo,
2472
+ delegate_to: receipt.to,
2473
+ ...merchant
2474
+ }
2475
+ );
2476
+ }
2477
+ const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
2478
+ if (receipt.merchant && merchantSettlement.settlementTxHash) {
2479
+ receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2480
+ receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
2481
+ receipt.chainId,
2482
+ merchantSettlement.settlementTxHash
2483
+ );
2484
+ }
2485
+ await this.reportEvidence({
2486
+ paymentId: receipt.paymentId,
2487
+ rail: "x402",
2488
+ txHash: receipt.txHash,
2489
+ resourceUrl: receipt.resourceUrl,
2490
+ merchantStatus: retryResponse.status,
2491
+ challengePayload: paymentRequired,
2492
+ selectedPayment: receipt.accepted,
2493
+ paymentProofHeaderName: "X-PAYMENT",
2494
+ paymentProofHeader: receipt.paymentHeader,
2495
+ protocolReceiptHeaderName: "PAYMENT-RESPONSE",
2496
+ protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
2497
+ });
2498
+ await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2499
+ return retryResponse;
2500
+ }
2501
+ /**
2502
+ * #956: capture the merchant's OWN receipt when the paid response carries
2503
+ * one, and report it to Haven so the reporting feed can attach it next to
2504
+ * the Haven-generated payment evidence (#498). Two supported signals on the
2505
+ * paid response:
2506
+ *
2507
+ * x-receipt-json: base64-encoded JSON receipt document (inline)
2508
+ * x-receipt-url: https URL to the receipt document (reference)
2509
+ *
2510
+ * Strictly best-effort: absence is the normal case, and no failure here may
2511
+ * ever affect the completed payment — the response is already paid for.
2512
+ */
2513
+ async reportMerchantReceipt(paymentId, response) {
2514
+ try {
2515
+ const inlineB64 = response.headers.get("x-receipt-json");
2516
+ const url = response.headers.get("x-receipt-url");
2517
+ if (!inlineB64 && !url) return;
2518
+ let body = null;
2519
+ if (inlineB64) {
2520
+ if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
2521
+ const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
2522
+ if (decoded && typeof decoded === "object") body = { json: decoded };
2523
+ } else if (url && url.startsWith("https://") && url.length <= 2048) {
2524
+ body = { url };
2525
+ }
2526
+ if (!body) return;
2527
+ await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
2528
+ } catch {
2529
+ }
2530
+ }
2531
+ async resolveCompletionContext(input) {
2532
+ const status = await this.getPaymentStatus(input.paymentId);
2533
+ if (status.rail !== "x402") {
2534
+ throw new HavenPaymentStateError(
2535
+ `Payment ${status.paymentId} is ${status.rail}, not x402.`,
2536
+ 409,
2537
+ status
2538
+ );
2539
+ }
2540
+ const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
2541
+ if (!readyForMerchantCompletion) {
2542
+ throw new HavenPaymentStateError(status.message, paymentStateStatusCode(status.status, 409), status);
2543
+ }
2544
+ if (!input.noFundingLeg && !status.txHash) {
2545
+ throw new HavenApiError(
2546
+ `x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
2547
+ 502,
2548
+ status,
2549
+ status.paymentId
2550
+ );
2551
+ }
2552
+ const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
2553
+ if (approvedResourceUrl && approvedResourceUrl !== input.url) {
2554
+ throw new HavenApiError(
2555
+ "x402 merchant completion does not match the approved resource URL.",
2556
+ 409,
2557
+ { status, url: input.url },
2558
+ status.paymentId
2559
+ );
2560
+ }
2561
+ return {
2562
+ paymentId: status.paymentId,
2563
+ txHash: status.txHash,
2564
+ resourceUrl: approvedResourceUrl ?? input.url,
2565
+ merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
2566
+ };
2567
+ }
2568
+ async resolveWalletForMerchantCall() {
2569
+ const localWallet = x402PayerAddress(this.delegateAddress, this.x402Wallet);
2570
+ if (localWallet) return localWallet;
2571
+ try {
2572
+ const agent = await this.getAgent();
2573
+ return agent.delegateAddress ?? void 0;
2574
+ } catch {
2575
+ return void 0;
2576
+ }
2577
+ }
2578
+ // #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
2579
+ // / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
2580
+ // MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
2581
+ // backend's POST /machine-payments/authorize refuses unconditionally now,
2582
+ // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2583
+ // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2584
+ // for agent-to-merchant payments.
2585
+ async recordRetryRejected(input) {
2586
+ try {
2587
+ await this.post("/machine-payments/reconciliation-events", {
2588
+ paymentId: input.paymentId,
2589
+ rail: input.rail,
2590
+ eventType: "merchant_retry_rejected_after_payment",
2591
+ txHash: input.txHash,
2592
+ reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
2593
+ details: {
2594
+ resource_url: input.resourceUrl,
2595
+ retry_status: input.merchant.merchant_status,
2596
+ retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
2597
+ ...input.details
2598
+ }
2599
+ });
2600
+ } catch {
2601
+ }
2602
+ }
2603
+ async reportEvidence(input) {
2604
+ try {
2605
+ await this.post("/machine-payments/evidence", {
2606
+ paymentId: input.paymentId,
2607
+ rail: input.rail,
2608
+ txHash: input.txHash,
2609
+ resourceUrl: input.resourceUrl,
2610
+ merchantStatus: input.merchantStatus,
2611
+ challengePayload: input.challengePayload,
2612
+ selectedPayment: input.selectedPayment,
2613
+ paymentProofHeaderName: input.paymentProofHeaderName,
2614
+ paymentProofHeader: input.paymentProofHeader,
2615
+ protocolReceiptHeaderName: input.protocolReceiptHeaderName,
2616
+ protocolReceiptHeader: input.protocolReceiptHeader,
2617
+ protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
2618
+ });
2619
+ } catch {
2620
+ }
2621
+ }
2622
+ };
2623
+ function parseMerchantSettlement(header) {
2624
+ if (!header) return {};
2625
+ const parsed = parseProtocolReceiptHeader(header);
2626
+ const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
2627
+ return { settlementTxHash: tx };
2628
+ }
2629
+ function parseProtocolReceiptHeader(value) {
2630
+ try {
2631
+ return decodeBase64Json(value);
2632
+ } catch {
2633
+ try {
2634
+ return JSON.parse(value);
2635
+ } catch {
2636
+ return void 0;
2637
+ }
2638
+ }
2639
+ }
2640
+
2641
+ // src/client.ts
2642
+ var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
2643
+ var DEFAULT_POLLING_INTERVAL = 3e3;
1142
2644
  function x402TypedDataDigest(typedData) {
1143
2645
  if (!typedData || typeof typedData !== "object") return void 0;
1144
2646
  try {
@@ -1169,48 +2671,78 @@ function mapCatalogEntry(entry) {
1169
2671
  };
1170
2672
  }
1171
2673
  var HavenClient = class {
1172
- apiKey;
1173
2674
  delegateKey;
1174
- baseUrl;
2675
+ havenApi;
2676
+ accountReads;
2677
+ delegateSweep;
1175
2678
  x402Wallet;
1176
- requestTimeout;
1177
- merchantTimeout;
2679
+ merchantTransport;
1178
2680
  confirmationTimeout;
1179
2681
  pollingInterval;
1180
2682
  chainRpcs;
1181
2683
  inFlightX402 = /* @__PURE__ */ new Map();
1182
- x402ReceiptCache = /* @__PURE__ */ new Map();
1183
2684
  /**
1184
- * Setup-time headers configured via `HavenClientConfig.defaultHeaders`.
1185
- * Read-only after construction use `withRequestContext` for per-call
1186
- * scoping so concurrent requests don't race on shared mutable state.
2685
+ * The EIP-3009 funding-leg lifecycle (#1618). The facade holds a reference
2686
+ * and delegates; it does not reimplement any of it.
1187
2687
  */
1188
- defaultHeaders;
2688
+ fundingLeg;
1189
2689
  /**
1190
- * Async-local store for per-request context (currently: extra headers).
1191
- * Each `withRequestContext` invocation produces an isolated store, so
1192
- * overlapping async work — like two MCP tool dispatches in flight at
1193
- * the same time — see their own headers without stepping on each other.
2690
+ * The erc7710 direct-settlement lifecycle (#1619). Separate from the funding
2691
+ * leg on purpose: this scheme has no funding leg to share.
1194
2692
  */
1195
- requestContext = new async_hooks.AsyncLocalStorage();
1196
- /** Monotonic JSON-RPC id source for the MCP `initialize` handshake. */
1197
- mcpRequestId = 0;
2693
+ erc7710;
2694
+ /**
2695
+ * Merchant delivery and the evidence trail behind it (#1620). Scheme-neutral
2696
+ * on purpose — both settlement schemes finish through the same door.
2697
+ */
2698
+ merchantCompletion;
1198
2699
  /** Delegate address derived from the private key (if provided) */
1199
2700
  delegateAddress;
1200
2701
  constructor(config) {
1201
- this.apiKey = config.apiKey;
1202
2702
  this.delegateKey = config.delegateKey;
1203
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
2703
+ this.havenApi = new HavenApiTransport(config);
2704
+ this.accountReads = new AccountReads({
2705
+ transport: this.havenApi,
2706
+ getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId)
2707
+ });
2708
+ this.delegateSweep = new DelegateSweepApi({
2709
+ transport: this.havenApi,
2710
+ delegateKey: config.delegateKey,
2711
+ chainRpcs: config.chainRpcs ?? {},
2712
+ getAgent: () => this.getAgent(),
2713
+ buildExplorerUrl: (chainId, hash) => buildExplorerUrl(chainId, hash)
2714
+ });
1204
2715
  this.x402Wallet = config.x402Wallet;
1205
- this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
1206
- this.merchantTimeout = config.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
2716
+ this.merchantTransport = new McpMerchantTransport({ merchantTimeout: config.merchantTimeout });
1207
2717
  this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
1208
2718
  this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
1209
2719
  this.chainRpcs = config.chainRpcs ?? {};
1210
- this.defaultHeaders = { ...config.defaultHeaders ?? {} };
1211
2720
  if (this.delegateKey) {
1212
2721
  this.delegateAddress = addressFromKey(this.delegateKey);
1213
2722
  }
2723
+ this.fundingLeg = new X402FundingLeg({
2724
+ delegateKey: this.delegateKey,
2725
+ delegateAddress: this.delegateAddress,
2726
+ x402Wallet: this.x402Wallet,
2727
+ chainRpcs: this.chainRpcs,
2728
+ post: (path, body) => this.post(path, body),
2729
+ signForData: (signData) => this.signForData(signData),
2730
+ assertSignableAuthorizationState: (label, raw) => this.throwIfNonSignableAuthorizationState(label, raw)
2731
+ });
2732
+ this.merchantCompletion = new MerchantCompletion({
2733
+ post: (path, body) => this.post(path, body),
2734
+ merchantTransport: this.merchantTransport,
2735
+ getPaymentStatus: (paymentId) => this.getPaymentStatus(paymentId),
2736
+ getAgent: () => this.getAgent(),
2737
+ delegateAddress: this.delegateAddress,
2738
+ x402Wallet: this.x402Wallet
2739
+ });
2740
+ this.erc7710 = new X402Erc7710({
2741
+ delegateKey: this.delegateKey,
2742
+ post: (path, body) => this.post(path, body),
2743
+ signForData: (signData) => this.signForData(signData),
2744
+ getAgent: () => this.getAgent()
2745
+ });
1214
2746
  }
1215
2747
  /**
1216
2748
  * Run `fn` with extra Haven-API headers scoped to the async work it
@@ -1228,7 +2760,7 @@ var HavenClient = class {
1228
2760
  * context.
1229
2761
  */
1230
2762
  withRequestContext(headers, fn) {
1231
- return this.requestContext.run({ headers: { ...headers } }, fn);
2763
+ return this.havenApi.withRequestContext(headers, fn);
1232
2764
  }
1233
2765
  // ── High-Level API ───────────────────────────────────────────────
1234
2766
  /**
@@ -1264,7 +2796,7 @@ var HavenClient = class {
1264
2796
  ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
1265
2797
  });
1266
2798
  if (raw.status === "pending_approval") {
1267
- this.throwPaymentStateError("Payment", raw);
2799
+ throwPaymentStateError("Payment", raw);
1268
2800
  }
1269
2801
  return {
1270
2802
  paymentId: raw.payment_id,
@@ -1327,7 +2859,7 @@ var HavenClient = class {
1327
2859
  ...new TextEncoder().encode(JSON.stringify(paymentRequired)).length <= 65536 ? { paymentRequired } : {}
1328
2860
  });
1329
2861
  if (raw.status !== "pending_signature") {
1330
- this.throwPaymentStateError("x402 payment", raw);
2862
+ throwPaymentStateError("x402 payment", raw);
1331
2863
  }
1332
2864
  if (!raw.sign_data?.hash) {
1333
2865
  throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
@@ -1448,7 +2980,7 @@ var HavenClient = class {
1448
2980
  */
1449
2981
  async getPayment(paymentId) {
1450
2982
  const raw = await this.get(`/payments/${paymentId}`);
1451
- return this.mapPaymentResult(raw);
2983
+ return mapPaymentResult(raw, buildExplorerUrl);
1452
2984
  }
1453
2985
  /**
1454
2986
  * Get agent-actionable status for a payment intent or approval request.
@@ -1458,37 +2990,13 @@ var HavenClient = class {
1458
2990
  */
1459
2991
  async getPaymentStatus(paymentId) {
1460
2992
  const raw = await this.get(`/machine-payments/${paymentId}/status`);
1461
- return this.mapPaymentStatusResult(raw);
2993
+ return mapPaymentStatusResult(raw);
1462
2994
  }
1463
2995
  /**
1464
2996
  * Get the agent identity tied to this API key.
1465
2997
  */
1466
2998
  async getAgent() {
1467
- if (this.agentInFlight) return this.agentInFlight;
1468
- const request = this.fetchAgent();
1469
- this.agentInFlight = request;
1470
- request.finally(() => {
1471
- this.agentInFlight = null;
1472
- }).catch(() => {
1473
- });
1474
- return request;
1475
- }
1476
- agentInFlight = null;
1477
- async fetchAgent() {
1478
- const raw = await this.get("/machine-payments/agent");
1479
- return {
1480
- id: raw.id,
1481
- name: raw.name,
1482
- status: raw.status,
1483
- safeAddress: raw.safe_address,
1484
- delegateAddress: raw.delegate_address,
1485
- chainId: raw.chain_id,
1486
- // Defensive normalization, not trust: the backend contract is exactly
1487
- // 'legacy' | 'delegation' (#1306), but an older/mismatched backend
1488
- // during a rollout window should degrade to the wider legacy bucket
1489
- // rather than propagate an unrecognized string.
1490
- executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
1491
- };
2999
+ return this.accountReads.getAgent();
1492
3000
  }
1493
3001
  /**
1494
3002
  * One-shot "am I ready?" bootstrap: identity + live spend authority + a
@@ -1498,23 +3006,7 @@ var HavenClient = class {
1498
3006
  * without two round trips and manual assembly.
1499
3007
  */
1500
3008
  async getAgentSummary() {
1501
- const [agent, allowanceSummary] = await Promise.all([
1502
- this.getAgent(),
1503
- this.getAllowances()
1504
- ]);
1505
- const allowances = allowanceSummary.allowances.map((a) => {
1506
- const token = resolveTokenFromAddress(a.tokenAddress);
1507
- const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(a.onchain.remaining), token.decimals)} ${a.tokenSymbol}` : `${a.onchain.remaining} ${a.tokenSymbol} (atomic; unknown decimals)`;
1508
- return {
1509
- tokenSymbol: a.tokenSymbol,
1510
- remainingAtomic: a.onchain.remaining,
1511
- remainingDisplay,
1512
- configuredAmount: a.configuredAmount,
1513
- resetPeriodMin: a.resetPeriodMin,
1514
- isResetPending: a.onchain.isResetPending
1515
- };
1516
- });
1517
- return { ...agent, readiness: deriveReadiness(agent.status, allowances), allowances };
3009
+ return this.accountReads.getAgentSummary();
1518
3010
  }
1519
3011
  /**
1520
3012
  * Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
@@ -1526,68 +3018,7 @@ var HavenClient = class {
1526
3018
  * Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
1527
3019
  */
1528
3020
  async sweepDelegate() {
1529
- if (!this.delegateKey) {
1530
- throw new HavenSigningError("delegateKey is required for sweepDelegate.");
1531
- }
1532
- const agent = await this.getAgent();
1533
- const { safeAddress, delegateAddress, chainId } = agent;
1534
- if (!delegateAddress) {
1535
- throw new HavenApiError("Agent has no delegate address.", 422);
1536
- }
1537
- const rpcUrl = this.chainRpcs[chainId];
1538
- if (!rpcUrl) {
1539
- throw new HavenApiError(
1540
- `chainRpcs[${chainId}] must be configured to sweep the delegate wallet.`,
1541
- 422
1542
- );
1543
- }
1544
- const provider = createJsonRpcProvider(rpcUrl);
1545
- const wallet = createWallet(this.delegateKey, provider);
1546
- const ERC20_TRANSFER_ABI = ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"];
1547
- const transfers = [];
1548
- const usdcAddress = CHAIN_USDC[chainId];
1549
- if (usdcAddress) {
1550
- const usdcContract = createErc20Contract(usdcAddress, ERC20_TRANSFER_ABI, wallet);
1551
- const usdcBalance = await usdcContract.balanceOf(delegateAddress);
1552
- if (usdcBalance > 0n) {
1553
- const tx = await usdcContract.transfer(safeAddress, usdcBalance);
1554
- const receipt = await tx.wait(1);
1555
- const txHash = receipt?.hash ?? tx.hash;
1556
- transfers.push({
1557
- asset: "USDC",
1558
- amount: formatAtomicAmount(usdcBalance, 6),
1559
- amountAtomic: usdcBalance.toString(),
1560
- txHash,
1561
- explorerUrl: buildExplorerUrl(chainId, txHash)
1562
- });
1563
- }
1564
- }
1565
- const ethBalance = await provider.getBalance(delegateAddress);
1566
- if (ethBalance > 0n) {
1567
- const fee = await provider.getFeeData();
1568
- const effectiveGasPrice = fee.maxFeePerGas ?? fee.gasPrice ?? 1000000n;
1569
- const gasLimit = 21000n;
1570
- const gasCost = effectiveGasPrice * gasLimit * 2n;
1571
- const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
1572
- if (ethToSend > 0n) {
1573
- const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
1574
- const receipt = await tx.wait(1);
1575
- const txHash = receipt?.hash ?? tx.hash;
1576
- transfers.push({
1577
- asset: "ETH",
1578
- amount: formatAtomicAmount(ethToSend, 18),
1579
- amountAtomic: ethToSend.toString(),
1580
- txHash,
1581
- explorerUrl: buildExplorerUrl(chainId, txHash)
1582
- });
1583
- }
1584
- }
1585
- return {
1586
- fromAddress: delegateAddress,
1587
- toAddress: safeAddress,
1588
- chainId,
1589
- transfers
1590
- };
3021
+ return this.delegateSweep.sweepDelegate();
1591
3022
  }
1592
3023
  /**
1593
3024
  * Hosted (keyless) split-signer sweep — step 1 of 2.
@@ -1598,7 +3029,7 @@ var HavenClient = class {
1598
3029
  * edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
1599
3030
  */
1600
3031
  async prepareSweep() {
1601
- return this.post("/machine-payments/sweep/prepare", {});
3032
+ return this.delegateSweep.prepareSweep();
1602
3033
  }
1603
3034
  /**
1604
3035
  * Hosted (keyless) split-signer sweep — step 2 of 2.
@@ -1608,40 +3039,13 @@ var HavenClient = class {
1608
3039
  * the key.
1609
3040
  */
1610
3041
  async submitSweep(authorization, signature) {
1611
- return this.post("/machine-payments/sweep/submit", {
1612
- authorization,
1613
- signature
1614
- });
3042
+ return this.delegateSweep.submitSweep(authorization, signature);
1615
3043
  }
1616
3044
  /**
1617
3045
  * Get configured and on-chain allowances for the authenticated agent.
1618
3046
  */
1619
3047
  async getAllowances() {
1620
- const raw = await this.get("/machine-payments/allowances");
1621
- return {
1622
- agentId: raw.agent_id,
1623
- safeAddress: raw.safe_address,
1624
- delegateAddress: raw.delegate_address,
1625
- chainId: raw.chain_id,
1626
- allowances: raw.allowances.map((allowance) => ({
1627
- id: allowance.id,
1628
- tokenAddress: allowance.token_address,
1629
- tokenSymbol: allowance.token_symbol,
1630
- configuredAmount: allowance.configured_amount,
1631
- resetPeriodMin: allowance.reset_period_min,
1632
- onchain: {
1633
- amount: allowance.onchain.amount,
1634
- spent: allowance.onchain.spent,
1635
- remaining: allowance.onchain.remaining,
1636
- effectiveSpent: allowance.onchain.effective_spent,
1637
- resetTimeMin: allowance.onchain.reset_time_min,
1638
- lastResetMin: allowance.onchain.last_reset_min,
1639
- nonce: allowance.onchain.nonce,
1640
- isResetPending: allowance.onchain.is_reset_pending,
1641
- remainingIsFromChain: allowance.onchain.remaining_is_from_chain
1642
- }
1643
- }))
1644
- };
3048
+ return this.accountReads.getAllowances();
1645
3049
  }
1646
3050
  /**
1647
3051
  * Post-purchase allowance/budget summary for a settled payment (#1310).
@@ -1672,62 +3076,7 @@ var HavenClient = class {
1672
3076
  * phrase it as guaranteed-fresh.
1673
3077
  */
1674
3078
  async getPostPurchaseAllowanceSummary(paymentId) {
1675
- const unavailable = (detail) => ({
1676
- payment: null,
1677
- allowance: null,
1678
- warnings: [
1679
- {
1680
- code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
1681
- message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
1682
- }
1683
- ]
1684
- });
1685
- const [statusResult, agentResult, allowanceResult] = await Promise.allSettled([
1686
- this.getPaymentStatus(paymentId),
1687
- this.getAgent(),
1688
- this.getAllowances()
1689
- ]);
1690
- if (statusResult.status === "rejected") {
1691
- return unavailable(statusResult.reason instanceof Error ? statusResult.reason.message : String(statusResult.reason));
1692
- }
1693
- const status = statusResult.value;
1694
- if (agentResult.status === "rejected") {
1695
- return { ...unavailable(agentResult.reason instanceof Error ? agentResult.reason.message : String(agentResult.reason)), payment: status };
1696
- }
1697
- if (allowanceResult.status === "rejected") {
1698
- return { ...unavailable(allowanceResult.reason instanceof Error ? allowanceResult.reason.message : String(allowanceResult.reason)), payment: status };
1699
- }
1700
- try {
1701
- const tokenAddress = status.asset ?? status.x402?.asset ?? null;
1702
- if (!tokenAddress) {
1703
- return { ...unavailable("the settled payment does not carry a resolvable token address"), payment: status };
1704
- }
1705
- const rail = agentResult.value.executionRail;
1706
- const source = rail === "delegation" ? "active_delegations" : "allowance_module";
1707
- const match = allowanceResult.value.allowances.find(
1708
- (a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
1709
- );
1710
- if (!match) {
1711
- return { ...unavailable("no allowance/budget row matches the settled token"), payment: status };
1712
- }
1713
- const token = resolveTokenFromAddress(match.tokenAddress);
1714
- const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
1715
- return {
1716
- payment: status,
1717
- allowance: {
1718
- rail,
1719
- remaining_atomic: match.onchain.remaining,
1720
- ...remainingDisplay ? { remaining_display: remainingDisplay } : {},
1721
- token_symbol: match.tokenSymbol,
1722
- token_address: match.tokenAddress,
1723
- reset_period: match.resetPeriodMin,
1724
- source
1725
- },
1726
- warnings: []
1727
- };
1728
- } catch (err) {
1729
- return unavailable(err instanceof Error ? err.message : String(err));
1730
- }
3079
+ return this.accountReads.getPostPurchaseAllowanceSummary(paymentId);
1731
3080
  }
1732
3081
  /**
1733
3082
  * `haven_get_payment_status` convenience: fetch status and, for a
@@ -1784,9 +3133,7 @@ var HavenClient = class {
1784
3133
  * List recent machine-payment receipts/evidence for bookkeeping.
1785
3134
  */
1786
3135
  async listReceipts(options = {}) {
1787
- const query = options.limit ? `?limit=${encodeURIComponent(String(options.limit))}` : "";
1788
- const raw = await this.get(`/machine-payments/receipts${query}`);
1789
- return raw.receipts.map((receipt) => this.mapPaymentReceipt(receipt));
3136
+ return this.accountReads.listReceipts(options);
1790
3137
  }
1791
3138
  /**
1792
3139
  * Fetch the verifiable receipt bundle for a settled payment and verify it
@@ -1795,10 +3142,7 @@ var HavenClient = class {
1795
3142
  * authorisation, so the result is trustworthy even if the backend lied.
1796
3143
  */
1797
3144
  async getReceipt(paymentId) {
1798
- const { receipt } = await this.get(
1799
- `/payments/${paymentId}/receipt`
1800
- );
1801
- return { receipt, verification: verifyPaymentReceipt(receipt) };
3145
+ return this.accountReads.getReceipt(paymentId);
1802
3146
  }
1803
3147
  /**
1804
3148
  * Rehydrate the x402 resume-state bundle for a payment id (#1328: the MPP
@@ -1851,17 +3195,16 @@ var HavenClient = class {
1851
3195
  );
1852
3196
  }
1853
3197
  const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
1854
- const cached = this.x402ReceiptCache.get(idempotencyKey);
1855
- if (cached && cached.expiresAt > Date.now()) return cached.receipt;
3198
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3199
+ if (cached) return cached;
1856
3200
  const inFlight = this.inFlightX402.get(idempotencyKey);
1857
3201
  if (inFlight) return inFlight;
1858
- const promise = this.authorizeStandardX402(paymentRequired, option, idempotencyKey);
3202
+ const promise = this.fundingLeg.authorize(paymentRequired, option, idempotencyKey);
1859
3203
  this.inFlightX402.set(idempotencyKey, promise);
1860
3204
  try {
1861
3205
  return await promise;
1862
3206
  } catch (err) {
1863
- this.attachResumeState(err, {
1864
- rail: "x402",
3207
+ attachResumeState(err, {
1865
3208
  paymentRequired,
1866
3209
  accepted: option,
1867
3210
  idempotencyKey
@@ -1876,9 +3219,9 @@ var HavenClient = class {
1876
3219
  * payment or approval request.
1877
3220
  */
1878
3221
  async quoteX402(url, init, options = {}) {
1879
- const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
1880
- const request = this.snapshotX402Request(url, initialInit);
1881
- const response = await this.merchantFetch(url, initialInit);
3222
+ const initialInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3223
+ const request = snapshotX402Request(url, initialInit);
3224
+ const response = await this.merchantTransport.fetch(url, initialInit);
1882
3225
  if (response.status !== 402) {
1883
3226
  throw new X402UnexpectedStatusError(
1884
3227
  `Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
@@ -1889,8 +3232,8 @@ var HavenClient = class {
1889
3232
  throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
1890
3233
  }
1891
3234
  const paymentRequired = await parsePaymentRequiredResponse(response);
1892
- const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
1893
- return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
3235
+ const mcpTransport = await this.merchantTransport.detect(url, paymentRequired, response);
3236
+ return buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
1894
3237
  }
1895
3238
  /**
1896
3239
  * Probe an MCP tool for its x402 quote without creating a payment.
@@ -1903,8 +3246,8 @@ var HavenClient = class {
1903
3246
  * callers that need a plain x402 endpoint must use {@link quoteX402}.
1904
3247
  */
1905
3248
  async quoteMcpX402(url, init, options = {}) {
1906
- const wallet = await this.resolveX402WalletForMerchantCall();
1907
- const sessionId = await this.mcpInitialize(url, init, wallet);
3249
+ const wallet = await this.merchantCompletion.resolveWalletForMerchantCall();
3250
+ const sessionId = await this.merchantTransport.initialize(url, init, wallet);
1908
3251
  if (!sessionId) {
1909
3252
  throw new HavenApiError(
1910
3253
  "The merchant did not establish an MCP session before the x402 quote. No payment was created.",
@@ -1912,235 +3255,71 @@ var HavenClient = class {
1912
3255
  { mcpSessionNotEstablished: true }
1913
3256
  );
1914
3257
  }
1915
- let requestInit = this.withX402Wallet(init, wallet);
1916
- requestInit = this.withMcpHeaders(requestInit, sessionId);
3258
+ let requestInit = withX402Wallet(init, wallet);
3259
+ requestInit = this.merchantTransport.withSessionHeaders(requestInit, sessionId);
1917
3260
  const quote = await this.quoteX402(url, requestInit, options);
1918
3261
  return {
1919
3262
  ...quote,
1920
3263
  mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
1921
3264
  };
1922
3265
  }
1923
- /**
1924
- * Pay a previously inspected x402 quote and retry the exact captured request.
1925
- */
1926
- async payX402Quote(quote, options = {}) {
1927
- const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
1928
- try {
1929
- const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
1930
- return this.retryX402Request(
1931
- quote.request.url,
1932
- this.requestInitFromSnapshot(quote.request),
1933
- quote.paymentRequired,
1934
- receipt
1935
- );
1936
- } catch (err) {
1937
- this.attachResumeState(err, {
1938
- rail: "x402",
1939
- paymentRequired: quote.paymentRequired,
1940
- accepted: quote.accepted,
1941
- idempotencyKey,
1942
- request: quote.request
1943
- });
1944
- throw err;
1945
- }
1946
- }
1947
- async authorizeStandardX402(paymentRequired, option, idempotencyKey) {
1948
- const raw = await this.post("/x402", {
1949
- url: paymentRequired.resource.url,
1950
- payTo: this.delegateAddress,
1951
- merchantPayTo: option.payTo,
1952
- amount: x402AuthorizationAmount(option),
1953
- asset: option.asset,
1954
- network: option.network,
1955
- description: paymentRequired.resource.description,
1956
- idempotencyKey,
1957
- // #1360: same explicit funding-leg declaration as createX402Intent —
1958
- // this local-key path derives payTo from the key (never stale), but the
1959
- // declaration keeps both writers of the 3009 shape loud-by-default.
1960
- settlementScheme: "eip3009"
1961
- });
1962
- const state = this.paymentStateFromRaw("x402 payment", raw);
1963
- const executedReplay = raw.success && raw.tx_hash ? "idempotency-collision" : state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request ? "approval-resume" : null;
1964
- if (executedReplay) {
1965
- const canFund = await this.delegateCanFund(
1966
- raw.chain_id ?? state?.chainId ?? chainIdFromNetwork(option.network),
1967
- option.asset,
1968
- x402AuthorizationAmount(option)
1969
- );
1970
- const refuse = executedReplay === "idempotency-collision" ? canFund !== true : canFund === false;
1971
- if (refuse) {
1972
- const settledReceipt = state && executedReplay === "approval-resume" ? this.mapX402ReceiptFromStatus(paymentRequired, option, void 0, state) : this.mapX402ReceiptFromAuthorization(paymentRequired, option, void 0, raw);
1973
- throw new X402AlreadySettledError(
1974
- canFund === false ? "This x402 payment already settled \u2014 the delegate no longer holds the funds to authorize it again. To buy the same item a second time, pass a distinct `idempotencyKey`; the synthesised key intentionally collapses repeat calls for the same product within a 5-minute window so a retried request cannot pay twice." : "This x402 payment already settled, and whether the delegate can still fund a new authorization could not be verified (no `chainRpcs` entry for this chain). Refusing rather than issue an authorization that may be unfundable. To buy the same item a second time, pass a distinct `idempotencyKey`; to finish an interrupted payment, resume it by `paymentId`.",
1975
- settledReceipt,
1976
- canFund === false ? "settled" : "unverifiable"
1977
- );
1978
- }
1979
- }
1980
- const paymentHeader = await this.createStandardX402Header(paymentRequired, option);
1981
- if (raw.success && raw.tx_hash) {
1982
- const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
1983
- this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
1984
- return receipt2;
1985
- }
1986
- if (state?.nextAction === AgentPaymentNextAction.RetryOriginalX402Request) {
1987
- const receipt2 = this.mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, state);
1988
- this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
1989
- return receipt2;
1990
- }
1991
- this.throwIfNonSignableAuthorizationState("x402 payment", raw);
1992
- if (!raw.sign_data?.hash) {
1993
- throw new HavenApiError("No sign_hash returned from x402/authorize", 500, raw);
1994
- }
1995
- const sig = await this.signForData(raw.sign_data);
1996
- const execResult = await this.post(
1997
- `/payments/${raw.payment_id}/sign`,
1998
- { signature: sig }
1999
- );
2000
- if (execResult.status !== "confirmed") {
2001
- this.throwPaymentStateError("x402 payment", execResult);
2002
- }
2003
- await this.waitForFundingTx(
2004
- execResult.tx_hash,
2005
- execResult.chain_id ?? chainIdFromNetwork(option.network)
2006
- );
2007
- const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
2008
- this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
2009
- return receipt;
2010
- }
2011
- /**
2012
- * Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
2013
- *
2014
- * The whole point of this path is what it does NOT do. There is no funding
2015
- * leg: the merchant redeems a delegation chain and pulls from the treasury
2016
- * directly, so the delegate EOA never holds the money, no sweep can strand
2017
- * it, and the #713 reconciliation class does not apply. It is also why this
2018
- * method is SMALLER than the 3009 path — the backend assembles the merchant
2019
- * `X-PAYMENT` header in `assembleSettlementPayload`, so the SDK builds no
2020
- * header locally.
2021
- *
2022
- * authorize (payTo = the MERCHANT) → sign the child → settle → header
2023
- *
2024
- * The caller then retries the merchant with that header. **Nothing has
2025
- * settled when this returns** — that is why it does not return an
2026
- * `X402Receipt`.
2027
- *
2028
- * Requires a delegation-rail account. The backend enforces that
2029
- * (`validateGenericSchemeRail`), and so does this method, before building a
2030
- * request the backend would only reject: an error a client can explain is
2031
- * worth more than a 400 it has to decode.
2032
- *
2033
- * **MCP callers must pass `options.resourceUrl`.** An in-band MCP 402
2034
- * challenge frequently carries no `resource` object at all, so
2035
- * `paymentRequired.resource?.url` is undefined and the backend answers
2036
- * "Valid url is required". The QA scenario this path was ported from falls
2037
- * back to the request URL for exactly that reason — the SDK cannot, because
2038
- * it never saw the request. Pass it.
2039
- */
2040
- async settleX402Erc7710(paymentRequired, options = {}) {
2041
- if (!this.delegateKey) {
2042
- throw new HavenSigningError(
2043
- "delegateKey is required for x402 payments. Pass it in the HavenClient config."
2044
- );
2045
- }
2046
- const prepared = await this.prepareX402Erc7710(paymentRequired, options);
2047
- const signature = await this.signForData(prepared.signData);
2048
- const paymentHeader = await this.submitX402Erc7710(prepared.paymentId, signature);
2049
- return { ...prepared.settlement, paymentHeader };
2050
- }
2051
- /**
2052
- * The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
2053
- * the request, and return the child to be signed — without signing it.
2054
- *
2055
- * Split out because the hosted topology cannot use `settleX402Erc7710()`:
2056
- * that method signs in-process with `delegateKey`, and hosted Haven does not
2057
- * have one and must not. The hosted MCP server drives these two halves with
2058
- * the LOCAL signer in between, so the key stays where it belongs and the
2059
- * request shaping stays in one place rather than being reimplemented.
2060
- */
2061
- async prepareX402Erc7710(paymentRequired, options = {}) {
2062
- const delegationRail = options.delegationRail ?? (await this.getAgent()).executionRail === "delegation";
2063
- if (!delegationRail) {
2064
- throw new HavenApiError(
2065
- "erc7710 settlement requires a delegation-rail account; this one is not on it. Use authorizeX402() for the standard EIP-3009 path.",
2066
- 400
2067
- );
2068
- }
2069
- const selection = selectX402SettlementScheme(paymentRequired.accepts, { delegationRail });
2070
- if (!selection || selection.scheme !== "erc7710") {
2071
- throw new HavenApiError(
2072
- "This merchant does not advertise an erc7710 settlement option (no accepts[] entry carries extra.assetTransferMethod: 'erc7710'). Use authorizeX402() for the standard EIP-3009 path.",
2073
- 400
2074
- );
2075
- }
2076
- const option = selection.option;
2077
- const merchantPayTo = option.payTo;
2078
- const amountAtomic = x402AuthorizationAmount(option);
2079
- const raw = await this.post("/x402", {
2080
- url: options.resourceUrl ?? paymentRequired.resource?.url,
2081
- // payTo = the MERCHANT is what selects direct settlement server-side.
2082
- // The explicit settlementScheme must AGREE with that shape (#1360) —
2083
- // disagreement is a 400 by design, so that a stale delegate address
2084
- // becomes a loud mismatch instead of a silent reroute to the 3009 leg.
2085
- payTo: merchantPayTo,
2086
- settlementScheme: "erc7710",
2087
- amount: amountAtomic,
2088
- asset: option.asset,
2089
- network: option.network,
2090
- // The v2 header echoes the accepted entry field-for-field, so the quoted
2091
- // timeout must round-trip or the merchant rejects the echo (#1064).
2092
- maxTimeoutSeconds: option.maxTimeoutSeconds,
2093
- // #1058: forward the advertised facilitators verbatim — the child becomes
2094
- // redeemable ONLY by them. `null` here means the merchant advertised none
2095
- // (or an empty array, which the backend 400s on), so the field is OMITTED
2096
- // rather than sent empty. See x402FacilitatorAddresses.
2097
- ...selection.facilitatorAddresses ? { facilitatorAddresses: selection.facilitatorAddresses } : {}
2098
- });
2099
- if (!raw.payment_id) {
2100
- throw new HavenApiError("No payment_id returned from x402/authorize", 500, raw);
2101
- }
2102
- const signData = raw.sign_data;
2103
- if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
2104
- throw new HavenApiError(
2105
- `x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
2106
- 500,
2107
- raw
3266
+ /**
3267
+ * Pay a previously inspected x402 quote and retry the exact captured request.
3268
+ */
3269
+ async payX402Quote(quote, options = {}) {
3270
+ const idempotencyKey = options.idempotencyKey ?? quote.idempotencyKey;
3271
+ try {
3272
+ const receipt = await this.authorizeX402(quote.paymentRequired, { idempotencyKey });
3273
+ return this.merchantCompletion.retryRequest(
3274
+ quote.request.url,
3275
+ requestInitFromSnapshot(quote.request),
3276
+ quote.paymentRequired,
3277
+ receipt
2108
3278
  );
3279
+ } catch (err) {
3280
+ attachResumeState(err, {
3281
+ paymentRequired: quote.paymentRequired,
3282
+ accepted: quote.accepted,
3283
+ idempotencyKey,
3284
+ request: quote.request
3285
+ });
3286
+ throw err;
2109
3287
  }
2110
- return {
2111
- paymentId: raw.payment_id,
2112
- signData,
2113
- settlement: {
2114
- paymentId: raw.payment_id,
2115
- merchantPayTo,
2116
- amountAtomic,
2117
- asset: option.asset,
2118
- network: option.network,
2119
- facilitatorAddresses: selection.facilitatorAddresses
2120
- }
2121
- };
3288
+ }
3289
+ /**
3290
+ * Pay a merchant through **erc7710 direct settlement** (#1454, epic #1450).
3291
+ *
3292
+ * **Nothing has settled when this returns** — that is why it does not return
3293
+ * an `X402Receipt`; the caller still has to retry the merchant with the
3294
+ * header. **MCP callers must pass `options.resourceUrl`**, because an in-band
3295
+ * MCP 402 challenge frequently carries no `resource` object at all.
3296
+ *
3297
+ * Both caveats, and why this scheme has no funding leg, are explained where
3298
+ * the lifecycle lives: `x402-erc7710.ts` (#1619).
3299
+ */
3300
+ async settleX402Erc7710(paymentRequired, options = {}) {
3301
+ return this.erc7710.settle(paymentRequired, options);
3302
+ }
3303
+ /**
3304
+ * The AUTHORIZE half of erc7710 settlement (#1456): select the scheme, build
3305
+ * the request, and return the child to be signed — without signing it.
3306
+ *
3307
+ * Split out because the hosted topology cannot use `settleX402Erc7710()`:
3308
+ * that method signs in-process with `delegateKey`, and hosted Haven does not
3309
+ * have one and must not.
3310
+ */
3311
+ async prepareX402Erc7710(paymentRequired, options = {}) {
3312
+ return this.erc7710.prepare(paymentRequired, options);
2122
3313
  }
2123
3314
  /**
2124
3315
  * The SETTLE half (#1456): exchange the signed child for the merchant header.
2125
3316
  *
2126
3317
  * The SDK builds no header on this path — the backend assembles the MetaMask
2127
- * erc7710 payload in `assembleSettlementPayload`. Whoever produced the
2128
- * signature (an in-process delegate key, or the local edge signer over the
2129
- * hosted boundary) is irrelevant here.
3318
+ * erc7710 payload. Whoever produced the signature (an in-process delegate
3319
+ * key, or the local edge signer over the hosted boundary) is irrelevant.
2130
3320
  */
2131
3321
  async submitX402Erc7710(paymentId, signature) {
2132
- const settled = await this.post(
2133
- `/x402/${paymentId}/settle`,
2134
- { signature }
2135
- );
2136
- if (!settled.payment_header) {
2137
- throw new HavenApiError(
2138
- "x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
2139
- 500,
2140
- settled
2141
- );
2142
- }
2143
- return settled.payment_header;
3322
+ return this.erc7710.submit(paymentId, signature);
2144
3323
  }
2145
3324
  async resumeAuthorizedX402(input) {
2146
3325
  if (!this.delegateKey) {
@@ -2159,11 +3338,11 @@ var HavenClient = class {
2159
3338
  );
2160
3339
  }
2161
3340
  const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
2162
- const cached = this.x402ReceiptCache.get(idempotencyKey);
2163
- if (cached && cached.expiresAt > Date.now()) return cached.receipt;
3341
+ const cached = this.fundingLeg.cachedReceipt(idempotencyKey);
3342
+ if (cached) return cached;
2164
3343
  const status = await this.getPaymentStatus(input.paymentId);
2165
- this.assertCanResumeX402(status, input.paymentRequired, option);
2166
- const canFund = await this.delegateCanFund(
3344
+ assertCanResumeX402(status, input.paymentRequired, option);
3345
+ const canFund = await this.fundingLeg.delegateCanFund(
2167
3346
  status.chainId ?? chainIdFromNetwork(option.network),
2168
3347
  option.asset,
2169
3348
  x402AuthorizationAmount(option)
@@ -2171,20 +3350,20 @@ var HavenClient = class {
2171
3350
  if (canFund === false) {
2172
3351
  throw new X402AlreadySettledError(
2173
3352
  `x402 payment ${status.paymentId} has already settled \u2014 the delegate no longer holds the funds to authorize it again, so there is nothing left to resume.`,
2174
- this.mapX402ReceiptFromStatus(input.paymentRequired, option, void 0, status),
3353
+ this.fundingLeg.receiptFromStatus(input.paymentRequired, option, void 0, status),
2175
3354
  "settled"
2176
3355
  );
2177
3356
  }
2178
- const paymentHeader = await this.createStandardX402Header(input.paymentRequired, option);
2179
- const receipt = this.mapX402ReceiptFromStatus(input.paymentRequired, option, paymentHeader, status);
2180
- this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
3357
+ const paymentHeader = await this.fundingLeg.createPaymentHeader(input.paymentRequired, option);
3358
+ const receipt = this.fundingLeg.receiptFromStatus(input.paymentRequired, option, paymentHeader, status);
3359
+ this.fundingLeg.cacheReceipt(idempotencyKey, paymentHeader, receipt);
2181
3360
  return receipt;
2182
3361
  }
2183
3362
  async resumeX402Payment(input) {
2184
3363
  const inputInit = "init" in input ? input.init : void 0;
2185
- const initialInit = this.withX402Wallet(
2186
- inputInit ?? (input.request ? this.requestInitFromSnapshot(input.request) : void 0),
2187
- this.x402PayerAddress()
3364
+ const initialInit = withX402Wallet(
3365
+ inputInit ?? (input.request ? requestInitFromSnapshot(input.request) : void 0),
3366
+ x402PayerAddress(this.delegateAddress, this.x402Wallet)
2188
3367
  );
2189
3368
  let paymentRequired = input.paymentRequired;
2190
3369
  const url = input.url ?? input.request?.url;
@@ -2192,7 +3371,7 @@ var HavenClient = class {
2192
3371
  if (!url) {
2193
3372
  throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
2194
3373
  }
2195
- const response = await this.merchantFetch(url, initialInit);
3374
+ const response = await this.merchantTransport.fetch(url, initialInit);
2196
3375
  if (response.status !== 402) {
2197
3376
  throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
2198
3377
  }
@@ -2203,7 +3382,7 @@ var HavenClient = class {
2203
3382
  paymentRequired,
2204
3383
  idempotencyKey: input.idempotencyKey
2205
3384
  });
2206
- return this.retryX402Request(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
3385
+ return this.merchantCompletion.retryRequest(url ?? paymentRequired.resource.url, initialInit, paymentRequired, receipt);
2207
3386
  }
2208
3387
  /**
2209
3388
  * Fetch wrapper that automatically handles HTTP 402 responses.
@@ -2217,268 +3396,57 @@ var HavenClient = class {
2217
3396
  * ```
2218
3397
  *
2219
3398
  * **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
2220
- * MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
2221
- * Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
2222
- * `initialize` handshake, threads the resulting `mcp-session-id`,
2223
- * `Accept: application/json, text/event-stream`, and `x402-wallet` headers
2224
- * through every request, and collapses SSE responses to the JSON-RPC
2225
- * `result`. The caller just passes `(url, { body })` and never sees the
2226
- * protocol plumbing. A non-MCP server (handshake error / no session id)
2227
- * falls back to standard x402 behaviour.
2228
- *
2229
- * Requires `delegateKey` to be set in the client config.
2230
- */
2231
- async fetch(url, init, options = {}) {
2232
- let mcpSessionId;
2233
- if (isMcpUrl(url)) {
2234
- mcpSessionId = await this.mcpInitialize(url, init);
2235
- }
2236
- let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
2237
- if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
2238
- const response = await this.merchantFetch(url, requestInit);
2239
- if (response.status !== 402) {
2240
- return mcpSessionId ? this.surfaceMcpResult(response) : response;
2241
- }
2242
- let paymentRequired;
2243
- try {
2244
- paymentRequired = await parsePaymentRequiredResponse(response);
2245
- } catch {
2246
- return response;
2247
- }
2248
- if (!mcpSessionId && await responseHasBazaarExtension(response)) {
2249
- mcpSessionId = await this.mcpInitialize(url, init);
2250
- if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
2251
- }
2252
- const request = this.snapshotX402Request(url, requestInit);
2253
- const option = selectStandardPaymentOption(paymentRequired.accepts);
2254
- const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
2255
- let receipt;
2256
- try {
2257
- receipt = await this.authorizeX402(paymentRequired, options);
2258
- } catch (err) {
2259
- if (option && idempotencyKey) {
2260
- this.attachResumeState(err, {
2261
- rail: "x402",
2262
- paymentRequired,
2263
- accepted: option,
2264
- idempotencyKey,
2265
- request
2266
- });
2267
- }
2268
- throw err;
2269
- }
2270
- const retryResponse = await this.retryX402Request(url, requestInit, paymentRequired, receipt);
2271
- return mcpSessionId ? this.surfaceMcpResult(retryResponse) : retryResponse;
2272
- }
2273
- // ── MCP-over-x402 transport helpers (issue #315) ─────────────────
2274
- /**
2275
- * Run the MCP `initialize` handshake against a Streamable-HTTP endpoint and
2276
- * return the `mcp-session-id` the server assigns.
2277
- *
2278
- * Returns `undefined` whenever the endpoint is not actually an MCP server —
2279
- * a transport/HTTP error, a missing session id, or a JSON-RPC error in the
2280
- * handshake response — so the caller can fall back to plain x402.
2281
- */
2282
- async mcpInitialize(url, init, wallet = this.x402PayerAddress()) {
2283
- try {
2284
- const headers = new Headers(init?.headers);
2285
- headers.set("Content-Type", "application/json");
2286
- headers.set("Accept", MCP_ACCEPT);
2287
- if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
2288
- const response = await this.merchantFetch(url, {
2289
- method: "POST",
2290
- headers,
2291
- body: JSON.stringify({
2292
- jsonrpc: "2.0",
2293
- id: ++this.mcpRequestId,
2294
- method: "initialize",
2295
- params: {
2296
- protocolVersion: MCP_PROTOCOL_VERSION,
2297
- capabilities: {},
2298
- clientInfo: MCP_CLIENT_INFO
2299
- }
2300
- })
2301
- });
2302
- if (!response.ok) return void 0;
2303
- const sessionId = response.headers.get("mcp-session-id");
2304
- if (!sessionId) return void 0;
2305
- const message = await this.readMcpMessage(response);
2306
- if (message && "error" in message) return void 0;
2307
- await this.mcpNotifyInitialized(url, init, sessionId, wallet);
2308
- return sessionId;
2309
- } catch {
2310
- return void 0;
2311
- }
2312
- }
2313
- /**
2314
- * Send the MCP `notifications/initialized` notification that completes the
2315
- * lifecycle handshake. Best-effort: the session is already established, so a
2316
- * failed notification must not abort the payment.
2317
- */
2318
- async mcpNotifyInitialized(url, init, sessionId, wallet = this.x402PayerAddress()) {
2319
- try {
2320
- const headers = new Headers(init?.headers);
2321
- headers.set("Content-Type", "application/json");
2322
- headers.set("Accept", MCP_ACCEPT);
2323
- headers.set("mcp-session-id", sessionId);
2324
- if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
2325
- await this.merchantFetch(
2326
- url,
2327
- {
2328
- method: "POST",
2329
- headers,
2330
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
2331
- },
2332
- NOTIFY_TIMEOUT
2333
- );
2334
- } catch {
2335
- }
2336
- }
2337
- /** Read a single JSON-RPC message from an MCP response (JSON or SSE body). */
2338
- async readMcpMessage(response) {
2339
- let text;
2340
- try {
2341
- text = await response.clone().text();
2342
- } catch {
2343
- return void 0;
2344
- }
2345
- if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
2346
- return selectJsonRpcResult(parseSseJsonRpcMessages(text));
2347
- }
2348
- try {
2349
- return JSON.parse(text);
2350
- } catch {
2351
- return void 0;
2352
- }
2353
- }
2354
- /** Add the MCP transport headers (session id + SSE Accept) to a request. */
2355
- withMcpHeaders(init, sessionId) {
2356
- const headers = new Headers(init?.headers);
2357
- headers.set("mcp-session-id", sessionId);
2358
- headers.set("Accept", MCP_ACCEPT);
2359
- return { ...init, headers };
2360
- }
2361
- /**
2362
- * Collapse an MCP SSE response into a plain JSON response carrying the
2363
- * JSON-RPC `result`, so callers of `fetch()` never see raw SSE framing.
2364
- * Non-SSE responses pass through untouched.
2365
- */
2366
- async surfaceMcpResult(response) {
2367
- if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
2368
- return response;
2369
- }
2370
- let text;
2371
- try {
2372
- text = await response.clone().text();
2373
- } catch {
2374
- return response;
2375
- }
2376
- const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
2377
- if (!message) return response;
2378
- const body = "result" in message ? message.result : message;
2379
- const headers = new Headers(response.headers);
2380
- headers.set("content-type", "application/json");
2381
- headers.delete("content-length");
2382
- headers.delete("mcp-session-id");
2383
- return new Response(JSON.stringify(body), {
2384
- status: response.status,
2385
- statusText: response.statusText,
2386
- headers
2387
- });
2388
- }
2389
- async retryX402Request(url, initialInit, paymentRequired, receipt) {
2390
- if (!receipt.accepted) {
2391
- throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
2392
- }
2393
- if (!receipt.paymentHeader) {
2394
- throw new HavenApiError("No x402 payment header was returned for payment retry", 500);
2395
- }
2396
- const retryHeaders = new Headers(initialInit?.headers);
2397
- retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
2398
- const retryResponse = await this.merchantFetch(url, {
2399
- ...initialInit,
2400
- headers: retryHeaders
2401
- });
2402
- if (!retryResponse.ok) {
2403
- const merchant = await captureMerchantResponse(retryResponse);
2404
- await this.recordMerchantRetryRejected({
2405
- rail: "x402",
2406
- paymentId: receipt.paymentId,
2407
- txHash: receipt.txHash,
2408
- resourceUrl: receipt.resourceUrl,
2409
- merchant,
2410
- details: {
2411
- merchant_to: receipt.merchantTo,
2412
- delegate_to: receipt.to
2413
- }
2414
- });
2415
- throw new HavenApiError(
2416
- "x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
2417
- merchant.merchant_status,
2418
- {
2419
- marker: "x402_retry_rejected_after_funding",
2420
- payment_id: receipt.paymentId,
2421
- tx_hash: receipt.txHash,
2422
- resource_url: receipt.resourceUrl,
2423
- merchant_to: receipt.merchantTo,
2424
- delegate_to: receipt.to,
2425
- ...merchant
2426
- }
2427
- );
2428
- }
2429
- const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
2430
- if (receipt.merchant && merchantSettlement.settlementTxHash) {
2431
- receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
2432
- receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
2433
- receipt.chainId,
2434
- merchantSettlement.settlementTxHash
2435
- );
2436
- }
2437
- await this.reportMachinePaymentEvidence({
2438
- paymentId: receipt.paymentId,
2439
- rail: "x402",
2440
- txHash: receipt.txHash,
2441
- resourceUrl: receipt.resourceUrl,
2442
- merchantStatus: retryResponse.status,
2443
- challengePayload: paymentRequired,
2444
- selectedPayment: receipt.accepted,
2445
- paymentProofHeaderName: "X-PAYMENT",
2446
- paymentProofHeader: receipt.paymentHeader,
2447
- protocolReceiptHeaderName: "PAYMENT-RESPONSE",
2448
- protocolReceiptHeader: retryResponse.headers.get("PAYMENT-RESPONSE") ?? void 0
2449
- });
2450
- await this.reportMerchantReceipt(receipt.paymentId, retryResponse);
2451
- return retryResponse;
2452
- }
2453
- /**
2454
- * #956: capture the merchant's OWN receipt when the paid response carries
2455
- * one, and report it to Haven so the reporting feed can attach it next to
2456
- * the Haven-generated payment evidence (#498). Two supported signals on the
2457
- * paid response:
2458
- *
2459
- * x-receipt-json: base64-encoded JSON receipt document (inline)
2460
- * x-receipt-url: https URL to the receipt document (reference)
3399
+ * MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
3400
+ * Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
3401
+ * `initialize` handshake, threads the resulting `mcp-session-id`,
3402
+ * `Accept: application/json, text/event-stream`, and `x402-wallet` headers
3403
+ * through every request, and collapses SSE responses to the JSON-RPC
3404
+ * `result`. The caller just passes `(url, { body })` and never sees the
3405
+ * protocol plumbing. A non-MCP server (handshake error / no session id)
3406
+ * falls back to standard x402 behaviour.
2461
3407
  *
2462
- * Strictly best-effort: absence is the normal case, and no failure here may
2463
- * ever affect the completed payment — the response is already paid for.
3408
+ * Requires `delegateKey` to be set in the client config.
2464
3409
  */
2465
- async reportMerchantReceipt(paymentId, response) {
3410
+ async fetch(url, init, options = {}) {
3411
+ let mcpSessionId;
3412
+ if (this.merchantTransport.isMcpUrl(url)) {
3413
+ mcpSessionId = await this.merchantTransport.initialize(url, init);
3414
+ }
3415
+ let requestInit = withX402Wallet(init, x402PayerAddress(this.delegateAddress, this.x402Wallet));
3416
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3417
+ const response = await this.merchantTransport.fetch(url, requestInit);
3418
+ if (response.status !== 402) {
3419
+ return mcpSessionId ? this.merchantTransport.surfaceResult(response) : response;
3420
+ }
3421
+ let paymentRequired;
2466
3422
  try {
2467
- const inlineB64 = response.headers.get("x-receipt-json");
2468
- const url = response.headers.get("x-receipt-url");
2469
- if (!inlineB64 && !url) return;
2470
- let body = null;
2471
- if (inlineB64) {
2472
- if (inlineB64.length > Math.ceil(64 * 1024 * 4 / 3)) return;
2473
- const decoded = JSON.parse(Buffer.from(inlineB64, "base64").toString("utf8"));
2474
- if (decoded && typeof decoded === "object") body = { json: decoded };
2475
- } else if (url && url.startsWith("https://") && url.length <= 2048) {
2476
- body = { url };
2477
- }
2478
- if (!body) return;
2479
- await this.post(`/machine-payments/${paymentId}/merchant-receipt`, body);
3423
+ paymentRequired = await parsePaymentRequiredResponse(response);
2480
3424
  } catch {
3425
+ return response;
3426
+ }
3427
+ if (!mcpSessionId && await this.merchantTransport.hasBazaarExtension(response)) {
3428
+ mcpSessionId = await this.merchantTransport.initialize(url, init);
3429
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3430
+ }
3431
+ const request = snapshotX402Request(url, requestInit);
3432
+ const option = selectStandardPaymentOption(paymentRequired.accepts);
3433
+ const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
3434
+ let receipt;
3435
+ try {
3436
+ receipt = await this.authorizeX402(paymentRequired, options);
3437
+ } catch (err) {
3438
+ if (option && idempotencyKey) {
3439
+ attachResumeState(err, {
3440
+ paymentRequired,
3441
+ accepted: option,
3442
+ idempotencyKey,
3443
+ request
3444
+ });
3445
+ }
3446
+ throw err;
2481
3447
  }
3448
+ const retryResponse = await this.merchantCompletion.retryRequest(url, requestInit, paymentRequired, receipt);
3449
+ return mcpSessionId ? this.merchantTransport.surfaceResult(retryResponse) : retryResponse;
2482
3450
  }
2483
3451
  /**
2484
3452
  * Deliver an already-signed x402 payment header to the merchant and return
@@ -2503,7 +3471,7 @@ var HavenClient = class {
2503
3471
  * and before delivering the X-PAYMENT header, so the merchant's
2504
3472
  * balanceOf(delegate) / transferWithAuthorization verification sees the funded
2505
3473
  * balance — otherwise it rejects with "Payment verification failed". The
2506
- * SDK's local path already does this (see authorizeStandardX402); the hosted
3474
+ * SDK's local path already does this (see `X402FundingLeg.authorize`); the hosted
2507
3475
  * split flow regressed when the 5→3 collapse removed the incidental
2508
3476
  * inter-call latency that used to mask it.
2509
3477
  *
@@ -2520,28 +3488,25 @@ var HavenClient = class {
2520
3488
  */
2521
3489
  async ensureFundingConfirmed(paymentId, fundingTxHash) {
2522
3490
  const status = await this.getPaymentStatus(paymentId);
2523
- await this.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
3491
+ await this.fundingLeg.waitForFundingTx(fundingTxHash ?? status.txHash ?? void 0, status.chainId);
2524
3492
  }
2525
3493
  async completeX402MerchantCall(input) {
2526
- const evidenceContext = await this.resolveX402MerchantCompletionContext({
3494
+ const evidenceContext = await this.merchantCompletion.resolveCompletionContext({
2527
3495
  paymentId: input.paymentId,
2528
3496
  url: input.url,
2529
3497
  noFundingLeg: input.noFundingLeg === true
2530
3498
  });
2531
3499
  const fundingTxHash = evidenceContext.txHash;
2532
- const shouldHandshakeMcp = isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
2533
- const x402Wallet = shouldHandshakeMcp ? await this.resolveX402WalletForMerchantCall() : this.x402PayerAddress();
3500
+ const shouldHandshakeMcp = this.merchantTransport.isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
3501
+ const x402Wallet = shouldHandshakeMcp ? await this.merchantCompletion.resolveWalletForMerchantCall() : x402PayerAddress(this.delegateAddress, this.x402Wallet);
2534
3502
  let mcpSessionId;
2535
3503
  if (shouldHandshakeMcp) {
2536
- mcpSessionId = await this.mcpInitialize(input.url, input.init, x402Wallet);
3504
+ mcpSessionId = await this.merchantTransport.initialize(input.url, input.init, x402Wallet);
2537
3505
  }
2538
- let requestInit = this.withX402Wallet(input.init, x402Wallet) ?? {};
2539
- if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
2540
- const headers = new Headers(requestInit.headers);
2541
- headers.set("X-PAYMENT", input.paymentHeader);
2542
- requestInit = { ...requestInit, headers };
2543
- const response = await this.merchantFetch(input.url, requestInit);
2544
- const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
3506
+ let requestInit = withX402Wallet(input.init, x402Wallet) ?? {};
3507
+ if (mcpSessionId) requestInit = this.merchantTransport.withSessionHeaders(requestInit, mcpSessionId);
3508
+ const response = await this.merchantTransport.deliverPayment(input.url, requestInit, input.paymentHeader);
3509
+ const surfaced = mcpSessionId ? await this.merchantTransport.surfaceResult(response) : response;
2545
3510
  const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
2546
3511
  const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
2547
3512
  const text = await surfaced.text();
@@ -2553,7 +3518,7 @@ var HavenClient = class {
2553
3518
  }
2554
3519
  if (!surfaced.ok) {
2555
3520
  if (!input.noFundingLeg && fundingTxHash) {
2556
- await this.recordMerchantRetryRejected({
3521
+ await this.merchantCompletion.recordRetryRejected({
2557
3522
  rail: "x402",
2558
3523
  paymentId: evidenceContext.paymentId,
2559
3524
  txHash: fundingTxHash,
@@ -2571,7 +3536,7 @@ var HavenClient = class {
2571
3536
  }
2572
3537
  } else {
2573
3538
  if (!input.noFundingLeg && fundingTxHash) {
2574
- await this.reportMachinePaymentEvidence({
3539
+ await this.merchantCompletion.reportEvidence({
2575
3540
  paymentId: evidenceContext.paymentId,
2576
3541
  rail: "x402",
2577
3542
  txHash: fundingTxHash,
@@ -2583,7 +3548,7 @@ var HavenClient = class {
2583
3548
  protocolReceiptHeader
2584
3549
  });
2585
3550
  }
2586
- await this.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
3551
+ await this.merchantCompletion.reportMerchantReceipt(evidenceContext.paymentId, surfaced);
2587
3552
  }
2588
3553
  return {
2589
3554
  status: surfaced.status,
@@ -2613,271 +3578,11 @@ var HavenClient = class {
2613
3578
  arguments: raw.arguments ?? {},
2614
3579
  ...raw.mcp_transport ? {
2615
3580
  mcpTransport: {
2616
- handshakeRequired: raw.mcp_transport.handshake_required,
2617
- source: raw.mcp_transport.source
2618
- }
2619
- } : {}
2620
- };
2621
- }
2622
- async resolveX402MerchantCompletionContext(input) {
2623
- const status = await this.getPaymentStatus(input.paymentId);
2624
- if (status.rail !== "x402") {
2625
- throw new HavenPaymentStateError(
2626
- `Payment ${status.paymentId} is ${status.rail}, not x402.`,
2627
- 409,
2628
- status
2629
- );
2630
- }
2631
- const readyForMerchantCompletion = input.noFundingLeg ? status.kind === "payment_intent" && status.status === "submitted" : status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
2632
- if (!readyForMerchantCompletion) {
2633
- throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
2634
- }
2635
- if (!input.noFundingLeg && !status.txHash) {
2636
- throw new HavenApiError(
2637
- `x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
2638
- 502,
2639
- status,
2640
- status.paymentId
2641
- );
2642
- }
2643
- const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
2644
- if (approvedResourceUrl && approvedResourceUrl !== input.url) {
2645
- throw new HavenApiError(
2646
- "x402 merchant completion does not match the approved resource URL.",
2647
- 409,
2648
- { status, url: input.url },
2649
- status.paymentId
2650
- );
2651
- }
2652
- return {
2653
- paymentId: status.paymentId,
2654
- txHash: status.txHash,
2655
- resourceUrl: approvedResourceUrl ?? input.url,
2656
- merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
2657
- };
2658
- }
2659
- async resolveX402WalletForMerchantCall() {
2660
- const localWallet = this.x402PayerAddress();
2661
- if (localWallet) return localWallet;
2662
- try {
2663
- const agent = await this.getAgent();
2664
- return agent.delegateAddress ?? void 0;
2665
- } catch {
2666
- return void 0;
2667
- }
2668
- }
2669
- // #1328: authorizeMachinePayment / authorizeMppDemoPayment / resumeAuthorizedMpp
2670
- // / resumeMppPayment / fetchWithMachinePayment / retryMppRequest (the
2671
- // MACHINE-PAYMENT-CHALLENGE / mpp_demo client surface) are retired — the
2672
- // backend's POST /machine-payments/authorize refuses unconditionally now,
2673
- // and MACHINE-PAYMENT-CHALLENGE was never produced by any other Haven
2674
- // surface. Use the x402 flow (authorizeX402 / fetch / quoteX402 / payX402Quote)
2675
- // for agent-to-merchant payments.
2676
- assertCanResumeX402(status, paymentRequired, option) {
2677
- if (status.rail !== "x402") {
2678
- throw new HavenPaymentStateError(
2679
- `Payment ${status.paymentId} is ${status.rail}, not x402.`,
2680
- 409,
2681
- status
2682
- );
2683
- }
2684
- if (status.nextAction !== AgentPaymentNextAction.RetryOriginalX402Request) {
2685
- throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
2686
- }
2687
- if (!status.txHash) {
2688
- throw new HavenApiError(
2689
- `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
2690
- 502,
2691
- status,
2692
- status.paymentId
2693
- );
2694
- }
2695
- if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
2696
- throw new HavenApiError(
2697
- "x402 resume request does not match the approved resource URL.",
2698
- 409,
2699
- { status, paymentRequired },
2700
- status.paymentId
2701
- );
2702
- }
2703
- if (status.merchantAddress && !sameAddress3(status.merchantAddress, option.payTo)) {
2704
- throw new HavenApiError(
2705
- "x402 resume request does not match the approved merchant.",
2706
- 409,
2707
- { status, selectedPayment: option },
2708
- status.paymentId
2709
- );
2710
- }
2711
- const optionChainId = chainIdFromNetwork(option.network);
2712
- if (status.chainId && optionChainId && status.chainId !== optionChainId) {
2713
- throw new HavenApiError(
2714
- "x402 resume request does not match the approved network.",
2715
- 409,
2716
- { status, selectedPayment: option },
2717
- status.paymentId
2718
- );
2719
- }
2720
- if (status.token && status.token !== "USDC") {
2721
- throw new HavenApiError(
2722
- "x402 resume request does not match the approved token.",
2723
- 409,
2724
- { status, selectedPayment: option },
2725
- status.paymentId
2726
- );
2727
- }
2728
- const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
2729
- const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(x402AuthorizationAmount(option)));
2730
- if (approvedAmount && approvedAmount !== requestedAmount) {
2731
- throw new HavenApiError(
2732
- "x402 resume request does not match the approved amount.",
2733
- 409,
2734
- { status, selectedPayment: option },
2735
- status.paymentId
2736
- );
2737
- }
2738
- }
2739
- mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
2740
- const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
2741
- const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
2742
- const token = execResult?.token ?? raw.token ?? "USDC";
2743
- const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(x402AuthorizationAmount(option));
2744
- const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
2745
- const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
2746
- const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
2747
- const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
2748
- return this.buildX402Receipt({
2749
- paymentId: raw.payment_id,
2750
- txHash,
2751
- token,
2752
- amount,
2753
- to,
2754
- resourceUrl: paymentRequired.resource.url,
2755
- explorerUrl,
2756
- accepted: option,
2757
- paymentHeader,
2758
- merchantTo,
2759
- payer,
2760
- chainId
2761
- });
2762
- }
2763
- mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, status) {
2764
- if (!status.txHash) {
2765
- throw new HavenApiError(
2766
- `x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
2767
- 502,
2768
- status,
2769
- status.paymentId
2770
- );
2771
- }
2772
- return this.buildX402Receipt({
2773
- paymentId: status.paymentId,
2774
- txHash: status.txHash,
2775
- token: status.token || "USDC",
2776
- amount: status.amount || decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
2777
- to: this.delegateAddress ?? "",
2778
- resourceUrl: paymentRequired.resource.url,
2779
- explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
2780
- accepted: option,
2781
- paymentHeader,
2782
- merchantTo: status.merchantAddress ?? option.payTo,
2783
- payer: this.x402Wallet,
2784
- chainId: status.chainId || chainIdFromNetwork(option.network)
2785
- });
2786
- }
2787
- buildX402Receipt(input) {
2788
- const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
2789
- return {
2790
- success: true,
2791
- paymentId: input.paymentId,
2792
- txHash: input.txHash,
2793
- token: input.token,
2794
- amount: input.amount,
2795
- to: input.to,
2796
- resourceUrl: input.resourceUrl,
2797
- explorerUrl: input.explorerUrl,
2798
- accepted: input.accepted,
2799
- paymentHeader: input.paymentHeader,
2800
- merchantTo: input.merchantTo ?? input.accepted.payTo,
2801
- payer: input.payer,
2802
- chainId: input.chainId,
2803
- haven: {
2804
- paymentId: input.paymentId,
2805
- fundingTxHash: input.txHash,
2806
- fundingExplorerUrl
2807
- },
2808
- merchant: {
2809
- payTo: input.merchantTo ?? input.accepted.payTo
2810
- },
2811
- x402: {
2812
- amount: x402AuthorizationAmount(input.accepted),
2813
- token: input.token,
2814
- network: input.accepted.network,
2815
- asset: input.accepted.asset,
2816
- resource: input.accepted.resource ?? input.resourceUrl
2817
- }
2818
- };
2819
- }
2820
- async createStandardX402Header(paymentRequired, option) {
2821
- if (!this.delegateKey) {
2822
- throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
2823
- }
2824
- const account = accounts.privateKeyToAccount(this.delegateKey);
2825
- const requirements = toStandardPaymentRequirements(paymentRequired, option);
2826
- const header = await schemes.exact.evm.createPaymentHeader(
2827
- account,
2828
- paymentRequired.x402Version,
2829
- requirements
2830
- );
2831
- if (paymentRequired.x402Version < 2) return header;
2832
- const payment = decodeBase64Json(header);
2833
- return encodeBase64Json({
2834
- x402Version: paymentRequired.x402Version,
2835
- accepted: option,
2836
- payload: payment.payload
2837
- });
2838
- }
2839
- cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
2840
- const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
2841
- if (expiresAt > Date.now()) {
2842
- this.x402ReceiptCache.set(idempotencyKey, { expiresAt, receipt });
2843
- }
2844
- }
2845
- async recordMerchantRetryRejected(input) {
2846
- try {
2847
- await this.post("/machine-payments/reconciliation-events", {
2848
- paymentId: input.paymentId,
2849
- rail: input.rail,
2850
- eventType: "merchant_retry_rejected_after_payment",
2851
- txHash: input.txHash,
2852
- reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
2853
- details: {
2854
- resource_url: input.resourceUrl,
2855
- retry_status: input.merchant.merchant_status,
2856
- retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
2857
- ...input.details
2858
- }
2859
- });
2860
- } catch {
2861
- }
2862
- }
2863
- async reportMachinePaymentEvidence(input) {
2864
- try {
2865
- await this.post("/machine-payments/evidence", {
2866
- paymentId: input.paymentId,
2867
- rail: input.rail,
2868
- txHash: input.txHash,
2869
- resourceUrl: input.resourceUrl,
2870
- merchantStatus: input.merchantStatus,
2871
- challengePayload: input.challengePayload,
2872
- selectedPayment: input.selectedPayment,
2873
- paymentProofHeaderName: input.paymentProofHeaderName,
2874
- paymentProofHeader: input.paymentProofHeader,
2875
- protocolReceiptHeaderName: input.protocolReceiptHeaderName,
2876
- protocolReceiptHeader: input.protocolReceiptHeader,
2877
- protocolReceiptPayload: input.protocolReceiptHeader ? parseProtocolReceiptHeader(input.protocolReceiptHeader) : void 0
2878
- });
2879
- } catch {
2880
- }
3581
+ handshakeRequired: raw.mcp_transport.handshake_required,
3582
+ source: raw.mcp_transport.source
3583
+ }
3584
+ } : {}
3585
+ };
2881
3586
  }
2882
3587
  /**
2883
3588
  * Wait for a funding tx to be mined with ≥1 confirmation before the
@@ -2888,264 +3593,9 @@ var HavenClient = class {
2888
3593
  * backend has already confirmed on-chain submission and callers accept the
2889
3594
  * small propagation window as a trade-off for not configuring an RPC URL.
2890
3595
  */
2891
- async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
2892
- if (!txHash || !chainId) return;
2893
- const rpcUrl = this.chainRpcs[chainId];
2894
- if (!rpcUrl) return;
2895
- const provider = createJsonRpcProvider(rpcUrl);
2896
- const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
2897
- if (!onChainReceipt || onChainReceipt.status !== 1) {
2898
- throw new HavenApiError(
2899
- "Funding tx did not confirm on-chain within the timeout window.",
2900
- 500,
2901
- { txHash, chainId }
2902
- );
2903
- }
2904
- }
2905
- /**
2906
- * Can the delegate EOA still fund an authorization for `amountAtomic`?
2907
- *
2908
- * #1521: the only question that separates a legitimate resume (funding
2909
- * confirmed, merchant never paid — the delegate still holds the money) from
2910
- * a replayed settled payment (funding confirmed, merchant paid, delegate
2911
- * spent). The intent's own `status: 'confirmed'` is identical in both.
2912
- *
2913
- * The balance is asked of the CHAIN rather than of Haven's bookkeeping on
2914
- * purpose: the merchant-settlement evidence record is written by this SDK
2915
- * *after* the merchant call, so a client that dies between the two leaves
2916
- * the backend believing the merchant was never paid — the exact case the
2917
- * discriminator has to get right. The chain cannot be behind in that way.
2918
- *
2919
- * Returns `null` — never a guess — when `chainRpcs` has no entry for the
2920
- * chain or the read fails. Callers must treat that as "unverifiable", not
2921
- * as "funded".
2922
- */
2923
- async delegateCanFund(chainId, tokenAddress, amountAtomic, timeoutMs = 1e4) {
2924
- if (!chainId || !this.delegateAddress) return null;
2925
- const rpcUrl = this.chainRpcs[chainId];
2926
- if (!rpcUrl) return null;
2927
- try {
2928
- const provider = createJsonRpcProvider(rpcUrl);
2929
- const token = createErc20Contract(
2930
- tokenAddress,
2931
- ["function balanceOf(address) view returns (uint256)"],
2932
- provider
2933
- );
2934
- const balance = await Promise.race([
2935
- token.balanceOf(this.delegateAddress),
2936
- new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs).unref?.())
2937
- ]);
2938
- if (balance === null) return null;
2939
- return balance >= BigInt(amountAtomic);
2940
- } catch {
2941
- return null;
2942
- }
2943
- }
2944
3596
  throwIfNonSignableAuthorizationState(label, raw) {
2945
3597
  if (raw.status === "pending_signature") return;
2946
- this.throwPaymentStateError(label, raw);
2947
- }
2948
- throwPaymentStateError(label, raw) {
2949
- const statusCode = PAYMENT_STATE_STATUS_CODES[raw.status] ?? 502;
2950
- const state = this.paymentStateFromRaw(label, raw);
2951
- if (state) {
2952
- throw new HavenPaymentStateError(state.message, statusCode, state, raw);
2953
- }
2954
- if (raw.status === "pending_approval") {
2955
- throw new HavenApiError(
2956
- `${label} exceeds the on-chain allowance and was queued for owner approval (payment_id: ${raw.payment_id}).`,
2957
- statusCode,
2958
- raw
2959
- );
2960
- }
2961
- if (raw.status === "expired") {
2962
- throw new HavenApiError(
2963
- `${label} expired before it could be completed (payment_id: ${raw.payment_id}).`,
2964
- statusCode,
2965
- raw
2966
- );
2967
- }
2968
- const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
2969
- const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
2970
- throw new HavenApiError(message, statusCode, raw);
2971
- }
2972
- paymentStateFromRaw(label, raw) {
2973
- if (!raw.payment_id || !raw.status) return null;
2974
- const phase = raw.phase ?? phaseForStatus(raw.status);
2975
- const nextAction = raw.next_action ?? nextActionForStatus(raw.status);
2976
- if (!phase || !nextAction) return null;
2977
- const amount = raw.amount ?? raw.requested ?? "";
2978
- const token = raw.token ?? "";
2979
- const message = raw.message ?? raw.error ?? messageForState(label, raw.status, raw.payment_id, nextAction);
2980
- return {
2981
- paymentId: raw.payment_id,
2982
- kind: raw.kind === "payment_intent" ? "payment_intent" : "approval_request",
2983
- rail: raw.rail ?? "direct",
2984
- status: raw.status === "pending" ? "pending_approval" : raw.status,
2985
- phase,
2986
- nextAction,
2987
- amount,
2988
- token,
2989
- resourceUrl: raw.resource_url ?? null,
2990
- merchantAddress: raw.merchant_address ?? raw.merchant_to ?? null,
2991
- txHash: raw.tx_hash ?? null,
2992
- expiresAt: raw.expires_at ?? "",
2993
- chainId: raw.chain_id ?? 0,
2994
- message,
2995
- amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? raw.mpp?.amount_atomic ?? null,
2996
- asset: raw.asset ?? raw.x402?.asset ?? raw.mpp?.asset ?? null,
2997
- network: raw.network ?? raw.x402?.network ?? raw.mpp?.network ?? null,
2998
- description: raw.description ?? raw.x402?.description ?? raw.mpp?.description ?? null,
2999
- idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? raw.mpp?.idempotency_key ?? null,
3000
- x402: raw.x402 ? {
3001
- amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
3002
- asset: raw.x402.asset ?? raw.asset ?? null,
3003
- network: raw.x402.network ?? raw.network ?? null,
3004
- resourceUrl: raw.x402.resource_url ?? raw.resource_url ?? null,
3005
- merchantAddress: raw.x402.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
3006
- description: raw.x402.description ?? raw.description ?? null,
3007
- idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
3008
- } : void 0,
3009
- mpp: raw.mpp ? {
3010
- amountAtomic: raw.mpp.amount_atomic ?? raw.amount_atomic ?? null,
3011
- asset: raw.mpp.asset ?? raw.asset ?? null,
3012
- network: raw.mpp.network ?? raw.network ?? null,
3013
- resourceUrl: raw.mpp.resource_url ?? raw.resource_url ?? null,
3014
- merchantAddress: raw.mpp.merchant_address ?? raw.merchant_address ?? raw.merchant_to ?? null,
3015
- description: raw.mpp.description ?? raw.description ?? null,
3016
- idempotencyKey: raw.mpp.idempotency_key ?? raw.idempotency_key ?? null,
3017
- challengeId: raw.mpp.challenge_id ?? raw.challenge_id ?? null
3018
- } : void 0
3019
- };
3020
- }
3021
- x402PayerAddress() {
3022
- return this.delegateAddress ?? this.x402Wallet;
3023
- }
3024
- snapshotX402Request(url, init) {
3025
- return {
3026
- url,
3027
- method: init?.method ?? "GET",
3028
- headers: Array.from(new Headers(init?.headers).entries()),
3029
- body: this.snapshotRequestBody(init?.body)
3030
- };
3031
- }
3032
- snapshotRequestBody(body) {
3033
- if (body == null) return void 0;
3034
- if (typeof body === "string") return body;
3035
- if (body instanceof URLSearchParams) return body.toString();
3036
- throw new HavenApiError(
3037
- "Quote helpers can only capture resumable request bodies that are strings or URLSearchParams. For streams, blobs, or binary bodies, preserve the original request yourself and call the matching resume method with fresh init.",
3038
- 400
3039
- );
3040
- }
3041
- requestInitFromSnapshot(request) {
3042
- return {
3043
- method: request.method,
3044
- headers: request.headers,
3045
- body: request.body
3046
- };
3047
- }
3048
- withX402Wallet(init, wallet = this.x402PayerAddress()) {
3049
- if (!wallet) return init;
3050
- const headers = new Headers(init?.headers);
3051
- if (!headers.has("x402-wallet")) {
3052
- headers.set("x402-wallet", wallet);
3053
- }
3054
- return {
3055
- ...init,
3056
- headers
3057
- };
3058
- }
3059
- buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
3060
- const option = selectStandardPaymentOption(paymentRequired.accepts);
3061
- if (!option) {
3062
- throw new HavenApiError(
3063
- "No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
3064
- 400
3065
- );
3066
- }
3067
- const token = resolveTokenFromAddress(option.asset, option.network);
3068
- return {
3069
- rail: "x402",
3070
- idempotencyKey: idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option),
3071
- paymentRequired,
3072
- accepted: option,
3073
- request,
3074
- ...mcpTransport ? { mcpTransport } : {},
3075
- resourceUrl: paymentRequired.resource.url,
3076
- description: paymentRequired.resource.description ?? option.description ?? null,
3077
- mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
3078
- amountAtomic: x402AuthorizationAmount(option),
3079
- amount: decimalFromUsdcAtomic(x402AuthorizationAmount(option)),
3080
- token: token?.symbol ?? "USDC",
3081
- // #1351: null when the asset is unrecognised on this network — the
3082
- // `token` fallback above is a LABEL, not evidence of 6 decimals, and a
3083
- // human-denominated cap must fail closed rather than convert against a
3084
- // guess. Same resolution as `token`, so the two never disagree.
3085
- decimals: token?.decimals ?? null,
3086
- asset: option.asset,
3087
- network: option.network,
3088
- chainId: chainIdOrNull(option.network),
3089
- merchantAddress: option.payTo,
3090
- maxTimeoutSeconds: option.maxTimeoutSeconds
3091
- };
3092
- }
3093
- async detectX402McpTransport(url, paymentRequired, response) {
3094
- if (isMcpUrl(url)) {
3095
- return { handshakeRequired: true, source: "path" };
3096
- }
3097
- if (paymentRequired.extensions?.bazaar != null) {
3098
- return { handshakeRequired: true, source: "bazaar" };
3099
- }
3100
- if (await responseHasBazaarExtension(response)) {
3101
- return { handshakeRequired: true, source: "bazaar" };
3102
- }
3103
- return void 0;
3104
- }
3105
- buildX402ResumeState(input) {
3106
- const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
3107
- return {
3108
- rail: "x402",
3109
- paymentId: input.paymentId,
3110
- idempotencyKey: input.idempotencyKey,
3111
- paymentRequired: input.paymentRequired,
3112
- accepted: input.accepted,
3113
- url: input.request?.url ?? input.paymentRequired.resource.url,
3114
- request: input.request,
3115
- resourceUrl: input.paymentRequired.resource.url,
3116
- description: input.paymentRequired.resource.description ?? input.accepted.description ?? null,
3117
- amountAtomic: x402AuthorizationAmount(input.accepted),
3118
- amount: decimalFromUsdcAtomic(x402AuthorizationAmount(input.accepted)),
3119
- token: token?.symbol ?? "USDC",
3120
- asset: input.accepted.asset,
3121
- network: input.accepted.network,
3122
- chainId: chainIdOrNull(input.accepted.network),
3123
- merchantAddress: input.accepted.payTo
3124
- };
3125
- }
3126
- // #1328: attachResumeState's 'mpp' branch (buildMppQuote / buildMppResumeState
3127
- // / attachMppResumeState) is retired along with the rest of the MPP-demo
3128
- // client surface — every remaining caller passes rail: 'x402' only, so this
3129
- // is now a direct alias for attachX402ResumeState rather than a dispatcher.
3130
- attachResumeState(err, input) {
3131
- this.attachX402ResumeState(
3132
- err,
3133
- input.paymentRequired,
3134
- input.accepted,
3135
- input.idempotencyKey,
3136
- input.request
3137
- );
3138
- }
3139
- attachX402ResumeState(err, paymentRequired, accepted, idempotencyKey, request) {
3140
- if (!(err instanceof HavenPaymentStateError)) return;
3141
- if (err.state.rail !== "x402") return;
3142
- err.resumeState = this.buildX402ResumeState({
3143
- paymentId: err.state.paymentId,
3144
- paymentRequired,
3145
- accepted,
3146
- idempotencyKey,
3147
- request
3148
- });
3598
+ throwPaymentStateError(label, raw);
3149
3599
  }
3150
3600
  // ── Tool Execution (for agent frameworks) ────────────────────────
3151
3601
  /**
@@ -3177,19 +3627,19 @@ var HavenClient = class {
3177
3627
  error: result.errorMessage
3178
3628
  };
3179
3629
  } catch (err) {
3180
- return this.toolError(err);
3630
+ return toolError(err);
3181
3631
  }
3182
3632
  }
3183
3633
  if (toolName === "authorize_x402_payment") {
3184
3634
  const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
3185
3635
  try {
3186
3636
  const receipt = await this.authorizeX402(
3187
- this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3637
+ toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3188
3638
  { idempotencyKey }
3189
3639
  );
3190
- return this.x402ToolReceipt(receipt);
3640
+ return x402ToolReceipt(receipt);
3191
3641
  } catch (err) {
3192
- return this.toolError(err);
3642
+ return toolError(err);
3193
3643
  }
3194
3644
  }
3195
3645
  if (toolName === "resume_x402_payment") {
@@ -3197,12 +3647,12 @@ var HavenClient = class {
3197
3647
  try {
3198
3648
  const receipt = await this.resumeAuthorizedX402({
3199
3649
  paymentId: payment_id,
3200
- paymentRequired: this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3650
+ paymentRequired: toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
3201
3651
  idempotencyKey
3202
3652
  });
3203
- return this.x402ToolReceipt(receipt);
3653
+ return x402ToolReceipt(receipt);
3204
3654
  } catch (err) {
3205
- return this.toolError(err);
3655
+ return toolError(err);
3206
3656
  }
3207
3657
  }
3208
3658
  if (toolName === "get_payment_status") {
@@ -3237,302 +3687,17 @@ var HavenClient = class {
3237
3687
  }
3238
3688
  throw new Error(`Unknown tool: ${toolName}`);
3239
3689
  }
3240
- toolX402PaymentRequired(input) {
3241
- return {
3242
- x402Version: 2,
3243
- resource: { url: input.url, description: input.description },
3244
- accepts: [
3245
- {
3246
- scheme: "exact",
3247
- network: input.network,
3248
- amount: input.amount,
3249
- asset: input.asset,
3250
- payTo: input.payTo,
3251
- maxTimeoutSeconds: 30
3252
- }
3253
- ]
3254
- };
3255
- }
3256
- x402ToolReceipt(receipt) {
3257
- return {
3258
- success: true,
3259
- payment_id: receipt.paymentId,
3260
- tx_hash: receipt.txHash,
3261
- token: receipt.token,
3262
- amount: receipt.amount,
3263
- to: receipt.to,
3264
- resource_url: receipt.resourceUrl,
3265
- explorer_url: receipt.explorerUrl,
3266
- payment_header: receipt.paymentHeader,
3267
- merchant_to: receipt.merchantTo,
3268
- payer: receipt.payer,
3269
- chain_id: receipt.chainId,
3270
- haven: receipt.haven,
3271
- merchant: receipt.merchant,
3272
- x402: receipt.x402
3273
- };
3274
- }
3275
- toolError(err) {
3276
- if (err instanceof HavenPaymentStateError) {
3277
- return {
3278
- success: false,
3279
- payment_id: err.state.paymentId,
3280
- kind: err.state.kind,
3281
- rail: err.state.rail,
3282
- status: err.state.status,
3283
- phase: err.state.phase,
3284
- next_action: err.state.nextAction,
3285
- tx_hash: err.state.txHash,
3286
- token: err.state.token,
3287
- amount: err.state.amount,
3288
- resource_url: err.state.resourceUrl,
3289
- merchant_address: err.state.merchantAddress,
3290
- amount_atomic: err.state.amountAtomic,
3291
- asset: err.state.asset,
3292
- network: err.state.network,
3293
- description: err.state.description,
3294
- idempotency_key: err.state.idempotencyKey,
3295
- x402: err.state.x402 ? {
3296
- amount_atomic: err.state.x402.amountAtomic,
3297
- asset: err.state.x402.asset,
3298
- network: err.state.x402.network,
3299
- resource_url: err.state.x402.resourceUrl,
3300
- merchant_address: err.state.x402.merchantAddress,
3301
- description: err.state.x402.description,
3302
- idempotency_key: err.state.x402.idempotencyKey
3303
- } : void 0,
3304
- mpp: err.state.mpp ? {
3305
- amount_atomic: err.state.mpp.amountAtomic,
3306
- asset: err.state.mpp.asset,
3307
- network: err.state.mpp.network,
3308
- resource_url: err.state.mpp.resourceUrl,
3309
- merchant_address: err.state.mpp.merchantAddress,
3310
- description: err.state.mpp.description,
3311
- idempotency_key: err.state.mpp.idempotencyKey,
3312
- challenge_id: err.state.mpp.challengeId
3313
- } : void 0,
3314
- resume_state: err.resumeState,
3315
- expires_at: err.state.expiresAt,
3316
- chain_id: err.state.chainId,
3317
- message: err.state.message,
3318
- error: err.message
3319
- };
3320
- }
3321
- if (err instanceof HavenApiError) {
3322
- return {
3323
- success: false,
3324
- status_code: err.statusCode,
3325
- error: err.message,
3326
- body: err.body
3327
- };
3328
- }
3329
- return {
3330
- success: false,
3331
- error: err instanceof Error ? err.message : String(err)
3332
- };
3333
- }
3334
3690
  // ── HTTP Helpers ─────────────────────────────────────────────────
3335
3691
  async post(path, body) {
3336
- return this.request("POST", path, body);
3692
+ return this.havenApi.post(path, body);
3337
3693
  }
3338
3694
  async get(path) {
3339
- return this.request("GET", path);
3340
- }
3341
- /**
3342
- * #1300: every MERCHANT-facing fetch goes through here. Haven API calls
3343
- * have always been bounded (request() below); the merchant probes/retries
3344
- * called globalThis.fetch bare, so a slow-loris merchant could hold a tool
3345
- * call open forever. A caller-supplied signal still applies (combined via
3346
- * AbortSignal.any); a timeout abort surfaces as a clear HavenApiError 504
3347
- * naming the URL rather than a bare AbortError.
3348
- */
3349
- async merchantFetch(url, init = {}, timeoutMs = this.merchantTimeout) {
3350
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
3351
- const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
3352
- try {
3353
- return await globalThis.fetch(url, { ...init, signal });
3354
- } catch (err) {
3355
- if (timeoutSignal.aborted) {
3356
- throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
3357
- }
3358
- throw err;
3359
- }
3360
- }
3361
- async request(method, path, body) {
3362
- const url = `${this.baseUrl}${path}`;
3363
- const controller = new AbortController();
3364
- const timeout = setTimeout(() => controller.abort(), this.requestTimeout);
3365
- try {
3366
- const contextHeaders = this.requestContext.getStore()?.headers ?? {};
3367
- const res = await fetch(url, {
3368
- method,
3369
- headers: {
3370
- "Content-Type": "application/json",
3371
- "Authorization": `Bearer ${this.apiKey}`,
3372
- ...this.defaultHeaders,
3373
- ...contextHeaders
3374
- },
3375
- body: body ? JSON.stringify(body) : void 0,
3376
- signal: controller.signal
3377
- });
3378
- const data = await res.json();
3379
- if (!res.ok) {
3380
- const record = data;
3381
- const errorText = typeof record.error === "string" ? record.error : void 0;
3382
- const rawDetails = record.details ?? record.detail;
3383
- const detailsText = typeof rawDetails === "string" ? rawDetails : rawDetails != null ? JSON.stringify(rawDetails) : void 0;
3384
- const message = errorText && detailsText ? `${errorText}: ${detailsText}` : errorText ?? detailsText ?? `API request failed`;
3385
- throw new HavenApiError(message, res.status, data);
3386
- }
3387
- return data;
3388
- } catch (err) {
3389
- if (err instanceof HavenApiError) throw err;
3390
- if (err instanceof Error && err.name === "AbortError") {
3391
- throw new HavenApiError(`Request to ${path} timed out`, 408);
3392
- }
3393
- throw new HavenApiError(
3394
- `Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
3395
- 0
3396
- );
3397
- } finally {
3398
- clearTimeout(timeout);
3399
- }
3400
- }
3401
- // ── Mapping Helpers ──────────────────────────────────────────────
3402
- mapPaymentResult(raw) {
3403
- return {
3404
- paymentId: raw.payment_id,
3405
- status: raw.status,
3406
- token: raw.token,
3407
- amount: raw.amount,
3408
- to: raw.to,
3409
- txHash: raw.tx_hash,
3410
- errorMessage: raw.error_message,
3411
- explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : null),
3412
- fee: raw.fee ? {
3413
- amount: raw.fee.amount,
3414
- token: raw.fee.token,
3415
- basisPoints: raw.fee.basis_points,
3416
- applied: raw.fee.applied
3417
- } : null,
3418
- createdAt: raw.created_at,
3419
- signedAt: raw.signed_at,
3420
- submittedAt: raw.submitted_at,
3421
- confirmedAt: raw.confirmed_at,
3422
- expiresAt: raw.expires_at
3423
- };
3424
- }
3425
- mapPaymentStatusResult(raw) {
3426
- return {
3427
- paymentId: raw.payment_id,
3428
- kind: raw.kind,
3429
- rail: raw.rail,
3430
- status: raw.status,
3431
- phase: raw.phase,
3432
- nextAction: raw.next_action,
3433
- amount: raw.amount,
3434
- token: raw.token,
3435
- resourceUrl: raw.resource_url,
3436
- merchantAddress: raw.merchant_address,
3437
- payerAddress: raw.payer_address ?? null,
3438
- txHash: raw.tx_hash,
3439
- expiresAt: raw.expires_at,
3440
- chainId: raw.chain_id,
3441
- message: raw.message,
3442
- fee: raw.fee ? {
3443
- amount: raw.fee.amount,
3444
- token: raw.fee.token,
3445
- basisPoints: raw.fee.basis_points,
3446
- applied: raw.fee.applied
3447
- } : null,
3448
- amountAtomic: raw.amount_atomic ?? raw.x402?.amount_atomic ?? null,
3449
- asset: raw.asset ?? raw.x402?.asset ?? null,
3450
- network: raw.network ?? raw.x402?.network ?? null,
3451
- description: raw.description ?? raw.x402?.description ?? null,
3452
- idempotencyKey: raw.idempotency_key ?? raw.x402?.idempotency_key ?? null,
3453
- x402: raw.x402 ? {
3454
- amountAtomic: raw.x402.amount_atomic ?? raw.amount_atomic ?? null,
3455
- asset: raw.x402.asset ?? raw.asset ?? null,
3456
- network: raw.x402.network ?? raw.network ?? null,
3457
- resourceUrl: raw.x402.resource_url ?? raw.resource_url,
3458
- merchantAddress: raw.x402.merchant_address ?? raw.merchant_address,
3459
- description: raw.x402.description ?? raw.description ?? null,
3460
- idempotencyKey: raw.x402.idempotency_key ?? raw.idempotency_key ?? null
3461
- } : void 0
3462
- };
3463
- }
3464
- mapPaymentReceipt(raw) {
3465
- const receipt = {
3466
- id: raw.id,
3467
- paymentId: raw.payment_id,
3468
- rail: raw.rail,
3469
- proofStatus: raw.proof_status,
3470
- txHash: raw.tx_hash,
3471
- chainId: raw.chain_id,
3472
- resourceUrl: raw.resource_url,
3473
- merchantAddress: raw.merchant_address,
3474
- payerAddress: raw.payer_address,
3475
- settlementAddress: raw.settlement_address,
3476
- tokenSymbol: raw.token_symbol,
3477
- tokenAddress: raw.token_address,
3478
- amountRaw: raw.amount_raw,
3479
- amount: raw.amount_human,
3480
- challengeId: raw.challenge_id,
3481
- idempotencyKey: raw.idempotency_key,
3482
- challengePayload: raw.challenge_payload,
3483
- selectedPayment: raw.selected_payment,
3484
- paymentProofHeaderName: raw.payment_proof_header_name,
3485
- protocolReceiptHeaderName: raw.protocol_receipt_header_name,
3486
- protocolReceiptPayload: raw.protocol_receipt_payload,
3487
- merchantStatus: raw.merchant_status,
3488
- confirmedAt: raw.confirmed_at,
3489
- createdAt: raw.created_at,
3490
- updatedAt: raw.updated_at
3491
- };
3492
- if ("payment_intent_id" in raw) {
3493
- receipt.paymentIntentId = raw.payment_intent_id ?? null;
3494
- }
3495
- if ("approval_request_id" in raw) {
3496
- receipt.approvalRequestId = raw.approval_request_id ?? null;
3497
- }
3498
- return receipt;
3695
+ return this.havenApi.get(path);
3499
3696
  }
3500
3697
  };
3501
3698
  function sleep(ms) {
3502
3699
  return new Promise((resolve) => setTimeout(resolve, ms));
3503
3700
  }
3504
- function getPaymentHeaderValidBefore(paymentHeader) {
3505
- try {
3506
- const payment = decodeBase64Json(
3507
- paymentHeader
3508
- );
3509
- const payload = payment.payload;
3510
- const validBeforeSeconds = Number(payload.authorization?.validBefore);
3511
- if (Number.isFinite(validBeforeSeconds)) return validBeforeSeconds * 1e3;
3512
- } catch {
3513
- }
3514
- return 0;
3515
- }
3516
- function parseProtocolReceiptHeader(value) {
3517
- try {
3518
- return decodeBase64Json(value);
3519
- } catch {
3520
- try {
3521
- return JSON.parse(value);
3522
- } catch {
3523
- return void 0;
3524
- }
3525
- }
3526
- }
3527
- async function captureMerchantResponse(response) {
3528
- const merchant_body = await response.text().catch(() => "");
3529
- return {
3530
- merchant_status: response.status,
3531
- merchant_status_text: response.statusText,
3532
- merchant_headers: Object.fromEntries(response.headers.entries()),
3533
- merchant_body
3534
- };
3535
- }
3536
3701
 
3537
3702
  // src/tool-descriptions.ts
3538
3703
  function composeDescription(d) {
@@ -3576,9 +3741,9 @@ var toolDescriptions = {
3576
3741
  nextActionGuidance: ""
3577
3742
  },
3578
3743
  getAgent: {
3579
- summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, a readiness signal, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether you can pay right now.",
3744
+ summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, and per-token remaining allowance (atomic + human-readable). The recommended first call in a new session to confirm who you are and whether Haven will let you spend right now.",
3580
3745
  selectionGuidance: "Use this as the one-shot orientation/bootstrap at the start of a session, or whenever you need to confirm identity together with whether the agent can spend right now. For a detailed per-token breakdown (configured vs spent vs reset window) use haven_get_allowances.",
3581
- behavior: 'Reads identity plus the live spend-authority snapshot in one shot \u2014 the on-chain AllowanceModule on the legacy rail, the active budget delegation on the delegation rail. readiness is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. What an over-budget payment does differs by rail: on the legacy AllowanceModule rail it is queued for the wallet owner to approve in Haven; on the delegation rail there is no approval queue \u2014 an over-budget redemption reverts on-chain, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.',
3746
+ behavior: 'Reads identity plus the live spend-authority snapshot in one shot \u2014 the on-chain AllowanceModule on the legacy rail, the active budget delegation on the delegation rail. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. It covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify the signer with a signer tool call or connect --doctor. What an over-budget payment does differs by rail: on the legacy AllowanceModule rail it is queued for the wallet owner to approve in Haven; on the delegation rail there is no approval queue \u2014 an over-budget redemption reverts on-chain, so ask the owner to grant or raise the budget in Haven rather than waiting for an approval. allowances[] carries remainingAtomic and remainingDisplay per token. Identity fields (id, name, status, safeAddress, delegateAddress, chainId) are unchanged from before.',
3582
3747
  nextActionGuidance: ""
3583
3748
  },
3584
3749
  getAllowances: {
@@ -3609,7 +3774,7 @@ var toolDescriptions = {
3609
3774
  summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
3610
3775
  selectionGuidance: "Use this when the user asks what the agent can buy, pay for, or which paid services exist \u2014 or when you need a resource URL for a service the user described. Do NOT use for balance, budget, or spend-limit questions \u2014 use haven_get_allowances. Do NOT use to pay \u2014 each returned entry names the pay tool to use next.",
3611
3776
  behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, and suggested_tool. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
3612
- nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand (#1351).`
3777
+ nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand.`
3613
3778
  },
3614
3779
  sweep_delegate: {
3615
3780
  summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
@@ -3854,9 +4019,13 @@ user's approval in Haven.
3854
4019
 
3855
4020
  Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
3856
4021
  the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
3857
- Tool results carry the exact next step (\`next_action\`, \`next_tool\`,
3858
- \`next_arguments\`) \u2014 follow those fields first; the prose below is fallback
3859
- and orientation, not the source of truth.
4022
+ That namespacing is Claude-family; other runtimes name the servers by their
4023
+ own config keys (Codex: \`haven\`, \`haven_signer\`). Tool results carry the
4024
+ exact next step (\`next_action\`, \`next_tool\`, \`next_arguments\`, plus the
4025
+ runtime-neutral \`next_tool_server\` + \`next_tool_name\` \u2014 the bare tool name
4026
+ on that logical server, whatever your runtime calls it).
4027
+ Follow those fields first; the prose below is fallback and orientation, not
4028
+ the source of truth.
3860
4029
 
3861
4030
  ## When to use this skill
3862
4031
 
@@ -3882,8 +4051,10 @@ Before any payment, confirm the *live remaining* budget with the tools \u2014
3882
4051
  spending:
3883
4052
 
3884
4053
  - \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
3885
- (wallet, network) plus a readiness signal (\`ready\` / \`needs_approval\` /
3886
- \`revoked\`) and live remaining per-token allowance, in one shot.
4054
+ (wallet, network) plus \`spend_authority_readiness\` (\`ready\` / \`needs_approval\` /
4055
+ \`revoked\`) and live remaining per-token allowance, in one shot. That signal
4056
+ covers hosted identity and on-chain spend authority only \u2014 it cannot see the
4057
+ local signer; the signer is verified by calling any signer tool.
3887
4058
  - \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
3888
4059
  (configured, spent, reset window) when you need more than the summary.
3889
4060