@haven_ai/sdk 0.1.9 → 0.1.11-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 +477 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -3
- package/dist/index.d.ts +204 -3
- package/dist/index.js +474 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -37,7 +37,14 @@ var AgentPaymentPhase = {
|
|
|
37
37
|
* is no approval that would fix this — the originating Safe needs more
|
|
38
38
|
* funds or the agent's per-token allowance needs to be raised first.
|
|
39
39
|
*/
|
|
40
|
-
InsufficientFunds: "insufficient_funds"
|
|
40
|
+
InsufficientFunds: "insufficient_funds",
|
|
41
|
+
/**
|
|
42
|
+
* Haven's funding leg (Safe → delegate) confirmed on-chain, but the
|
|
43
|
+
* merchant rejected the x402 retry. The delegate wallet may hold stranded
|
|
44
|
+
* USDC that was never settled to the merchant. The agent should stop, tell
|
|
45
|
+
* the user, and wait for the sweep flow to reclaim the funds.
|
|
46
|
+
*/
|
|
47
|
+
FundedButUnsettled: "funded_but_unsettled"
|
|
41
48
|
};
|
|
42
49
|
var AgentPaymentNextAction = {
|
|
43
50
|
/** Sign with the delegate key and submit the payment to Haven. */
|
|
@@ -61,7 +68,13 @@ var AgentPaymentNextAction = {
|
|
|
61
68
|
* the agent's per-token allowance needs to be raised before the payment
|
|
62
69
|
* can succeed. A user approval will not fix this state on its own.
|
|
63
70
|
*/
|
|
64
|
-
FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance"
|
|
71
|
+
FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance",
|
|
72
|
+
/**
|
|
73
|
+
* The delegate wallet may hold funds that were sent from the Safe but never
|
|
74
|
+
* settled to the merchant. The wallet owner should initiate a sweep to
|
|
75
|
+
* return those funds to the originating Safe.
|
|
76
|
+
*/
|
|
77
|
+
SweepStrandedFunds: "sweep_stranded_funds"
|
|
65
78
|
};
|
|
66
79
|
var AgentPaymentRail = {
|
|
67
80
|
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
@@ -93,7 +106,8 @@ var AgentPaymentPhaseDescriptions = {
|
|
|
93
106
|
[AgentPaymentPhase.Rejected]: "The wallet owner rejected the request; the agent should stop and tell the user.",
|
|
94
107
|
[AgentPaymentPhase.Expired]: "The payment or approval request expired before completion.",
|
|
95
108
|
[AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure.",
|
|
96
|
-
[AgentPaymentPhase.InsufficientFunds]: "Pre-flight check determined the delegate balance plus the remaining on-chain allowance cannot cover the requested amount, so no payment was created. The originating Safe must be funded or the agent allowance raised before retrying."
|
|
109
|
+
[AgentPaymentPhase.InsufficientFunds]: "Pre-flight check determined the delegate balance plus the remaining on-chain allowance cannot cover the requested amount, so no payment was created. The originating Safe must be funded or the agent allowance raised before retrying.",
|
|
110
|
+
[AgentPaymentPhase.FundedButUnsettled]: "Haven's funding leg confirmed on-chain but the merchant rejected the x402 retry. The delegate wallet may hold stranded funds. The agent should stop and wait for the wallet owner to sweep the stranded funds back to the Safe."
|
|
97
111
|
};
|
|
98
112
|
var AgentPaymentNextActionDescriptions = {
|
|
99
113
|
[AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
|
|
@@ -104,7 +118,8 @@ var AgentPaymentNextActionDescriptions = {
|
|
|
104
118
|
[AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
|
|
105
119
|
[AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
|
|
106
120
|
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
|
|
107
|
-
[AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the originating Safe needs to be funded or the agent allowance raised before the payment can succeed."
|
|
121
|
+
[AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the originating Safe needs to be funded or the agent allowance raised before the payment can succeed.",
|
|
122
|
+
[AgentPaymentNextAction.SweepStrandedFunds]: "Tell the user that funds may be stranded in the delegate wallet and prompt them to initiate a sweep in Haven to return them to the originating Safe."
|
|
108
123
|
};
|
|
109
124
|
var AgentPaymentRailDescriptions = {
|
|
110
125
|
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
|
|
@@ -218,16 +233,52 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
218
233
|
return false;
|
|
219
234
|
}
|
|
220
235
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
|
|
237
|
+
// src/base64.ts
|
|
238
|
+
function normalizeBase64(value) {
|
|
239
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
240
|
+
const remainder = standard.length % 4;
|
|
241
|
+
return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
|
|
242
|
+
}
|
|
243
|
+
function encodeBase64Utf8(value) {
|
|
244
|
+
if (typeof Buffer !== "undefined") {
|
|
245
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
246
|
+
}
|
|
247
|
+
const bytes = new TextEncoder().encode(value);
|
|
248
|
+
let binary = "";
|
|
249
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
250
|
+
binary += String.fromCharCode(bytes[i]);
|
|
251
|
+
}
|
|
252
|
+
return btoa(binary);
|
|
253
|
+
}
|
|
254
|
+
function decodeBase64Utf8(value) {
|
|
255
|
+
const normalized = normalizeBase64(value);
|
|
256
|
+
if (typeof Buffer !== "undefined") {
|
|
257
|
+
return Buffer.from(normalized, "base64").toString("utf8");
|
|
258
|
+
}
|
|
259
|
+
const binary = atob(normalized);
|
|
260
|
+
const bytes = new Uint8Array(binary.length);
|
|
261
|
+
for (let i = 0; i < binary.length; i++) {
|
|
262
|
+
bytes[i] = binary.charCodeAt(i);
|
|
263
|
+
}
|
|
264
|
+
return new TextDecoder().decode(bytes);
|
|
265
|
+
}
|
|
266
|
+
function encodeBase64Json(value) {
|
|
267
|
+
return encodeBase64Utf8(JSON.stringify(value));
|
|
268
|
+
}
|
|
224
269
|
function decodeBase64Json(value, label) {
|
|
225
270
|
try {
|
|
226
|
-
return JSON.parse(
|
|
227
|
-
} catch {
|
|
228
|
-
throw new Error(`Failed to decode ${label}`);
|
|
271
|
+
return JSON.parse(decodeBase64Utf8(value));
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (label) throw new Error(`Failed to decode ${label}`);
|
|
274
|
+
throw err;
|
|
229
275
|
}
|
|
230
276
|
}
|
|
277
|
+
|
|
278
|
+
// src/x402.ts
|
|
279
|
+
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
280
|
+
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
281
|
+
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
231
282
|
function isPositiveDecimalAtomicAmount(value) {
|
|
232
283
|
return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
|
|
233
284
|
}
|
|
@@ -435,7 +486,7 @@ function encodePaymentProof(receipt) {
|
|
|
435
486
|
chainId: receipt.chainId
|
|
436
487
|
}
|
|
437
488
|
};
|
|
438
|
-
return
|
|
489
|
+
return encodeBase64Json(payload);
|
|
439
490
|
}
|
|
440
491
|
function resolveTokenFromAddress(address, network) {
|
|
441
492
|
const lower = address.toLowerCase();
|
|
@@ -453,13 +504,6 @@ function stableStringify(value) {
|
|
|
453
504
|
const object = value;
|
|
454
505
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
455
506
|
}
|
|
456
|
-
function decodeBase64Json2(value, label) {
|
|
457
|
-
try {
|
|
458
|
-
return JSON.parse(atob(value));
|
|
459
|
-
} catch {
|
|
460
|
-
throw new Error(`Failed to decode ${label}`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
507
|
function normalizeChallenge(value) {
|
|
464
508
|
const candidate = value;
|
|
465
509
|
if (!candidate || typeof candidate !== "object" || candidate.rail !== "mpp_demo" || typeof candidate.version !== "string" || typeof candidate.challengeId !== "string" || typeof candidate.resource !== "string" || typeof candidate.description !== "string" || // TODO: relax these checks when non-demo machine payment rails are added.
|
|
@@ -486,7 +530,7 @@ function parseMachinePaymentChallenge(response) {
|
|
|
486
530
|
throw new Error("No MACHINE-PAYMENT-CHALLENGE header found in 402 response.");
|
|
487
531
|
}
|
|
488
532
|
const parsed = normalizeChallenge(
|
|
489
|
-
|
|
533
|
+
decodeBase64Json(header, "MACHINE-PAYMENT-CHALLENGE header")
|
|
490
534
|
);
|
|
491
535
|
if (!parsed) throw new Error("Invalid machine payment challenge");
|
|
492
536
|
return parsed;
|
|
@@ -517,7 +561,7 @@ function buildMachinePaymentIdempotencyKey(challenge) {
|
|
|
517
561
|
return `${challenge.rail}:${crypto.createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
518
562
|
}
|
|
519
563
|
function encodeMachinePaymentProof(receipt) {
|
|
520
|
-
return
|
|
564
|
+
return encodeBase64Json({
|
|
521
565
|
rail: receipt.rail,
|
|
522
566
|
challengeId: receipt.challengeId,
|
|
523
567
|
paymentId: receipt.paymentId,
|
|
@@ -525,7 +569,16 @@ function encodeMachinePaymentProof(receipt) {
|
|
|
525
569
|
settledVia: "haven",
|
|
526
570
|
payer: receipt.payer,
|
|
527
571
|
chainId: receipt.chainId
|
|
528
|
-
})
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function createJsonRpcProvider(url) {
|
|
575
|
+
return new ethers.ethers.JsonRpcProvider(url);
|
|
576
|
+
}
|
|
577
|
+
function createWallet(privateKey, provider) {
|
|
578
|
+
return new ethers.ethers.Wallet(privateKey, provider);
|
|
579
|
+
}
|
|
580
|
+
function createErc20Contract(address, abi, signer) {
|
|
581
|
+
return new ethers.ethers.Contract(address, abi, signer);
|
|
529
582
|
}
|
|
530
583
|
|
|
531
584
|
// src/client.ts
|
|
@@ -534,6 +587,9 @@ var CHAIN_EXPLORER_TX = {
|
|
|
534
587
|
100: "https://gnosisscan.io/tx",
|
|
535
588
|
8453: "https://basescan.org/tx"
|
|
536
589
|
};
|
|
590
|
+
var CHAIN_USDC = {
|
|
591
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
592
|
+
};
|
|
537
593
|
function buildExplorerUrl(chainId, txHash) {
|
|
538
594
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
539
595
|
return `${base}/${txHash}`;
|
|
@@ -544,6 +600,16 @@ function explorerUrlOrEmpty(chainId, txHash) {
|
|
|
544
600
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
545
601
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
546
602
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
603
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
604
|
+
const s = atomic.toString().padStart(decimals + 1, "0");
|
|
605
|
+
const intPart = s.slice(0, s.length - decimals) || "0";
|
|
606
|
+
const fracPart = s.slice(s.length - decimals).replace(/0+$/, "") || "0";
|
|
607
|
+
return `${intPart}.${fracPart}`;
|
|
608
|
+
}
|
|
609
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
610
|
+
var MCP_ACCEPT = "application/json, text/event-stream";
|
|
611
|
+
var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
|
|
612
|
+
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
547
613
|
var PAYMENT_STATE_STATUS_CODES = {
|
|
548
614
|
pending: 202,
|
|
549
615
|
pending_approval: 202,
|
|
@@ -631,6 +697,47 @@ function parseMerchantSettlement(header) {
|
|
|
631
697
|
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
632
698
|
return { settlementTxHash: tx };
|
|
633
699
|
}
|
|
700
|
+
function isMcpUrl(url) {
|
|
701
|
+
try {
|
|
702
|
+
return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
|
|
703
|
+
} catch {
|
|
704
|
+
return /\/mcp(?:[/?#]|$)/.test(url);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function responseHasBazaarExtension(response) {
|
|
708
|
+
try {
|
|
709
|
+
const body = await response.clone().json();
|
|
710
|
+
return body?.extensions?.bazaar != null;
|
|
711
|
+
} catch {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function parseSseJsonRpcMessages(text) {
|
|
716
|
+
const messages = [];
|
|
717
|
+
let dataLines = [];
|
|
718
|
+
const flush = () => {
|
|
719
|
+
if (dataLines.length === 0) return;
|
|
720
|
+
try {
|
|
721
|
+
messages.push(JSON.parse(dataLines.join("\n")));
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
dataLines = [];
|
|
725
|
+
};
|
|
726
|
+
for (const line of text.split(/\r?\n/)) {
|
|
727
|
+
if (line === "") {
|
|
728
|
+
flush();
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (line.startsWith("data:")) {
|
|
732
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
flush();
|
|
736
|
+
return messages;
|
|
737
|
+
}
|
|
738
|
+
function selectJsonRpcResult(messages) {
|
|
739
|
+
return messages.find((m) => "result" in m || "error" in m) ?? messages[messages.length - 1];
|
|
740
|
+
}
|
|
634
741
|
var HavenClient = class {
|
|
635
742
|
apiKey;
|
|
636
743
|
delegateKey;
|
|
@@ -639,6 +746,7 @@ var HavenClient = class {
|
|
|
639
746
|
requestTimeout;
|
|
640
747
|
confirmationTimeout;
|
|
641
748
|
pollingInterval;
|
|
749
|
+
chainRpcs;
|
|
642
750
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
643
751
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
644
752
|
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
@@ -655,6 +763,8 @@ var HavenClient = class {
|
|
|
655
763
|
* the same time — see their own headers without stepping on each other.
|
|
656
764
|
*/
|
|
657
765
|
requestContext = new async_hooks.AsyncLocalStorage();
|
|
766
|
+
/** Monotonic JSON-RPC id source for the MCP `initialize` handshake. */
|
|
767
|
+
mcpRequestId = 0;
|
|
658
768
|
/** Delegate address derived from the private key (if provided) */
|
|
659
769
|
delegateAddress;
|
|
660
770
|
constructor(config) {
|
|
@@ -665,6 +775,7 @@ var HavenClient = class {
|
|
|
665
775
|
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
666
776
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
667
777
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
778
|
+
this.chainRpcs = config.chainRpcs ?? {};
|
|
668
779
|
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
669
780
|
if (this.delegateKey) {
|
|
670
781
|
this.delegateAddress = addressFromKey(this.delegateKey);
|
|
@@ -860,6 +971,78 @@ var HavenClient = class {
|
|
|
860
971
|
chainId: raw.chain_id
|
|
861
972
|
};
|
|
862
973
|
}
|
|
974
|
+
/**
|
|
975
|
+
* Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
|
|
976
|
+
*
|
|
977
|
+
* The delegate key held by this client signs and submits the transfer transactions
|
|
978
|
+
* directly — Haven's backend never handles the key or constructs signed txs
|
|
979
|
+
* (CASP/MiCA Red Line #2). Funds always go to the Safe linked to this agent.
|
|
980
|
+
*
|
|
981
|
+
* Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
|
|
982
|
+
*/
|
|
983
|
+
async sweepDelegate() {
|
|
984
|
+
if (!this.delegateKey) {
|
|
985
|
+
throw new HavenSigningError("delegateKey is required for sweepDelegate.");
|
|
986
|
+
}
|
|
987
|
+
const agent = await this.getAgent();
|
|
988
|
+
const { safeAddress, delegateAddress, chainId } = agent;
|
|
989
|
+
if (!delegateAddress) {
|
|
990
|
+
throw new HavenApiError("Agent has no delegate address.", 422);
|
|
991
|
+
}
|
|
992
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
993
|
+
if (!rpcUrl) {
|
|
994
|
+
throw new HavenApiError(
|
|
995
|
+
`chainRpcs[${chainId}] must be configured to sweep the delegate wallet.`,
|
|
996
|
+
422
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
1000
|
+
const wallet = createWallet(this.delegateKey, provider);
|
|
1001
|
+
const ERC20_TRANSFER_ABI = ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"];
|
|
1002
|
+
const transfers = [];
|
|
1003
|
+
const usdcAddress = CHAIN_USDC[chainId];
|
|
1004
|
+
if (usdcAddress) {
|
|
1005
|
+
const usdcContract = createErc20Contract(usdcAddress, ERC20_TRANSFER_ABI, wallet);
|
|
1006
|
+
const usdcBalance = await usdcContract.balanceOf(delegateAddress);
|
|
1007
|
+
if (usdcBalance > 0n) {
|
|
1008
|
+
const tx = await usdcContract.transfer(safeAddress, usdcBalance);
|
|
1009
|
+
const receipt = await tx.wait(1);
|
|
1010
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1011
|
+
transfers.push({
|
|
1012
|
+
asset: "USDC",
|
|
1013
|
+
amount: formatAtomicAmount(usdcBalance, 6),
|
|
1014
|
+
amountAtomic: usdcBalance.toString(),
|
|
1015
|
+
txHash,
|
|
1016
|
+
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const ethBalance = await provider.getBalance(delegateAddress);
|
|
1021
|
+
if (ethBalance > 0n) {
|
|
1022
|
+
const gasPrice = (await provider.getFeeData()).gasPrice ?? 1000000n;
|
|
1023
|
+
const gasLimit = 21000n;
|
|
1024
|
+
const gasCost = gasPrice * gasLimit;
|
|
1025
|
+
const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
|
|
1026
|
+
if (ethToSend > 0n) {
|
|
1027
|
+
const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
|
|
1028
|
+
const receipt = await tx.wait(1);
|
|
1029
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1030
|
+
transfers.push({
|
|
1031
|
+
asset: "ETH",
|
|
1032
|
+
amount: formatAtomicAmount(ethToSend, 18),
|
|
1033
|
+
amountAtomic: ethToSend.toString(),
|
|
1034
|
+
txHash,
|
|
1035
|
+
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
fromAddress: delegateAddress,
|
|
1041
|
+
toAddress: safeAddress,
|
|
1042
|
+
chainId,
|
|
1043
|
+
transfers
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
863
1046
|
/**
|
|
864
1047
|
* Get configured and on-chain allowances for the authenticated agent.
|
|
865
1048
|
*/
|
|
@@ -889,6 +1072,36 @@ var HavenClient = class {
|
|
|
889
1072
|
}))
|
|
890
1073
|
};
|
|
891
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Discover payable services from Haven's curated merchant catalog.
|
|
1077
|
+
*
|
|
1078
|
+
* Read-only: returns catalog entries (price, rail, protocol) so an agent
|
|
1079
|
+
* can choose a service and pay it with the regular payment tools in the
|
|
1080
|
+
* same session. Never creates payments or signatures.
|
|
1081
|
+
*/
|
|
1082
|
+
async discoverTools(options = {}) {
|
|
1083
|
+
const params = new URLSearchParams();
|
|
1084
|
+
if (options.category) params.set("category", options.category);
|
|
1085
|
+
if (options.rail) params.set("rail", options.rail);
|
|
1086
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1087
|
+
const raw = await this.get(`/catalog${query}`);
|
|
1088
|
+
return raw.entries.map((entry) => ({
|
|
1089
|
+
id: entry.id,
|
|
1090
|
+
name: entry.name,
|
|
1091
|
+
description: entry.description,
|
|
1092
|
+
category: entry.category,
|
|
1093
|
+
resourceUrl: entry.resource_url,
|
|
1094
|
+
rail: entry.rail,
|
|
1095
|
+
protocol: entry.protocol,
|
|
1096
|
+
toolName: entry.tool_name,
|
|
1097
|
+
priceDisplay: entry.price_display,
|
|
1098
|
+
priceAtomic: entry.price_atomic,
|
|
1099
|
+
asset: entry.asset,
|
|
1100
|
+
network: entry.network,
|
|
1101
|
+
status: entry.status,
|
|
1102
|
+
verifiedAt: entry.verified_at
|
|
1103
|
+
}));
|
|
1104
|
+
}
|
|
892
1105
|
/**
|
|
893
1106
|
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
894
1107
|
*/
|
|
@@ -1046,6 +1259,10 @@ var HavenClient = class {
|
|
|
1046
1259
|
if (execResult.status !== "confirmed") {
|
|
1047
1260
|
this.throwPaymentStateError("x402 payment", execResult);
|
|
1048
1261
|
}
|
|
1262
|
+
await this.waitForFundingTx(
|
|
1263
|
+
execResult.tx_hash,
|
|
1264
|
+
execResult.chain_id ?? chainIdFromNetwork(option.network)
|
|
1265
|
+
);
|
|
1049
1266
|
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
1050
1267
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
1051
1268
|
return receipt;
|
|
@@ -1112,16 +1329,33 @@ var HavenClient = class {
|
|
|
1112
1329
|
* const data = await response.json()
|
|
1113
1330
|
* ```
|
|
1114
1331
|
*
|
|
1332
|
+
* **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
|
|
1333
|
+
* MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
|
|
1334
|
+
* Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
|
|
1335
|
+
* `initialize` handshake, threads the resulting `mcp-session-id`,
|
|
1336
|
+
* `Accept: application/json, text/event-stream`, and `x402-wallet` headers
|
|
1337
|
+
* through every request, and collapses SSE responses to the JSON-RPC
|
|
1338
|
+
* `result`. The caller just passes `(url, { body })` and never sees the
|
|
1339
|
+
* protocol plumbing. A non-MCP server (handshake error / no session id)
|
|
1340
|
+
* falls back to standard x402 behaviour.
|
|
1341
|
+
*
|
|
1115
1342
|
* Requires `delegateKey` to be set in the client config.
|
|
1116
1343
|
*/
|
|
1117
1344
|
async fetch(url, init, options = {}) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1345
|
+
let mcpSessionId;
|
|
1346
|
+
if (isMcpUrl(url)) {
|
|
1347
|
+
mcpSessionId = await this.mcpInitialize(url, init);
|
|
1348
|
+
}
|
|
1349
|
+
let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
1350
|
+
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1351
|
+
const response = await globalThis.fetch(url, requestInit);
|
|
1352
|
+
if (response.status !== 402) {
|
|
1353
|
+
return mcpSessionId ? this.surfaceMcpResult(response) : response;
|
|
1354
|
+
}
|
|
1121
1355
|
const machineChallengeHeader = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
1122
1356
|
if (machineChallengeHeader) {
|
|
1123
1357
|
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
1124
|
-
return this.fetchWithMachinePayment(url,
|
|
1358
|
+
return this.fetchWithMachinePayment(url, requestInit, challenge);
|
|
1125
1359
|
}
|
|
1126
1360
|
let paymentRequired;
|
|
1127
1361
|
try {
|
|
@@ -1133,9 +1367,13 @@ var HavenClient = class {
|
|
|
1133
1367
|
} catch {
|
|
1134
1368
|
return response;
|
|
1135
1369
|
}
|
|
1136
|
-
return this.fetchWithMachinePayment(url,
|
|
1370
|
+
return this.fetchWithMachinePayment(url, requestInit, challenge);
|
|
1137
1371
|
}
|
|
1138
|
-
|
|
1372
|
+
if (!mcpSessionId && await responseHasBazaarExtension(response)) {
|
|
1373
|
+
mcpSessionId = await this.mcpInitialize(url, init);
|
|
1374
|
+
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1375
|
+
}
|
|
1376
|
+
const request = this.snapshotX402Request(url, requestInit);
|
|
1139
1377
|
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1140
1378
|
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
1141
1379
|
let receipt;
|
|
@@ -1153,7 +1391,122 @@ var HavenClient = class {
|
|
|
1153
1391
|
}
|
|
1154
1392
|
throw err;
|
|
1155
1393
|
}
|
|
1156
|
-
|
|
1394
|
+
const retryResponse = await this.retryX402Request(url, requestInit, paymentRequired, receipt);
|
|
1395
|
+
return mcpSessionId ? this.surfaceMcpResult(retryResponse) : retryResponse;
|
|
1396
|
+
}
|
|
1397
|
+
// ── MCP-over-x402 transport helpers (issue #315) ─────────────────
|
|
1398
|
+
/**
|
|
1399
|
+
* Run the MCP `initialize` handshake against a Streamable-HTTP endpoint and
|
|
1400
|
+
* return the `mcp-session-id` the server assigns.
|
|
1401
|
+
*
|
|
1402
|
+
* Returns `undefined` whenever the endpoint is not actually an MCP server —
|
|
1403
|
+
* a transport/HTTP error, a missing session id, or a JSON-RPC error in the
|
|
1404
|
+
* handshake response — so the caller can fall back to plain x402.
|
|
1405
|
+
*/
|
|
1406
|
+
async mcpInitialize(url, init) {
|
|
1407
|
+
try {
|
|
1408
|
+
const headers = new Headers(init?.headers);
|
|
1409
|
+
headers.set("Content-Type", "application/json");
|
|
1410
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1411
|
+
const wallet = this.x402PayerAddress();
|
|
1412
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1413
|
+
const response = await globalThis.fetch(url, {
|
|
1414
|
+
method: "POST",
|
|
1415
|
+
headers,
|
|
1416
|
+
body: JSON.stringify({
|
|
1417
|
+
jsonrpc: "2.0",
|
|
1418
|
+
id: ++this.mcpRequestId,
|
|
1419
|
+
method: "initialize",
|
|
1420
|
+
params: {
|
|
1421
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1422
|
+
capabilities: {},
|
|
1423
|
+
clientInfo: MCP_CLIENT_INFO
|
|
1424
|
+
}
|
|
1425
|
+
})
|
|
1426
|
+
});
|
|
1427
|
+
if (!response.ok) return void 0;
|
|
1428
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1429
|
+
if (!sessionId) return void 0;
|
|
1430
|
+
const message = await this.readMcpMessage(response);
|
|
1431
|
+
if (message && "error" in message) return void 0;
|
|
1432
|
+
await this.mcpNotifyInitialized(url, init, sessionId);
|
|
1433
|
+
return sessionId;
|
|
1434
|
+
} catch {
|
|
1435
|
+
return void 0;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Send the MCP `notifications/initialized` notification that completes the
|
|
1440
|
+
* lifecycle handshake. Best-effort: the session is already established, so a
|
|
1441
|
+
* failed notification must not abort the payment.
|
|
1442
|
+
*/
|
|
1443
|
+
async mcpNotifyInitialized(url, init, sessionId) {
|
|
1444
|
+
try {
|
|
1445
|
+
const headers = new Headers(init?.headers);
|
|
1446
|
+
headers.set("Content-Type", "application/json");
|
|
1447
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1448
|
+
headers.set("mcp-session-id", sessionId);
|
|
1449
|
+
const wallet = this.x402PayerAddress();
|
|
1450
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1451
|
+
await globalThis.fetch(url, {
|
|
1452
|
+
method: "POST",
|
|
1453
|
+
headers,
|
|
1454
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1455
|
+
});
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/** Read a single JSON-RPC message from an MCP response (JSON or SSE body). */
|
|
1460
|
+
async readMcpMessage(response) {
|
|
1461
|
+
let text;
|
|
1462
|
+
try {
|
|
1463
|
+
text = await response.clone().text();
|
|
1464
|
+
} catch {
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1468
|
+
return selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
return JSON.parse(text);
|
|
1472
|
+
} catch {
|
|
1473
|
+
return void 0;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/** Add the MCP transport headers (session id + SSE Accept) to a request. */
|
|
1477
|
+
withMcpHeaders(init, sessionId) {
|
|
1478
|
+
const headers = new Headers(init?.headers);
|
|
1479
|
+
headers.set("mcp-session-id", sessionId);
|
|
1480
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1481
|
+
return { ...init, headers };
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Collapse an MCP SSE response into a plain JSON response carrying the
|
|
1485
|
+
* JSON-RPC `result`, so callers of `fetch()` never see raw SSE framing.
|
|
1486
|
+
* Non-SSE responses pass through untouched.
|
|
1487
|
+
*/
|
|
1488
|
+
async surfaceMcpResult(response) {
|
|
1489
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1490
|
+
return response;
|
|
1491
|
+
}
|
|
1492
|
+
let text;
|
|
1493
|
+
try {
|
|
1494
|
+
text = await response.clone().text();
|
|
1495
|
+
} catch {
|
|
1496
|
+
return response;
|
|
1497
|
+
}
|
|
1498
|
+
const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1499
|
+
if (!message) return response;
|
|
1500
|
+
const body = "result" in message ? message.result : message;
|
|
1501
|
+
const headers = new Headers(response.headers);
|
|
1502
|
+
headers.set("content-type", "application/json");
|
|
1503
|
+
headers.delete("content-length");
|
|
1504
|
+
headers.delete("mcp-session-id");
|
|
1505
|
+
return new Response(JSON.stringify(body), {
|
|
1506
|
+
status: response.status,
|
|
1507
|
+
statusText: response.statusText,
|
|
1508
|
+
headers
|
|
1509
|
+
});
|
|
1157
1510
|
}
|
|
1158
1511
|
/**
|
|
1159
1512
|
* Probe a paid MPP endpoint or inspect an existing challenge without creating
|
|
@@ -1212,12 +1565,13 @@ var HavenClient = class {
|
|
|
1212
1565
|
headers: retryHeaders
|
|
1213
1566
|
});
|
|
1214
1567
|
if (!retryResponse.ok) {
|
|
1568
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
1215
1569
|
await this.recordMerchantRetryRejected({
|
|
1216
1570
|
rail: "x402",
|
|
1217
1571
|
paymentId: receipt.paymentId,
|
|
1218
1572
|
txHash: receipt.txHash,
|
|
1219
1573
|
resourceUrl: receipt.resourceUrl,
|
|
1220
|
-
|
|
1574
|
+
merchant,
|
|
1221
1575
|
details: {
|
|
1222
1576
|
merchant_to: receipt.merchantTo,
|
|
1223
1577
|
delegate_to: receipt.to
|
|
@@ -1225,14 +1579,15 @@ var HavenClient = class {
|
|
|
1225
1579
|
});
|
|
1226
1580
|
throw new HavenApiError(
|
|
1227
1581
|
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
1228
|
-
|
|
1582
|
+
merchant.merchant_status,
|
|
1229
1583
|
{
|
|
1230
1584
|
marker: "x402_retry_rejected_after_funding",
|
|
1231
1585
|
payment_id: receipt.paymentId,
|
|
1232
1586
|
tx_hash: receipt.txHash,
|
|
1233
1587
|
resource_url: receipt.resourceUrl,
|
|
1234
1588
|
merchant_to: receipt.merchantTo,
|
|
1235
|
-
delegate_to: receipt.to
|
|
1589
|
+
delegate_to: receipt.to,
|
|
1590
|
+
...merchant
|
|
1236
1591
|
}
|
|
1237
1592
|
);
|
|
1238
1593
|
}
|
|
@@ -1365,25 +1720,27 @@ var HavenClient = class {
|
|
|
1365
1720
|
headers: retryHeaders
|
|
1366
1721
|
});
|
|
1367
1722
|
if (!retryResponse.ok) {
|
|
1723
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
1368
1724
|
await this.recordMerchantRetryRejected({
|
|
1369
1725
|
rail: receipt.rail,
|
|
1370
1726
|
paymentId: receipt.paymentId,
|
|
1371
1727
|
txHash: receipt.txHash,
|
|
1372
1728
|
resourceUrl: receipt.resourceUrl,
|
|
1373
|
-
|
|
1729
|
+
merchant,
|
|
1374
1730
|
details: {
|
|
1375
1731
|
challenge_id: receipt.challengeId
|
|
1376
1732
|
}
|
|
1377
1733
|
});
|
|
1378
1734
|
throw new HavenApiError(
|
|
1379
1735
|
"Machine payment retry failed after Haven sent the payment.",
|
|
1380
|
-
|
|
1736
|
+
merchant.merchant_status,
|
|
1381
1737
|
{
|
|
1382
1738
|
marker: "machine_payment_retry_rejected_after_payment",
|
|
1383
1739
|
payment_id: receipt.paymentId,
|
|
1384
1740
|
tx_hash: receipt.txHash,
|
|
1385
1741
|
resource_url: receipt.resourceUrl,
|
|
1386
|
-
rail: receipt.rail
|
|
1742
|
+
rail: receipt.rail,
|
|
1743
|
+
...merchant
|
|
1387
1744
|
}
|
|
1388
1745
|
);
|
|
1389
1746
|
}
|
|
@@ -1619,12 +1976,12 @@ var HavenClient = class {
|
|
|
1619
1976
|
requirements
|
|
1620
1977
|
);
|
|
1621
1978
|
if (paymentRequired.x402Version < 2) return header;
|
|
1622
|
-
const payment =
|
|
1623
|
-
return
|
|
1979
|
+
const payment = decodeBase64Json(header);
|
|
1980
|
+
return encodeBase64Json({
|
|
1624
1981
|
x402Version: paymentRequired.x402Version,
|
|
1625
1982
|
accepted: option,
|
|
1626
1983
|
payload: payment.payload
|
|
1627
|
-
})
|
|
1984
|
+
});
|
|
1628
1985
|
}
|
|
1629
1986
|
cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
|
|
1630
1987
|
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
@@ -1686,11 +2043,11 @@ var HavenClient = class {
|
|
|
1686
2043
|
rail: input.rail,
|
|
1687
2044
|
eventType: "merchant_retry_rejected_after_payment",
|
|
1688
2045
|
txHash: input.txHash,
|
|
1689
|
-
reason: `Merchant returned HTTP ${input.
|
|
2046
|
+
reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
|
|
1690
2047
|
details: {
|
|
1691
2048
|
resource_url: input.resourceUrl,
|
|
1692
|
-
retry_status: input.
|
|
1693
|
-
retry_body:
|
|
2049
|
+
retry_status: input.merchant.merchant_status,
|
|
2050
|
+
retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
1694
2051
|
...input.details
|
|
1695
2052
|
}
|
|
1696
2053
|
});
|
|
@@ -1716,6 +2073,29 @@ var HavenClient = class {
|
|
|
1716
2073
|
} catch {
|
|
1717
2074
|
}
|
|
1718
2075
|
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
2078
|
+
* merchant retry, eliminating the race where the merchant's
|
|
2079
|
+
* `balanceOf(delegate)` runs before the funding block propagates.
|
|
2080
|
+
*
|
|
2081
|
+
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2082
|
+
* backend has already confirmed on-chain submission and callers accept the
|
|
2083
|
+
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2084
|
+
*/
|
|
2085
|
+
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2086
|
+
if (!txHash || !chainId) return;
|
|
2087
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2088
|
+
if (!rpcUrl) return;
|
|
2089
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2090
|
+
const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2091
|
+
if (!onChainReceipt || onChainReceipt.status !== 1) {
|
|
2092
|
+
throw new HavenApiError(
|
|
2093
|
+
"Funding tx did not confirm on-chain within the timeout window.",
|
|
2094
|
+
500,
|
|
2095
|
+
{ txHash, chainId }
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1719
2099
|
throwIfNonSignableAuthorizationState(label, raw) {
|
|
1720
2100
|
if (raw.status === "pending_signature") return;
|
|
1721
2101
|
this.throwPaymentStateError(label, raw);
|
|
@@ -2301,7 +2681,7 @@ function sleep(ms) {
|
|
|
2301
2681
|
}
|
|
2302
2682
|
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
2303
2683
|
try {
|
|
2304
|
-
const payment =
|
|
2684
|
+
const payment = decodeBase64Json(
|
|
2305
2685
|
paymentHeader
|
|
2306
2686
|
);
|
|
2307
2687
|
const payload = payment.payload;
|
|
@@ -2311,12 +2691,9 @@ function getPaymentHeaderValidBefore(paymentHeader) {
|
|
|
2311
2691
|
}
|
|
2312
2692
|
return 0;
|
|
2313
2693
|
}
|
|
2314
|
-
function decodeBase64Json3(value) {
|
|
2315
|
-
return JSON.parse(atob(value));
|
|
2316
|
-
}
|
|
2317
2694
|
function parseProtocolReceiptHeader(value) {
|
|
2318
2695
|
try {
|
|
2319
|
-
return
|
|
2696
|
+
return decodeBase64Json(value);
|
|
2320
2697
|
} catch {
|
|
2321
2698
|
try {
|
|
2322
2699
|
return JSON.parse(value);
|
|
@@ -2325,13 +2702,14 @@ function parseProtocolReceiptHeader(value) {
|
|
|
2325
2702
|
}
|
|
2326
2703
|
}
|
|
2327
2704
|
}
|
|
2328
|
-
async function
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2705
|
+
async function captureMerchantResponse(response) {
|
|
2706
|
+
const merchant_body = await response.text().catch(() => "");
|
|
2707
|
+
return {
|
|
2708
|
+
merchant_status: response.status,
|
|
2709
|
+
merchant_status_text: response.statusText,
|
|
2710
|
+
merchant_headers: Object.fromEntries(response.headers.entries()),
|
|
2711
|
+
merchant_body
|
|
2712
|
+
};
|
|
2335
2713
|
}
|
|
2336
2714
|
|
|
2337
2715
|
// src/tool-descriptions.ts
|
|
@@ -2403,6 +2781,30 @@ var toolDescriptions = {
|
|
|
2403
2781
|
selectionGuidance: "Use this for transaction history, receipts, payment evidence, or bookkeeping; use the allowance tool instead for remaining allowance, budget, spend-limit, or what-can-I-spend questions.",
|
|
2404
2782
|
behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
|
|
2405
2783
|
nextActionGuidance: ""
|
|
2784
|
+
},
|
|
2785
|
+
payMcpTool: {
|
|
2786
|
+
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
|
|
2787
|
+
selectionGuidance: "Use this when the agent wants to call a specific tool on an MCP merchant (e.g. Soundside, Coinbase Bazaar) and payment is required. Prefer this over haven_pay_x402 when you know the merchant_url and tool_name \u2014 it builds the JSON-RPC envelope internally. Use haven_pay_x402 for arbitrary HTTP resources. Do NOT use for read-only allowance or budget questions \u2014 use haven_get_allowances.",
|
|
2788
|
+
behavior: "Builds the JSON-RPC tools/call envelope, runs the MCP Streamable-HTTP initialize handshake automatically (if the endpoint is MCP-shaped), pays any HTTP 402 x402 challenge through Haven's AllowanceModule path, and retries the request. Returns the JSON-RPC result (the actual merchant output) on success. Amounts within the on-chain allowance execute automatically; over-allowance transfers are queued as pending_approval.",
|
|
2789
|
+
nextActionGuidance: "If pending_approval is returned, preserve payment_id and resume_state and wait for the wallet owner to approve in Haven. Use haven_resume_x402_payment once nextAction=retry_original_x402_request."
|
|
2790
|
+
},
|
|
2791
|
+
discoverTools: {
|
|
2792
|
+
summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
|
|
2793
|
+
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.",
|
|
2794
|
+
behavior: "Read-only lookup against Haven's curated catalog. Entries are periodically re-verified against the live merchant; degraded entries are flagged. Returns name, description, price, rail, resource URL, and a suggested_tool field naming the exact Haven pay tool for that entry. Never creates a payment, signature, or approval.",
|
|
2795
|
+
nextActionGuidance: "Pick an entry, confirm the price with the user if it is non-trivial, and pay it with the tool named in suggested_tool, passing the entry's resource_url (and tool_name for MCP merchants)."
|
|
2796
|
+
},
|
|
2797
|
+
sweep_delegate: {
|
|
2798
|
+
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
2799
|
+
selectionGuidance: "Use this when the user instructs you to recover stranded funds on the delegate wallet, or when a payment status returns nextAction=sweep_stranded_funds. Do NOT use for normal payments \u2014 use haven_pay_x402 or haven_pay_mpp_challenge. Do NOT use to read balances only \u2014 use haven_get_allowances.",
|
|
2800
|
+
behavior: "Reads the delegate EOA's on-chain USDC and ETH balances. For each non-zero balance, signs and submits a transfer from the delegate EOA to the originating Safe (hardcoded destination). The delegate key signs locally \u2014 Haven never sees it and the backend never constructs signed transactions (CASP/MiCA Red Line #2). Returns tx hashes and recovered amounts. Returns an empty transfers list when nothing is stranded.",
|
|
2801
|
+
nextActionGuidance: "If transfers is non-empty, confirm the amounts with the user. No further action required \u2014 funds are on their way back to the Safe."
|
|
2802
|
+
},
|
|
2803
|
+
send: {
|
|
2804
|
+
summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
|
|
2805
|
+
selectionGuidance: "Use this for plain transfers \u2014 refunding a user, paying a freelancer, topping up a co-agent's wallet, or moving funds between addresses. Do NOT use for x402 paid endpoints (use haven_pay_x402 instead) or MPP merchant payments (use haven_pay_mpp_challenge). Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances.",
|
|
2806
|
+
behavior: "Sends the requested amount through the Safe AllowanceModule. Amounts within the remaining on-chain allowance for the asset execute automatically; amounts that exceed the allowance are queued as pending_approval for the wallet owner to approve in Haven. The agent's signing key signs the AllowanceModule transfer hash; Haven never receives the key.",
|
|
2807
|
+
nextActionGuidance: "If pending_approval is returned, preserve the payment_id and wait for the wallet owner to approve in Haven. Poll haven_get_payment_status until nextAction=none."
|
|
2406
2808
|
}
|
|
2407
2809
|
};
|
|
2408
2810
|
|
|
@@ -2532,6 +2934,12 @@ var GET_ALLOWANCES_DESCRIPTION = composeDescription(toolDescriptions.getAllowanc
|
|
|
2532
2934
|
var AUTHORIZE_X402_DESCRIPTION = composeDescription(toolDescriptions.payX402) + " In this SDK tool set, the allowance lookup tool is get_allowances. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key and request a policy-limited Safe AllowanceModule top-up when needed. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. If this returns pending_approval, tell the user it is waiting in Haven, preserve the original merchant/MCP session and x402 details, call get_payment_status later, and use resume_x402_payment only when next_action is retry_original_x402_request. Do not start a new merchant session or loop retries while approval is pending. Use the returned payment_header as the X-PAYMENT header on the retry request when doing a manual HTTP retry.";
|
|
2533
2935
|
var RESUME_X402_DESCRIPTION = toolDescriptions.resumeX402.summary + " Use this only after get_payment_status returns next_action=retry_original_x402_request. It checks the approved payment, validates the original x402 details, and returns a merchant X-PAYMENT header without creating a new approval request or merchant session.";
|
|
2534
2936
|
var AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION = composeDescription(toolDescriptions.payMpp) + " In this SDK tool set, the allowance lookup tool is get_allowances. Currently scoped to the internal MPP demo rail. The agent signs the payment, Haven relays it within the on-chain allowance, and the tool returns a proof header for the retry request.";
|
|
2937
|
+
var SWEEP_DELEGATE_DESCRIPTION = composeDescription(toolDescriptions.sweep_delegate);
|
|
2938
|
+
var sweepDelegateSchema = {
|
|
2939
|
+
type: "object",
|
|
2940
|
+
properties: {},
|
|
2941
|
+
required: []
|
|
2942
|
+
};
|
|
2535
2943
|
function claudeTools() {
|
|
2536
2944
|
return [
|
|
2537
2945
|
{
|
|
@@ -2563,6 +2971,11 @@ function claudeTools() {
|
|
|
2563
2971
|
name: "authorize_machine_payment",
|
|
2564
2972
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
2565
2973
|
input_schema: authorizeMachinePaymentSchema
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
name: "haven_sweep_delegate",
|
|
2977
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
2978
|
+
input_schema: sweepDelegateSchema
|
|
2566
2979
|
}
|
|
2567
2980
|
];
|
|
2568
2981
|
}
|
|
@@ -2615,6 +3028,14 @@ function openaiTools() {
|
|
|
2615
3028
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
2616
3029
|
parameters: authorizeMachinePaymentSchema
|
|
2617
3030
|
}
|
|
3031
|
+
},
|
|
3032
|
+
{
|
|
3033
|
+
type: "function",
|
|
3034
|
+
function: {
|
|
3035
|
+
name: "haven_sweep_delegate",
|
|
3036
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
3037
|
+
parameters: sweepDelegateSchema
|
|
3038
|
+
}
|
|
2618
3039
|
}
|
|
2619
3040
|
];
|
|
2620
3041
|
}
|
|
@@ -2647,6 +3068,10 @@ exports.addressFromKey = addressFromKey;
|
|
|
2647
3068
|
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
2648
3069
|
exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
|
|
2649
3070
|
exports.composeDescription = composeDescription;
|
|
3071
|
+
exports.decodeBase64Json = decodeBase64Json;
|
|
3072
|
+
exports.decodeBase64Utf8 = decodeBase64Utf8;
|
|
3073
|
+
exports.encodeBase64Json = encodeBase64Json;
|
|
3074
|
+
exports.encodeBase64Utf8 = encodeBase64Utf8;
|
|
2650
3075
|
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
2651
3076
|
exports.encodePaymentProof = encodePaymentProof;
|
|
2652
3077
|
exports.havenTools = havenTools;
|