@haven_ai/sdk 0.1.9 → 0.1.10-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 +548 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -3
- package/dist/index.d.ts +225 -3
- package/dist/index.js +543 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,7 +35,14 @@ var AgentPaymentPhase = {
|
|
|
35
35
|
* is no approval that would fix this — the originating Safe needs more
|
|
36
36
|
* funds or the agent's per-token allowance needs to be raised first.
|
|
37
37
|
*/
|
|
38
|
-
InsufficientFunds: "insufficient_funds"
|
|
38
|
+
InsufficientFunds: "insufficient_funds",
|
|
39
|
+
/**
|
|
40
|
+
* Haven's funding leg (Safe → delegate) confirmed on-chain, but the
|
|
41
|
+
* merchant rejected the x402 retry. The delegate wallet may hold stranded
|
|
42
|
+
* USDC that was never settled to the merchant. The agent should stop, tell
|
|
43
|
+
* the user, and wait for the sweep flow to reclaim the funds.
|
|
44
|
+
*/
|
|
45
|
+
FundedButUnsettled: "funded_but_unsettled"
|
|
39
46
|
};
|
|
40
47
|
var AgentPaymentNextAction = {
|
|
41
48
|
/** Sign with the delegate key and submit the payment to Haven. */
|
|
@@ -59,7 +66,13 @@ var AgentPaymentNextAction = {
|
|
|
59
66
|
* the agent's per-token allowance needs to be raised before the payment
|
|
60
67
|
* can succeed. A user approval will not fix this state on its own.
|
|
61
68
|
*/
|
|
62
|
-
FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance"
|
|
69
|
+
FundSafeOrRaiseAllowance: "fund_safe_or_raise_allowance",
|
|
70
|
+
/**
|
|
71
|
+
* The delegate wallet may hold funds that were sent from the Safe but never
|
|
72
|
+
* settled to the merchant. The wallet owner should initiate a sweep to
|
|
73
|
+
* return those funds to the originating Safe.
|
|
74
|
+
*/
|
|
75
|
+
SweepStrandedFunds: "sweep_stranded_funds"
|
|
63
76
|
};
|
|
64
77
|
var AgentPaymentRail = {
|
|
65
78
|
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
@@ -91,7 +104,8 @@ var AgentPaymentPhaseDescriptions = {
|
|
|
91
104
|
[AgentPaymentPhase.Rejected]: "The wallet owner rejected the request; the agent should stop and tell the user.",
|
|
92
105
|
[AgentPaymentPhase.Expired]: "The payment or approval request expired before completion.",
|
|
93
106
|
[AgentPaymentPhase.Failed]: "Haven could not complete the payment; the agent should stop and surface the failure.",
|
|
94
|
-
[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."
|
|
107
|
+
[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.",
|
|
108
|
+
[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."
|
|
95
109
|
};
|
|
96
110
|
var AgentPaymentNextActionDescriptions = {
|
|
97
111
|
[AgentPaymentNextAction.SignAndSubmitPayment]: "Sign with the delegate key and submit the payment to Haven.",
|
|
@@ -102,7 +116,8 @@ var AgentPaymentNextActionDescriptions = {
|
|
|
102
116
|
[AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
|
|
103
117
|
[AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
|
|
104
118
|
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
|
|
105
|
-
[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."
|
|
119
|
+
[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.",
|
|
120
|
+
[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."
|
|
106
121
|
};
|
|
107
122
|
var AgentPaymentRailDescriptions = {
|
|
108
123
|
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
|
|
@@ -216,16 +231,52 @@ function verifySignature(hash, signature, expectedAddress) {
|
|
|
216
231
|
return false;
|
|
217
232
|
}
|
|
218
233
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
|
|
235
|
+
// src/base64.ts
|
|
236
|
+
function normalizeBase64(value) {
|
|
237
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
238
|
+
const remainder = standard.length % 4;
|
|
239
|
+
return remainder === 0 ? standard : standard + "=".repeat(4 - remainder);
|
|
240
|
+
}
|
|
241
|
+
function encodeBase64Utf8(value) {
|
|
242
|
+
if (typeof Buffer !== "undefined") {
|
|
243
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
244
|
+
}
|
|
245
|
+
const bytes = new TextEncoder().encode(value);
|
|
246
|
+
let binary = "";
|
|
247
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
248
|
+
binary += String.fromCharCode(bytes[i]);
|
|
249
|
+
}
|
|
250
|
+
return btoa(binary);
|
|
251
|
+
}
|
|
252
|
+
function decodeBase64Utf8(value) {
|
|
253
|
+
const normalized = normalizeBase64(value);
|
|
254
|
+
if (typeof Buffer !== "undefined") {
|
|
255
|
+
return Buffer.from(normalized, "base64").toString("utf8");
|
|
256
|
+
}
|
|
257
|
+
const binary = atob(normalized);
|
|
258
|
+
const bytes = new Uint8Array(binary.length);
|
|
259
|
+
for (let i = 0; i < binary.length; i++) {
|
|
260
|
+
bytes[i] = binary.charCodeAt(i);
|
|
261
|
+
}
|
|
262
|
+
return new TextDecoder().decode(bytes);
|
|
263
|
+
}
|
|
264
|
+
function encodeBase64Json(value) {
|
|
265
|
+
return encodeBase64Utf8(JSON.stringify(value));
|
|
266
|
+
}
|
|
222
267
|
function decodeBase64Json(value, label) {
|
|
223
268
|
try {
|
|
224
|
-
return JSON.parse(
|
|
225
|
-
} catch {
|
|
226
|
-
throw new Error(`Failed to decode ${label}`);
|
|
269
|
+
return JSON.parse(decodeBase64Utf8(value));
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (label) throw new Error(`Failed to decode ${label}`);
|
|
272
|
+
throw err;
|
|
227
273
|
}
|
|
228
274
|
}
|
|
275
|
+
|
|
276
|
+
// src/x402.ts
|
|
277
|
+
var BASE_USDC_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
278
|
+
var X402_IDEMPOTENCY_BUCKET_MS = 3e5;
|
|
279
|
+
var DECIMAL_ATOMIC_AMOUNT_RE = /^[0-9]+$/;
|
|
229
280
|
function isPositiveDecimalAtomicAmount(value) {
|
|
230
281
|
return DECIMAL_ATOMIC_AMOUNT_RE.test(value) && BigInt(value) > 0n;
|
|
231
282
|
}
|
|
@@ -433,7 +484,7 @@ function encodePaymentProof(receipt) {
|
|
|
433
484
|
chainId: receipt.chainId
|
|
434
485
|
}
|
|
435
486
|
};
|
|
436
|
-
return
|
|
487
|
+
return encodeBase64Json(payload);
|
|
437
488
|
}
|
|
438
489
|
function resolveTokenFromAddress(address, network) {
|
|
439
490
|
const lower = address.toLowerCase();
|
|
@@ -451,13 +502,6 @@ function stableStringify(value) {
|
|
|
451
502
|
const object = value;
|
|
452
503
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
|
|
453
504
|
}
|
|
454
|
-
function decodeBase64Json2(value, label) {
|
|
455
|
-
try {
|
|
456
|
-
return JSON.parse(atob(value));
|
|
457
|
-
} catch {
|
|
458
|
-
throw new Error(`Failed to decode ${label}`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
505
|
function normalizeChallenge(value) {
|
|
462
506
|
const candidate = value;
|
|
463
507
|
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.
|
|
@@ -484,7 +528,7 @@ function parseMachinePaymentChallenge(response) {
|
|
|
484
528
|
throw new Error("No MACHINE-PAYMENT-CHALLENGE header found in 402 response.");
|
|
485
529
|
}
|
|
486
530
|
const parsed = normalizeChallenge(
|
|
487
|
-
|
|
531
|
+
decodeBase64Json(header, "MACHINE-PAYMENT-CHALLENGE header")
|
|
488
532
|
);
|
|
489
533
|
if (!parsed) throw new Error("Invalid machine payment challenge");
|
|
490
534
|
return parsed;
|
|
@@ -515,7 +559,7 @@ function buildMachinePaymentIdempotencyKey(challenge) {
|
|
|
515
559
|
return `${challenge.rail}:${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
516
560
|
}
|
|
517
561
|
function encodeMachinePaymentProof(receipt) {
|
|
518
|
-
return
|
|
562
|
+
return encodeBase64Json({
|
|
519
563
|
rail: receipt.rail,
|
|
520
564
|
challengeId: receipt.challengeId,
|
|
521
565
|
paymentId: receipt.paymentId,
|
|
@@ -523,7 +567,16 @@ function encodeMachinePaymentProof(receipt) {
|
|
|
523
567
|
settledVia: "haven",
|
|
524
568
|
payer: receipt.payer,
|
|
525
569
|
chainId: receipt.chainId
|
|
526
|
-
})
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
function createJsonRpcProvider(url) {
|
|
573
|
+
return new ethers.JsonRpcProvider(url);
|
|
574
|
+
}
|
|
575
|
+
function createWallet(privateKey, provider) {
|
|
576
|
+
return new ethers.Wallet(privateKey, provider);
|
|
577
|
+
}
|
|
578
|
+
function createErc20Contract(address, abi, signer) {
|
|
579
|
+
return new ethers.Contract(address, abi, signer);
|
|
527
580
|
}
|
|
528
581
|
|
|
529
582
|
// src/client.ts
|
|
@@ -532,6 +585,9 @@ var CHAIN_EXPLORER_TX = {
|
|
|
532
585
|
100: "https://gnosisscan.io/tx",
|
|
533
586
|
8453: "https://basescan.org/tx"
|
|
534
587
|
};
|
|
588
|
+
var CHAIN_USDC = {
|
|
589
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
590
|
+
};
|
|
535
591
|
function buildExplorerUrl(chainId, txHash) {
|
|
536
592
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
537
593
|
return `${base}/${txHash}`;
|
|
@@ -542,6 +598,16 @@ function explorerUrlOrEmpty(chainId, txHash) {
|
|
|
542
598
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
543
599
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
544
600
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
601
|
+
function formatAtomicAmount(atomic, decimals) {
|
|
602
|
+
const s = atomic.toString().padStart(decimals + 1, "0");
|
|
603
|
+
const intPart = s.slice(0, s.length - decimals) || "0";
|
|
604
|
+
const fracPart = s.slice(s.length - decimals).replace(/0+$/, "") || "0";
|
|
605
|
+
return `${intPart}.${fracPart}`;
|
|
606
|
+
}
|
|
607
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
608
|
+
var MCP_ACCEPT = "application/json, text/event-stream";
|
|
609
|
+
var MCP_CLIENT_INFO = { name: "haven-sdk", version: "1" };
|
|
610
|
+
var MERCHANT_BODY_SNIPPET_LIMIT = 1e3;
|
|
545
611
|
var PAYMENT_STATE_STATUS_CODES = {
|
|
546
612
|
pending: 202,
|
|
547
613
|
pending_approval: 202,
|
|
@@ -629,6 +695,47 @@ function parseMerchantSettlement(header) {
|
|
|
629
695
|
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
630
696
|
return { settlementTxHash: tx };
|
|
631
697
|
}
|
|
698
|
+
function isMcpUrl(url) {
|
|
699
|
+
try {
|
|
700
|
+
return new URL(url).pathname.replace(/\/+$/, "").endsWith("/mcp");
|
|
701
|
+
} catch {
|
|
702
|
+
return /\/mcp(?:[/?#]|$)/.test(url);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function responseHasBazaarExtension(response) {
|
|
706
|
+
try {
|
|
707
|
+
const body = await response.clone().json();
|
|
708
|
+
return body?.extensions?.bazaar != null;
|
|
709
|
+
} catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function parseSseJsonRpcMessages(text) {
|
|
714
|
+
const messages = [];
|
|
715
|
+
let dataLines = [];
|
|
716
|
+
const flush = () => {
|
|
717
|
+
if (dataLines.length === 0) return;
|
|
718
|
+
try {
|
|
719
|
+
messages.push(JSON.parse(dataLines.join("\n")));
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
dataLines = [];
|
|
723
|
+
};
|
|
724
|
+
for (const line of text.split(/\r?\n/)) {
|
|
725
|
+
if (line === "") {
|
|
726
|
+
flush();
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (line.startsWith("data:")) {
|
|
730
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
flush();
|
|
734
|
+
return messages;
|
|
735
|
+
}
|
|
736
|
+
function selectJsonRpcResult(messages) {
|
|
737
|
+
return messages.find((m) => "result" in m || "error" in m) ?? messages[messages.length - 1];
|
|
738
|
+
}
|
|
632
739
|
var HavenClient = class {
|
|
633
740
|
apiKey;
|
|
634
741
|
delegateKey;
|
|
@@ -637,6 +744,7 @@ var HavenClient = class {
|
|
|
637
744
|
requestTimeout;
|
|
638
745
|
confirmationTimeout;
|
|
639
746
|
pollingInterval;
|
|
747
|
+
chainRpcs;
|
|
640
748
|
inFlightX402 = /* @__PURE__ */ new Map();
|
|
641
749
|
x402ReceiptCache = /* @__PURE__ */ new Map();
|
|
642
750
|
inFlightMachinePayments = /* @__PURE__ */ new Map();
|
|
@@ -653,6 +761,8 @@ var HavenClient = class {
|
|
|
653
761
|
* the same time — see their own headers without stepping on each other.
|
|
654
762
|
*/
|
|
655
763
|
requestContext = new AsyncLocalStorage();
|
|
764
|
+
/** Monotonic JSON-RPC id source for the MCP `initialize` handshake. */
|
|
765
|
+
mcpRequestId = 0;
|
|
656
766
|
/** Delegate address derived from the private key (if provided) */
|
|
657
767
|
delegateAddress;
|
|
658
768
|
constructor(config) {
|
|
@@ -663,6 +773,7 @@ var HavenClient = class {
|
|
|
663
773
|
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
664
774
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
665
775
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
776
|
+
this.chainRpcs = config.chainRpcs ?? {};
|
|
666
777
|
this.defaultHeaders = { ...config.defaultHeaders ?? {} };
|
|
667
778
|
if (this.delegateKey) {
|
|
668
779
|
this.delegateAddress = addressFromKey(this.delegateKey);
|
|
@@ -858,6 +969,78 @@ var HavenClient = class {
|
|
|
858
969
|
chainId: raw.chain_id
|
|
859
970
|
};
|
|
860
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Sweep stranded USDC and ETH from the delegate EOA back to the originating Safe.
|
|
974
|
+
*
|
|
975
|
+
* The delegate key held by this client signs and submits the transfer transactions
|
|
976
|
+
* directly — Haven's backend never handles the key or constructs signed txs
|
|
977
|
+
* (CASP/MiCA Red Line #2). Funds always go to the Safe linked to this agent.
|
|
978
|
+
*
|
|
979
|
+
* Requires `chainRpcs` to be set for the agent's chain in `HavenClientConfig`.
|
|
980
|
+
*/
|
|
981
|
+
async sweepDelegate() {
|
|
982
|
+
if (!this.delegateKey) {
|
|
983
|
+
throw new HavenSigningError("delegateKey is required for sweepDelegate.");
|
|
984
|
+
}
|
|
985
|
+
const agent = await this.getAgent();
|
|
986
|
+
const { safeAddress, delegateAddress, chainId } = agent;
|
|
987
|
+
if (!delegateAddress) {
|
|
988
|
+
throw new HavenApiError("Agent has no delegate address.", 422);
|
|
989
|
+
}
|
|
990
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
991
|
+
if (!rpcUrl) {
|
|
992
|
+
throw new HavenApiError(
|
|
993
|
+
`chainRpcs[${chainId}] must be configured to sweep the delegate wallet.`,
|
|
994
|
+
422
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
998
|
+
const wallet = createWallet(this.delegateKey, provider);
|
|
999
|
+
const ERC20_TRANSFER_ABI = ["function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)"];
|
|
1000
|
+
const transfers = [];
|
|
1001
|
+
const usdcAddress = CHAIN_USDC[chainId];
|
|
1002
|
+
if (usdcAddress) {
|
|
1003
|
+
const usdcContract = createErc20Contract(usdcAddress, ERC20_TRANSFER_ABI, wallet);
|
|
1004
|
+
const usdcBalance = await usdcContract.balanceOf(delegateAddress);
|
|
1005
|
+
if (usdcBalance > 0n) {
|
|
1006
|
+
const tx = await usdcContract.transfer(safeAddress, usdcBalance);
|
|
1007
|
+
const receipt = await tx.wait(1);
|
|
1008
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1009
|
+
transfers.push({
|
|
1010
|
+
asset: "USDC",
|
|
1011
|
+
amount: formatAtomicAmount(usdcBalance, 6),
|
|
1012
|
+
amountAtomic: usdcBalance.toString(),
|
|
1013
|
+
txHash,
|
|
1014
|
+
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const ethBalance = await provider.getBalance(delegateAddress);
|
|
1019
|
+
if (ethBalance > 0n) {
|
|
1020
|
+
const gasPrice = (await provider.getFeeData()).gasPrice ?? 1000000n;
|
|
1021
|
+
const gasLimit = 21000n;
|
|
1022
|
+
const gasCost = gasPrice * gasLimit;
|
|
1023
|
+
const ethToSend = ethBalance > gasCost ? ethBalance - gasCost : 0n;
|
|
1024
|
+
if (ethToSend > 0n) {
|
|
1025
|
+
const tx = await wallet.sendTransaction({ to: safeAddress, value: ethToSend });
|
|
1026
|
+
const receipt = await tx.wait(1);
|
|
1027
|
+
const txHash = receipt?.hash ?? tx.hash;
|
|
1028
|
+
transfers.push({
|
|
1029
|
+
asset: "ETH",
|
|
1030
|
+
amount: formatAtomicAmount(ethToSend, 18),
|
|
1031
|
+
amountAtomic: ethToSend.toString(),
|
|
1032
|
+
txHash,
|
|
1033
|
+
explorerUrl: buildExplorerUrl(chainId, txHash)
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
fromAddress: delegateAddress,
|
|
1039
|
+
toAddress: safeAddress,
|
|
1040
|
+
chainId,
|
|
1041
|
+
transfers
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
861
1044
|
/**
|
|
862
1045
|
* Get configured and on-chain allowances for the authenticated agent.
|
|
863
1046
|
*/
|
|
@@ -887,6 +1070,36 @@ var HavenClient = class {
|
|
|
887
1070
|
}))
|
|
888
1071
|
};
|
|
889
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Discover payable services from Haven's curated merchant catalog.
|
|
1075
|
+
*
|
|
1076
|
+
* Read-only: returns catalog entries (price, rail, protocol) so an agent
|
|
1077
|
+
* can choose a service and pay it with the regular payment tools in the
|
|
1078
|
+
* same session. Never creates payments or signatures.
|
|
1079
|
+
*/
|
|
1080
|
+
async discoverTools(options = {}) {
|
|
1081
|
+
const params = new URLSearchParams();
|
|
1082
|
+
if (options.category) params.set("category", options.category);
|
|
1083
|
+
if (options.rail) params.set("rail", options.rail);
|
|
1084
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1085
|
+
const raw = await this.get(`/catalog${query}`);
|
|
1086
|
+
return raw.entries.map((entry) => ({
|
|
1087
|
+
id: entry.id,
|
|
1088
|
+
name: entry.name,
|
|
1089
|
+
description: entry.description,
|
|
1090
|
+
category: entry.category,
|
|
1091
|
+
resourceUrl: entry.resource_url,
|
|
1092
|
+
rail: entry.rail,
|
|
1093
|
+
protocol: entry.protocol,
|
|
1094
|
+
toolName: entry.tool_name,
|
|
1095
|
+
priceDisplay: entry.price_display,
|
|
1096
|
+
priceAtomic: entry.price_atomic,
|
|
1097
|
+
asset: entry.asset,
|
|
1098
|
+
network: entry.network,
|
|
1099
|
+
status: entry.status,
|
|
1100
|
+
verifiedAt: entry.verified_at
|
|
1101
|
+
}));
|
|
1102
|
+
}
|
|
890
1103
|
/**
|
|
891
1104
|
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
892
1105
|
*/
|
|
@@ -1044,6 +1257,10 @@ var HavenClient = class {
|
|
|
1044
1257
|
if (execResult.status !== "confirmed") {
|
|
1045
1258
|
this.throwPaymentStateError("x402 payment", execResult);
|
|
1046
1259
|
}
|
|
1260
|
+
await this.waitForFundingTx(
|
|
1261
|
+
execResult.tx_hash,
|
|
1262
|
+
execResult.chain_id ?? chainIdFromNetwork(option.network)
|
|
1263
|
+
);
|
|
1047
1264
|
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
1048
1265
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
1049
1266
|
return receipt;
|
|
@@ -1110,16 +1327,33 @@ var HavenClient = class {
|
|
|
1110
1327
|
* const data = await response.json()
|
|
1111
1328
|
* ```
|
|
1112
1329
|
*
|
|
1330
|
+
* **MCP-over-x402 auto-handshake (issue #315):** when the endpoint is
|
|
1331
|
+
* MCP-shaped — the URL path ends in `/mcp`, or the 402 body carries a
|
|
1332
|
+
* Coinbase Bazaar `extensions.bazaar` block — the SDK runs the MCP
|
|
1333
|
+
* `initialize` handshake, threads the resulting `mcp-session-id`,
|
|
1334
|
+
* `Accept: application/json, text/event-stream`, and `x402-wallet` headers
|
|
1335
|
+
* through every request, and collapses SSE responses to the JSON-RPC
|
|
1336
|
+
* `result`. The caller just passes `(url, { body })` and never sees the
|
|
1337
|
+
* protocol plumbing. A non-MCP server (handshake error / no session id)
|
|
1338
|
+
* falls back to standard x402 behaviour.
|
|
1339
|
+
*
|
|
1113
1340
|
* Requires `delegateKey` to be set in the client config.
|
|
1114
1341
|
*/
|
|
1115
1342
|
async fetch(url, init, options = {}) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1343
|
+
let mcpSessionId;
|
|
1344
|
+
if (isMcpUrl(url)) {
|
|
1345
|
+
mcpSessionId = await this.mcpInitialize(url, init);
|
|
1346
|
+
}
|
|
1347
|
+
let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
1348
|
+
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1349
|
+
const response = await globalThis.fetch(url, requestInit);
|
|
1350
|
+
if (response.status !== 402) {
|
|
1351
|
+
return mcpSessionId ? this.surfaceMcpResult(response) : response;
|
|
1352
|
+
}
|
|
1119
1353
|
const machineChallengeHeader = response.headers.get("MACHINE-PAYMENT-CHALLENGE");
|
|
1120
1354
|
if (machineChallengeHeader) {
|
|
1121
1355
|
const challenge = await parseMachinePaymentChallengeResponse(response);
|
|
1122
|
-
return this.fetchWithMachinePayment(url,
|
|
1356
|
+
return this.fetchWithMachinePayment(url, requestInit, challenge);
|
|
1123
1357
|
}
|
|
1124
1358
|
let paymentRequired;
|
|
1125
1359
|
try {
|
|
@@ -1131,9 +1365,13 @@ var HavenClient = class {
|
|
|
1131
1365
|
} catch {
|
|
1132
1366
|
return response;
|
|
1133
1367
|
}
|
|
1134
|
-
return this.fetchWithMachinePayment(url,
|
|
1368
|
+
return this.fetchWithMachinePayment(url, requestInit, challenge);
|
|
1135
1369
|
}
|
|
1136
|
-
|
|
1370
|
+
if (!mcpSessionId && await responseHasBazaarExtension(response)) {
|
|
1371
|
+
mcpSessionId = await this.mcpInitialize(url, init);
|
|
1372
|
+
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1373
|
+
}
|
|
1374
|
+
const request = this.snapshotX402Request(url, requestInit);
|
|
1137
1375
|
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
1138
1376
|
const idempotencyKey = options.idempotencyKey ?? (option ? buildX402IdempotencyKey(paymentRequired, option) : void 0);
|
|
1139
1377
|
let receipt;
|
|
@@ -1151,7 +1389,122 @@ var HavenClient = class {
|
|
|
1151
1389
|
}
|
|
1152
1390
|
throw err;
|
|
1153
1391
|
}
|
|
1154
|
-
|
|
1392
|
+
const retryResponse = await this.retryX402Request(url, requestInit, paymentRequired, receipt);
|
|
1393
|
+
return mcpSessionId ? this.surfaceMcpResult(retryResponse) : retryResponse;
|
|
1394
|
+
}
|
|
1395
|
+
// ── MCP-over-x402 transport helpers (issue #315) ─────────────────
|
|
1396
|
+
/**
|
|
1397
|
+
* Run the MCP `initialize` handshake against a Streamable-HTTP endpoint and
|
|
1398
|
+
* return the `mcp-session-id` the server assigns.
|
|
1399
|
+
*
|
|
1400
|
+
* Returns `undefined` whenever the endpoint is not actually an MCP server —
|
|
1401
|
+
* a transport/HTTP error, a missing session id, or a JSON-RPC error in the
|
|
1402
|
+
* handshake response — so the caller can fall back to plain x402.
|
|
1403
|
+
*/
|
|
1404
|
+
async mcpInitialize(url, init) {
|
|
1405
|
+
try {
|
|
1406
|
+
const headers = new Headers(init?.headers);
|
|
1407
|
+
headers.set("Content-Type", "application/json");
|
|
1408
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1409
|
+
const wallet = this.x402PayerAddress();
|
|
1410
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1411
|
+
const response = await globalThis.fetch(url, {
|
|
1412
|
+
method: "POST",
|
|
1413
|
+
headers,
|
|
1414
|
+
body: JSON.stringify({
|
|
1415
|
+
jsonrpc: "2.0",
|
|
1416
|
+
id: ++this.mcpRequestId,
|
|
1417
|
+
method: "initialize",
|
|
1418
|
+
params: {
|
|
1419
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1420
|
+
capabilities: {},
|
|
1421
|
+
clientInfo: MCP_CLIENT_INFO
|
|
1422
|
+
}
|
|
1423
|
+
})
|
|
1424
|
+
});
|
|
1425
|
+
if (!response.ok) return void 0;
|
|
1426
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1427
|
+
if (!sessionId) return void 0;
|
|
1428
|
+
const message = await this.readMcpMessage(response);
|
|
1429
|
+
if (message && "error" in message) return void 0;
|
|
1430
|
+
await this.mcpNotifyInitialized(url, init, sessionId);
|
|
1431
|
+
return sessionId;
|
|
1432
|
+
} catch {
|
|
1433
|
+
return void 0;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Send the MCP `notifications/initialized` notification that completes the
|
|
1438
|
+
* lifecycle handshake. Best-effort: the session is already established, so a
|
|
1439
|
+
* failed notification must not abort the payment.
|
|
1440
|
+
*/
|
|
1441
|
+
async mcpNotifyInitialized(url, init, sessionId) {
|
|
1442
|
+
try {
|
|
1443
|
+
const headers = new Headers(init?.headers);
|
|
1444
|
+
headers.set("Content-Type", "application/json");
|
|
1445
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1446
|
+
headers.set("mcp-session-id", sessionId);
|
|
1447
|
+
const wallet = this.x402PayerAddress();
|
|
1448
|
+
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1449
|
+
await globalThis.fetch(url, {
|
|
1450
|
+
method: "POST",
|
|
1451
|
+
headers,
|
|
1452
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1453
|
+
});
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
/** Read a single JSON-RPC message from an MCP response (JSON or SSE body). */
|
|
1458
|
+
async readMcpMessage(response) {
|
|
1459
|
+
let text;
|
|
1460
|
+
try {
|
|
1461
|
+
text = await response.clone().text();
|
|
1462
|
+
} catch {
|
|
1463
|
+
return void 0;
|
|
1464
|
+
}
|
|
1465
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1466
|
+
return selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
return JSON.parse(text);
|
|
1470
|
+
} catch {
|
|
1471
|
+
return void 0;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/** Add the MCP transport headers (session id + SSE Accept) to a request. */
|
|
1475
|
+
withMcpHeaders(init, sessionId) {
|
|
1476
|
+
const headers = new Headers(init?.headers);
|
|
1477
|
+
headers.set("mcp-session-id", sessionId);
|
|
1478
|
+
headers.set("Accept", MCP_ACCEPT);
|
|
1479
|
+
return { ...init, headers };
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Collapse an MCP SSE response into a plain JSON response carrying the
|
|
1483
|
+
* JSON-RPC `result`, so callers of `fetch()` never see raw SSE framing.
|
|
1484
|
+
* Non-SSE responses pass through untouched.
|
|
1485
|
+
*/
|
|
1486
|
+
async surfaceMcpResult(response) {
|
|
1487
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1488
|
+
return response;
|
|
1489
|
+
}
|
|
1490
|
+
let text;
|
|
1491
|
+
try {
|
|
1492
|
+
text = await response.clone().text();
|
|
1493
|
+
} catch {
|
|
1494
|
+
return response;
|
|
1495
|
+
}
|
|
1496
|
+
const message = selectJsonRpcResult(parseSseJsonRpcMessages(text));
|
|
1497
|
+
if (!message) return response;
|
|
1498
|
+
const body = "result" in message ? message.result : message;
|
|
1499
|
+
const headers = new Headers(response.headers);
|
|
1500
|
+
headers.set("content-type", "application/json");
|
|
1501
|
+
headers.delete("content-length");
|
|
1502
|
+
headers.delete("mcp-session-id");
|
|
1503
|
+
return new Response(JSON.stringify(body), {
|
|
1504
|
+
status: response.status,
|
|
1505
|
+
statusText: response.statusText,
|
|
1506
|
+
headers
|
|
1507
|
+
});
|
|
1155
1508
|
}
|
|
1156
1509
|
/**
|
|
1157
1510
|
* Probe a paid MPP endpoint or inspect an existing challenge without creating
|
|
@@ -1210,12 +1563,13 @@ var HavenClient = class {
|
|
|
1210
1563
|
headers: retryHeaders
|
|
1211
1564
|
});
|
|
1212
1565
|
if (!retryResponse.ok) {
|
|
1566
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
1213
1567
|
await this.recordMerchantRetryRejected({
|
|
1214
1568
|
rail: "x402",
|
|
1215
1569
|
paymentId: receipt.paymentId,
|
|
1216
1570
|
txHash: receipt.txHash,
|
|
1217
1571
|
resourceUrl: receipt.resourceUrl,
|
|
1218
|
-
|
|
1572
|
+
merchant,
|
|
1219
1573
|
details: {
|
|
1220
1574
|
merchant_to: receipt.merchantTo,
|
|
1221
1575
|
delegate_to: receipt.to
|
|
@@ -1223,14 +1577,15 @@ var HavenClient = class {
|
|
|
1223
1577
|
});
|
|
1224
1578
|
throw new HavenApiError(
|
|
1225
1579
|
"x402 retry failed after Haven funded the delegate wallet; reconciliation may be required.",
|
|
1226
|
-
|
|
1580
|
+
merchant.merchant_status,
|
|
1227
1581
|
{
|
|
1228
1582
|
marker: "x402_retry_rejected_after_funding",
|
|
1229
1583
|
payment_id: receipt.paymentId,
|
|
1230
1584
|
tx_hash: receipt.txHash,
|
|
1231
1585
|
resource_url: receipt.resourceUrl,
|
|
1232
1586
|
merchant_to: receipt.merchantTo,
|
|
1233
|
-
delegate_to: receipt.to
|
|
1587
|
+
delegate_to: receipt.to,
|
|
1588
|
+
...merchant
|
|
1234
1589
|
}
|
|
1235
1590
|
);
|
|
1236
1591
|
}
|
|
@@ -1363,25 +1718,27 @@ var HavenClient = class {
|
|
|
1363
1718
|
headers: retryHeaders
|
|
1364
1719
|
});
|
|
1365
1720
|
if (!retryResponse.ok) {
|
|
1721
|
+
const merchant = await captureMerchantResponse(retryResponse);
|
|
1366
1722
|
await this.recordMerchantRetryRejected({
|
|
1367
1723
|
rail: receipt.rail,
|
|
1368
1724
|
paymentId: receipt.paymentId,
|
|
1369
1725
|
txHash: receipt.txHash,
|
|
1370
1726
|
resourceUrl: receipt.resourceUrl,
|
|
1371
|
-
|
|
1727
|
+
merchant,
|
|
1372
1728
|
details: {
|
|
1373
1729
|
challenge_id: receipt.challengeId
|
|
1374
1730
|
}
|
|
1375
1731
|
});
|
|
1376
1732
|
throw new HavenApiError(
|
|
1377
1733
|
"Machine payment retry failed after Haven sent the payment.",
|
|
1378
|
-
|
|
1734
|
+
merchant.merchant_status,
|
|
1379
1735
|
{
|
|
1380
1736
|
marker: "machine_payment_retry_rejected_after_payment",
|
|
1381
1737
|
payment_id: receipt.paymentId,
|
|
1382
1738
|
tx_hash: receipt.txHash,
|
|
1383
1739
|
resource_url: receipt.resourceUrl,
|
|
1384
|
-
rail: receipt.rail
|
|
1740
|
+
rail: receipt.rail,
|
|
1741
|
+
...merchant
|
|
1385
1742
|
}
|
|
1386
1743
|
);
|
|
1387
1744
|
}
|
|
@@ -1617,12 +1974,12 @@ var HavenClient = class {
|
|
|
1617
1974
|
requirements
|
|
1618
1975
|
);
|
|
1619
1976
|
if (paymentRequired.x402Version < 2) return header;
|
|
1620
|
-
const payment =
|
|
1621
|
-
return
|
|
1977
|
+
const payment = decodeBase64Json(header);
|
|
1978
|
+
return encodeBase64Json({
|
|
1622
1979
|
x402Version: paymentRequired.x402Version,
|
|
1623
1980
|
accepted: option,
|
|
1624
1981
|
payload: payment.payload
|
|
1625
|
-
})
|
|
1982
|
+
});
|
|
1626
1983
|
}
|
|
1627
1984
|
cacheX402Receipt(idempotencyKey, paymentHeader, receipt) {
|
|
1628
1985
|
const expiresAt = getPaymentHeaderValidBefore(paymentHeader);
|
|
@@ -1684,11 +2041,11 @@ var HavenClient = class {
|
|
|
1684
2041
|
rail: input.rail,
|
|
1685
2042
|
eventType: "merchant_retry_rejected_after_payment",
|
|
1686
2043
|
txHash: input.txHash,
|
|
1687
|
-
reason: `Merchant returned HTTP ${input.
|
|
2044
|
+
reason: `Merchant returned HTTP ${input.merchant.merchant_status} after Haven payment confirmation`,
|
|
1688
2045
|
details: {
|
|
1689
2046
|
resource_url: input.resourceUrl,
|
|
1690
|
-
retry_status: input.
|
|
1691
|
-
retry_body:
|
|
2047
|
+
retry_status: input.merchant.merchant_status,
|
|
2048
|
+
retry_body: input.merchant.merchant_body.slice(0, MERCHANT_BODY_SNIPPET_LIMIT) || null,
|
|
1692
2049
|
...input.details
|
|
1693
2050
|
}
|
|
1694
2051
|
});
|
|
@@ -1714,6 +2071,29 @@ var HavenClient = class {
|
|
|
1714
2071
|
} catch {
|
|
1715
2072
|
}
|
|
1716
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Wait for a funding tx to be mined with ≥1 confirmation before the
|
|
2076
|
+
* merchant retry, eliminating the race where the merchant's
|
|
2077
|
+
* `balanceOf(delegate)` runs before the funding block propagates.
|
|
2078
|
+
*
|
|
2079
|
+
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2080
|
+
* backend has already confirmed on-chain submission and callers accept the
|
|
2081
|
+
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2082
|
+
*/
|
|
2083
|
+
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2084
|
+
if (!txHash || !chainId) return;
|
|
2085
|
+
const rpcUrl = this.chainRpcs[chainId];
|
|
2086
|
+
if (!rpcUrl) return;
|
|
2087
|
+
const provider = createJsonRpcProvider(rpcUrl);
|
|
2088
|
+
const onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2089
|
+
if (!onChainReceipt || onChainReceipt.status !== 1) {
|
|
2090
|
+
throw new HavenApiError(
|
|
2091
|
+
"Funding tx did not confirm on-chain within the timeout window.",
|
|
2092
|
+
500,
|
|
2093
|
+
{ txHash, chainId }
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1717
2097
|
throwIfNonSignableAuthorizationState(label, raw) {
|
|
1718
2098
|
if (raw.status === "pending_signature") return;
|
|
1719
2099
|
this.throwPaymentStateError(label, raw);
|
|
@@ -2299,7 +2679,7 @@ function sleep(ms) {
|
|
|
2299
2679
|
}
|
|
2300
2680
|
function getPaymentHeaderValidBefore(paymentHeader) {
|
|
2301
2681
|
try {
|
|
2302
|
-
const payment =
|
|
2682
|
+
const payment = decodeBase64Json(
|
|
2303
2683
|
paymentHeader
|
|
2304
2684
|
);
|
|
2305
2685
|
const payload = payment.payload;
|
|
@@ -2309,12 +2689,9 @@ function getPaymentHeaderValidBefore(paymentHeader) {
|
|
|
2309
2689
|
}
|
|
2310
2690
|
return 0;
|
|
2311
2691
|
}
|
|
2312
|
-
function decodeBase64Json3(value) {
|
|
2313
|
-
return JSON.parse(atob(value));
|
|
2314
|
-
}
|
|
2315
2692
|
function parseProtocolReceiptHeader(value) {
|
|
2316
2693
|
try {
|
|
2317
|
-
return
|
|
2694
|
+
return decodeBase64Json(value);
|
|
2318
2695
|
} catch {
|
|
2319
2696
|
try {
|
|
2320
2697
|
return JSON.parse(value);
|
|
@@ -2323,13 +2700,14 @@ function parseProtocolReceiptHeader(value) {
|
|
|
2323
2700
|
}
|
|
2324
2701
|
}
|
|
2325
2702
|
}
|
|
2326
|
-
async function
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2703
|
+
async function captureMerchantResponse(response) {
|
|
2704
|
+
const merchant_body = await response.text().catch(() => "");
|
|
2705
|
+
return {
|
|
2706
|
+
merchant_status: response.status,
|
|
2707
|
+
merchant_status_text: response.statusText,
|
|
2708
|
+
merchant_headers: Object.fromEntries(response.headers.entries()),
|
|
2709
|
+
merchant_body
|
|
2710
|
+
};
|
|
2333
2711
|
}
|
|
2334
2712
|
|
|
2335
2713
|
// src/tool-descriptions.ts
|
|
@@ -2401,6 +2779,30 @@ var toolDescriptions = {
|
|
|
2401
2779
|
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.",
|
|
2402
2780
|
behavior: "Returns the agent's recent machine-payment receipts ordered by recency. Proof header values are not returned.",
|
|
2403
2781
|
nextActionGuidance: ""
|
|
2782
|
+
},
|
|
2783
|
+
payMcpTool: {
|
|
2784
|
+
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
|
|
2785
|
+
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.",
|
|
2786
|
+
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.",
|
|
2787
|
+
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."
|
|
2788
|
+
},
|
|
2789
|
+
discoverTools: {
|
|
2790
|
+
summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
|
|
2791
|
+
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.",
|
|
2792
|
+
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.",
|
|
2793
|
+
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)."
|
|
2794
|
+
},
|
|
2795
|
+
sweep_delegate: {
|
|
2796
|
+
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
2797
|
+
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.",
|
|
2798
|
+
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.",
|
|
2799
|
+
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."
|
|
2800
|
+
},
|
|
2801
|
+
send: {
|
|
2802
|
+
summary: "Send ETH or USDC directly from the agent's Haven wallet to a recipient address.",
|
|
2803
|
+
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.",
|
|
2804
|
+
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.",
|
|
2805
|
+
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."
|
|
2404
2806
|
}
|
|
2405
2807
|
};
|
|
2406
2808
|
|
|
@@ -2530,6 +2932,12 @@ var GET_ALLOWANCES_DESCRIPTION = composeDescription(toolDescriptions.getAllowanc
|
|
|
2530
2932
|
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.";
|
|
2531
2933
|
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.";
|
|
2532
2934
|
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.";
|
|
2935
|
+
var SWEEP_DELEGATE_DESCRIPTION = composeDescription(toolDescriptions.sweep_delegate);
|
|
2936
|
+
var sweepDelegateSchema = {
|
|
2937
|
+
type: "object",
|
|
2938
|
+
properties: {},
|
|
2939
|
+
required: []
|
|
2940
|
+
};
|
|
2533
2941
|
function claudeTools() {
|
|
2534
2942
|
return [
|
|
2535
2943
|
{
|
|
@@ -2561,6 +2969,11 @@ function claudeTools() {
|
|
|
2561
2969
|
name: "authorize_machine_payment",
|
|
2562
2970
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
2563
2971
|
input_schema: authorizeMachinePaymentSchema
|
|
2972
|
+
},
|
|
2973
|
+
{
|
|
2974
|
+
name: "haven_sweep_delegate",
|
|
2975
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
2976
|
+
input_schema: sweepDelegateSchema
|
|
2564
2977
|
}
|
|
2565
2978
|
];
|
|
2566
2979
|
}
|
|
@@ -2613,6 +3026,14 @@ function openaiTools() {
|
|
|
2613
3026
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
2614
3027
|
parameters: authorizeMachinePaymentSchema
|
|
2615
3028
|
}
|
|
3029
|
+
},
|
|
3030
|
+
{
|
|
3031
|
+
type: "function",
|
|
3032
|
+
function: {
|
|
3033
|
+
name: "haven_sweep_delegate",
|
|
3034
|
+
description: SWEEP_DELEGATE_DESCRIPTION,
|
|
3035
|
+
parameters: sweepDelegateSchema
|
|
3036
|
+
}
|
|
2616
3037
|
}
|
|
2617
3038
|
];
|
|
2618
3039
|
}
|
|
@@ -2623,6 +3044,75 @@ var havenTools = {
|
|
|
2623
3044
|
openai: openaiTools
|
|
2624
3045
|
};
|
|
2625
3046
|
|
|
2626
|
-
|
|
3047
|
+
// src/skill-content.ts
|
|
3048
|
+
var HAVEN_SKILL_MD = `---
|
|
3049
|
+
name: haven-pay
|
|
3050
|
+
description: Pay for things from the user's Haven wallet within their agent rules. Use when the user asks to send, pay, tip, or transfer crypto \u2014 or when a request hits an HTTP 402 (x402) paywall.
|
|
3051
|
+
---
|
|
3052
|
+
|
|
3053
|
+
# Haven: pay from a Haven wallet
|
|
3054
|
+
|
|
3055
|
+
This skill lets the agent make payments from the user's Haven wallet through
|
|
3056
|
+
the Haven MCP tools. Every payment is checked against the agent's on-chain
|
|
3057
|
+
budget before money moves; payments above the remaining budget wait for the
|
|
3058
|
+
user's approval in Haven.
|
|
3059
|
+
|
|
3060
|
+
## When to use this skill
|
|
3061
|
+
|
|
3062
|
+
- The user asks to send money, pay someone, tip, donate, or transfer tokens.
|
|
3063
|
+
- A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
|
|
3064
|
+
then retry the original request.
|
|
3065
|
+
|
|
3066
|
+
## Identity and budget come from the tools \u2014 never assume them
|
|
3067
|
+
|
|
3068
|
+
Do not guess the wallet address, network, or budget. Read them live:
|
|
3069
|
+
|
|
3070
|
+
- \`haven_get_agent\` \u2014 agent identity, Haven wallet address, network.
|
|
3071
|
+
- \`haven_get_allowances\` \u2014 current per-token budgets and what remains.
|
|
3072
|
+
|
|
3073
|
+
Budgets reset on a period the user chose. If a payment exceeds the remaining
|
|
3074
|
+
budget it is queued for the user to approve in the Haven dashboard \u2014 this is
|
|
3075
|
+
normal, not an error.
|
|
3076
|
+
|
|
3077
|
+
## Paying
|
|
3078
|
+
|
|
3079
|
+
- **Direct transfer:** \`haven_pay\` with recipient, amount, and token.
|
|
3080
|
+
- **x402 paywall:** \`haven_quote_x402\` to get a quote, then
|
|
3081
|
+
\`haven_pay_x402_quote\`. In the hosted setup the signing step happens in
|
|
3082
|
+
the local Haven signer; follow the tool results \u2014 they tell you the next
|
|
3083
|
+
action at every step. Retry the original request only when the result says
|
|
3084
|
+
\`retry_original_x402_request\`.
|
|
3085
|
+
- **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
|
|
3086
|
+
queued or in-flight payments. Do not poll in a tight loop.
|
|
3087
|
+
|
|
3088
|
+
## Approval semantics
|
|
3089
|
+
|
|
3090
|
+
- A result with \`pending_approval\` means the payment exceeded the remaining
|
|
3091
|
+
budget and is waiting for the user in Haven. Tell the user, then check
|
|
3092
|
+
status later.
|
|
3093
|
+
- Never ask the user for private keys and never try to sign anything
|
|
3094
|
+
yourself \u2014 Haven signs. If a tool reports a missing or invalid credential,
|
|
3095
|
+
tell the user to re-run the Haven setup command.
|
|
3096
|
+
|
|
3097
|
+
## Failure handling
|
|
3098
|
+
|
|
3099
|
+
Haven errors are shaped \`{ error, status, details? }\` and written for
|
|
3100
|
+
humans \u2014 surface the message verbatim. Common cases:
|
|
3101
|
+
|
|
3102
|
+
- \`pending_approval\`: queued for the user's approval (see above).
|
|
3103
|
+
- \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
|
|
3104
|
+
Suggest the user add funds in the Haven dashboard.
|
|
3105
|
+
- Budget exceeded: tell the user how much remains (from
|
|
3106
|
+
\`haven_get_allowances\`) and that they can raise the budget in Haven.
|
|
3107
|
+
|
|
3108
|
+
## Revoke
|
|
3109
|
+
|
|
3110
|
+
If this agent's credential may have leaked, tell the user to pause or revoke
|
|
3111
|
+
the agent in the Haven dashboard under Agents. New requests stop immediately
|
|
3112
|
+
for that credential.
|
|
3113
|
+
`;
|
|
3114
|
+
var SKILL_FOLDER_NAME = "haven-pay";
|
|
3115
|
+
|
|
3116
|
+
export { AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, HAVEN_SKILL_MD, HavenApiError, HavenClient, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, SKILL_FOLDER_NAME, addressFromKey, buildMachinePaymentIdempotencyKey, buildX402ExpectedMessage, composeDescription, decodeBase64Json, decodeBase64Utf8, encodeBase64Json, encodeBase64Utf8, encodeMachinePaymentProof, encodePaymentProof, havenTools, parseMachinePaymentChallenge, parseMachinePaymentChallengeResponse, parsePaymentRequired, parsePaymentRequiredResponse, selectPaymentOption, selectStandardPaymentOption, signHash, toStandardPaymentRequirements, toolDescriptions, verifySignature, x402AuthorizationAmount };
|
|
2627
3117
|
//# sourceMappingURL=index.js.map
|
|
2628
3118
|
//# sourceMappingURL=index.js.map
|