@haven_ai/sdk 0.1.13-alpha.0 → 0.1.15-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/README.md +18 -0
- package/dist/index.cjs +354 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -4
- package/dist/index.d.ts +240 -4
- package/dist/index.js +344 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,6 +229,8 @@ The enum values and JSON Schema fragments are exported from `@haven_ai/sdk`:
|
|
|
229
229
|
import {
|
|
230
230
|
AgentPaymentNextAction,
|
|
231
231
|
AgentPaymentNextActionSchema,
|
|
232
|
+
AgentPaymentFailureCode,
|
|
233
|
+
AgentPaymentFailureCodeSchema,
|
|
232
234
|
AgentPaymentPhase,
|
|
233
235
|
AgentPaymentPhaseSchema,
|
|
234
236
|
AgentPaymentRail,
|
|
@@ -277,6 +279,10 @@ Terminal from any non-confirmed phase:
|
|
|
277
279
|
rejected → stop_and_tell_user
|
|
278
280
|
failed → stop_and_tell_user
|
|
279
281
|
expired → request_again_if_user_still_wants_it
|
|
282
|
+
|
|
283
|
+
x402 tool-window failures:
|
|
284
|
+
expired funding/quote window → PAYMENT_WINDOW_EXPIRED → re-quote with same idempotency_key
|
|
285
|
+
merchant rejection after funding → MERCHANT_REJECTED_AFTER_FUNDING → haven_sweep_delegate
|
|
280
286
|
```
|
|
281
287
|
|
|
282
288
|
### `phase` reference
|
|
@@ -308,6 +314,18 @@ The merchant settlement leg of x402 (and the MPP retry) is the agent's own reque
|
|
|
308
314
|
| `retry_original_x402_request` | Resume this payment id and retry the original x402 request with the merchant payment header. Do not start a new merchant session. |
|
|
309
315
|
| `stop_and_tell_user` | Stop retrying and tell the user the payment failed or was rejected. |
|
|
310
316
|
| `request_again_if_user_still_wants_it` | The request expired; ask again only if the user still wants the payment. |
|
|
317
|
+
| `payment_window_expired` | The x402 funding/quote window expired. Re-quote the same paid MCP tool call with the same `idempotency_key`, then sign the fresh `payload_hash`. |
|
|
318
|
+
| `sweep_stranded_funds` | A funding leg succeeded but the merchant/protocol leg did not settle. Stop retrying and use `haven_sweep_delegate` to recover stranded delegate funds. |
|
|
319
|
+
|
|
320
|
+
### Machine-readable recovery codes
|
|
321
|
+
|
|
322
|
+
Hosted MCP and signer tools also return stable `code` values on recoverable x402 failures:
|
|
323
|
+
|
|
324
|
+
| `code` | Meaning | Agent recovery |
|
|
325
|
+
|--------|---------|----------------|
|
|
326
|
+
| `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's `max_amount` cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher `max_amount`. |
|
|
327
|
+
| `PAYMENT_WINDOW_EXPIRED` | The funding/quote window closed before `haven_x402_sign_header`, `haven_submit`, or `haven_complete_mcp_tool` could finish. | Re-run `haven_pay_mcp_tool` with the same `idempotency_key`, then sign and complete the fresh quote. Payloads include `retry_with_new_quote: true`. |
|
|
328
|
+
| `MERCHANT_REJECTED_AFTER_FUNDING` | Haven's funding leg succeeded, but the merchant rejected the paid retry. | Stop retrying the merchant and call `haven_sweep_delegate` so the user can recover stranded delegate USDC. |
|
|
311
329
|
|
|
312
330
|
## Payments above the on-chain allowance
|
|
313
331
|
|
package/dist/index.cjs
CHANGED
|
@@ -63,6 +63,11 @@ var AgentPaymentNextAction = {
|
|
|
63
63
|
StopAndTellUser: "stop_and_tell_user",
|
|
64
64
|
/** Ask again only if the user still wants the payment after expiry. */
|
|
65
65
|
RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
|
|
66
|
+
/**
|
|
67
|
+
* The x402 funding/quote window expired. Re-quote the same logical merchant
|
|
68
|
+
* operation with the same idempotency key to stay double-charge-safe.
|
|
69
|
+
*/
|
|
70
|
+
PaymentWindowExpired: "payment_window_expired",
|
|
66
71
|
/**
|
|
67
72
|
* Stop and tell the user that the originating Safe needs to be funded or
|
|
68
73
|
* the agent's per-token allowance needs to be raised before the payment
|
|
@@ -76,6 +81,14 @@ var AgentPaymentNextAction = {
|
|
|
76
81
|
*/
|
|
77
82
|
SweepStrandedFunds: "sweep_stranded_funds"
|
|
78
83
|
};
|
|
84
|
+
var AgentPaymentFailureCode = {
|
|
85
|
+
/** A merchant-authoritative x402 price exceeds the caller's pre-funding max_amount cap. */
|
|
86
|
+
PriceExceedsMax: "PRICE_EXCEEDS_MAX",
|
|
87
|
+
/** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
|
|
88
|
+
PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
|
|
89
|
+
/** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
|
|
90
|
+
MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING"
|
|
91
|
+
};
|
|
79
92
|
var AgentPaymentRail = {
|
|
80
93
|
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
81
94
|
Direct: "direct",
|
|
@@ -94,6 +107,7 @@ var AgentPaymentRail = {
|
|
|
94
107
|
};
|
|
95
108
|
var AGENT_PAYMENT_PHASE_VALUES = Object.values(AgentPaymentPhase);
|
|
96
109
|
var AGENT_PAYMENT_NEXT_ACTION_VALUES = Object.values(AgentPaymentNextAction);
|
|
110
|
+
var AGENT_PAYMENT_FAILURE_CODE_VALUES = Object.values(AgentPaymentFailureCode);
|
|
97
111
|
var AGENT_PAYMENT_RAIL_VALUES = Object.values(AgentPaymentRail);
|
|
98
112
|
var AgentPaymentPhaseDescriptions = {
|
|
99
113
|
[AgentPaymentPhase.AgentSignatureRequired]: "The agent must sign and submit the prepared payment before Haven can relay it.",
|
|
@@ -118,9 +132,15 @@ var AgentPaymentNextActionDescriptions = {
|
|
|
118
132
|
[AgentPaymentNextAction.RetryOriginalX402Request]: "Resume this payment id and retry the original x402 request with the merchant payment header.",
|
|
119
133
|
[AgentPaymentNextAction.StopAndTellUser]: "Stop retrying this payment and tell the user what happened.",
|
|
120
134
|
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
|
|
135
|
+
[AgentPaymentNextAction.PaymentWindowExpired]: "The x402 funding/quote window expired. Re-quote with the same idempotency key before asking the signer to build a merchant payment header again.",
|
|
121
136
|
[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
137
|
[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."
|
|
123
138
|
};
|
|
139
|
+
var AgentPaymentFailureCodeDescriptions = {
|
|
140
|
+
[AgentPaymentFailureCode.PriceExceedsMax]: "The merchant-authoritative x402 amount exceeds the caller's max_amount cap. No funding transfer was created; ask the user before retrying with a larger cap.",
|
|
141
|
+
[AgentPaymentFailureCode.PaymentWindowExpired]: "The x402 funding/quote window expired before the signer or hosted settle step could finish. Re-quote via haven_pay_mcp_tool with the same idempotency key to avoid duplicate funding.",
|
|
142
|
+
[AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The Haven funding leg succeeded, but the merchant rejected the paid retry. Stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate."
|
|
143
|
+
};
|
|
124
144
|
var AgentPaymentRailDescriptions = {
|
|
125
145
|
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
|
|
126
146
|
[AgentPaymentRail.X402]: "x402 HTTP 402 payment flow with a Haven funding leg and merchant retry leg.",
|
|
@@ -142,6 +162,12 @@ var AgentPaymentNextActionSchema = {
|
|
|
142
162
|
description: "Stable next action an agent should take for a Haven payment state.",
|
|
143
163
|
"x-enumDescriptions": AgentPaymentNextActionDescriptions
|
|
144
164
|
};
|
|
165
|
+
var AgentPaymentFailureCodeSchema = {
|
|
166
|
+
type: "string",
|
|
167
|
+
enum: AGENT_PAYMENT_FAILURE_CODE_VALUES,
|
|
168
|
+
description: "Stable machine-readable failure codes for Haven agent payment recovery paths.",
|
|
169
|
+
"x-enumDescriptions": AgentPaymentFailureCodeDescriptions
|
|
170
|
+
};
|
|
145
171
|
var AgentPaymentRailSchema = {
|
|
146
172
|
type: "string",
|
|
147
173
|
enum: AGENT_PAYMENT_RAIL_VALUES,
|
|
@@ -329,7 +355,8 @@ function normalizePaymentRequired(value) {
|
|
|
329
355
|
x402Version: candidate.x402Version,
|
|
330
356
|
resource,
|
|
331
357
|
accepts,
|
|
332
|
-
error: candidate.error
|
|
358
|
+
error: candidate.error,
|
|
359
|
+
...candidate.extensions && typeof candidate.extensions === "object" ? { extensions: candidate.extensions } : {}
|
|
333
360
|
};
|
|
334
361
|
}
|
|
335
362
|
var SUPPORTED_X402_NETWORKS = {
|
|
@@ -425,8 +452,7 @@ function x402AuthorizationAmount(option) {
|
|
|
425
452
|
return amount;
|
|
426
453
|
}
|
|
427
454
|
function buildX402ExpectedMessage(context) {
|
|
428
|
-
|
|
429
|
-
${stableStringify({
|
|
455
|
+
const payload = {
|
|
430
456
|
version: 1,
|
|
431
457
|
kind: "haven.x402.expected",
|
|
432
458
|
paymentId: context.paymentId,
|
|
@@ -436,7 +462,12 @@ ${stableStringify({
|
|
|
436
462
|
amount: context.amount,
|
|
437
463
|
asset: context.asset.toLowerCase(),
|
|
438
464
|
network: context.network
|
|
439
|
-
}
|
|
465
|
+
};
|
|
466
|
+
if (context.expiresAt) {
|
|
467
|
+
payload.expiresAt = context.expiresAt;
|
|
468
|
+
}
|
|
469
|
+
return `Haven x402 expected context v1
|
|
470
|
+
${stableStringify(payload)}`;
|
|
440
471
|
}
|
|
441
472
|
function toStandardPaymentRequirements(paymentRequired, option) {
|
|
442
473
|
const network = STANDARD_X402_NETWORKS[option.network];
|
|
@@ -892,6 +923,7 @@ var HavenClient = class {
|
|
|
892
923
|
}
|
|
893
924
|
return {
|
|
894
925
|
paymentId: raw.payment_id,
|
|
926
|
+
idempotencyKey,
|
|
895
927
|
status: "pending_signature",
|
|
896
928
|
expiresAt: raw.expires_at,
|
|
897
929
|
signData: raw.sign_data,
|
|
@@ -1043,6 +1075,30 @@ var HavenClient = class {
|
|
|
1043
1075
|
transfers
|
|
1044
1076
|
};
|
|
1045
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Hosted (keyless) split-signer sweep — step 1 of 2.
|
|
1080
|
+
*
|
|
1081
|
+
* Asks the backend to build a gasless EIP-3009 sweep authorization for the
|
|
1082
|
+
* delegate's stranded USDC. Returns `nothing_stranded` when the delegate is
|
|
1083
|
+
* empty, otherwise an `authorization` + Haven `expected_auth` to hand to the
|
|
1084
|
+
* edge signer's `haven_sign_sweep_delegate`. No key is required on this client.
|
|
1085
|
+
*/
|
|
1086
|
+
async prepareSweep() {
|
|
1087
|
+
return this.post("/machine-payments/sweep/prepare", {});
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Hosted (keyless) split-signer sweep — step 2 of 2.
|
|
1091
|
+
*
|
|
1092
|
+
* Relays the delegate-signed authorization. The Haven relayer submits the
|
|
1093
|
+
* on-chain `transferWithAuthorization` and pays gas; this client never holds
|
|
1094
|
+
* the key.
|
|
1095
|
+
*/
|
|
1096
|
+
async submitSweep(authorization, signature) {
|
|
1097
|
+
return this.post("/machine-payments/sweep/submit", {
|
|
1098
|
+
authorization,
|
|
1099
|
+
signature
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1046
1102
|
/**
|
|
1047
1103
|
* Get configured and on-chain allowances for the authenticated agent.
|
|
1048
1104
|
*/
|
|
@@ -1198,7 +1254,8 @@ var HavenClient = class {
|
|
|
1198
1254
|
throw new HavenApiError("quoteX402 only supports standard x402 Payment Required responses.", 400);
|
|
1199
1255
|
}
|
|
1200
1256
|
const paymentRequired = await parsePaymentRequiredResponse(response);
|
|
1201
|
-
|
|
1257
|
+
const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
|
|
1258
|
+
return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
|
|
1202
1259
|
}
|
|
1203
1260
|
/**
|
|
1204
1261
|
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
@@ -1403,12 +1460,11 @@ var HavenClient = class {
|
|
|
1403
1460
|
* a transport/HTTP error, a missing session id, or a JSON-RPC error in the
|
|
1404
1461
|
* handshake response — so the caller can fall back to plain x402.
|
|
1405
1462
|
*/
|
|
1406
|
-
async mcpInitialize(url, init) {
|
|
1463
|
+
async mcpInitialize(url, init, wallet = this.x402PayerAddress()) {
|
|
1407
1464
|
try {
|
|
1408
1465
|
const headers = new Headers(init?.headers);
|
|
1409
1466
|
headers.set("Content-Type", "application/json");
|
|
1410
1467
|
headers.set("Accept", MCP_ACCEPT);
|
|
1411
|
-
const wallet = this.x402PayerAddress();
|
|
1412
1468
|
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1413
1469
|
const response = await globalThis.fetch(url, {
|
|
1414
1470
|
method: "POST",
|
|
@@ -1429,7 +1485,7 @@ var HavenClient = class {
|
|
|
1429
1485
|
if (!sessionId) return void 0;
|
|
1430
1486
|
const message = await this.readMcpMessage(response);
|
|
1431
1487
|
if (message && "error" in message) return void 0;
|
|
1432
|
-
await this.mcpNotifyInitialized(url, init, sessionId);
|
|
1488
|
+
await this.mcpNotifyInitialized(url, init, sessionId, wallet);
|
|
1433
1489
|
return sessionId;
|
|
1434
1490
|
} catch {
|
|
1435
1491
|
return void 0;
|
|
@@ -1440,13 +1496,12 @@ var HavenClient = class {
|
|
|
1440
1496
|
* lifecycle handshake. Best-effort: the session is already established, so a
|
|
1441
1497
|
* failed notification must not abort the payment.
|
|
1442
1498
|
*/
|
|
1443
|
-
async mcpNotifyInitialized(url, init, sessionId) {
|
|
1499
|
+
async mcpNotifyInitialized(url, init, sessionId, wallet = this.x402PayerAddress()) {
|
|
1444
1500
|
try {
|
|
1445
1501
|
const headers = new Headers(init?.headers);
|
|
1446
1502
|
headers.set("Content-Type", "application/json");
|
|
1447
1503
|
headers.set("Accept", MCP_ACCEPT);
|
|
1448
1504
|
headers.set("mcp-session-id", sessionId);
|
|
1449
|
-
const wallet = this.x402PayerAddress();
|
|
1450
1505
|
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1451
1506
|
await globalThis.fetch(url, {
|
|
1452
1507
|
method: "POST",
|
|
@@ -1614,6 +1669,133 @@ var HavenClient = class {
|
|
|
1614
1669
|
});
|
|
1615
1670
|
return retryResponse;
|
|
1616
1671
|
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Deliver an already-signed x402 payment header to the merchant and return
|
|
1674
|
+
* the merchant's response. Used by the hosted MCP server to complete the
|
|
1675
|
+
* merchant leg of an MCP tool payment after the edge signer has built the
|
|
1676
|
+
* `X-PAYMENT` header.
|
|
1677
|
+
*
|
|
1678
|
+
* Custody note: this never needs the delegate key. It relays a signed,
|
|
1679
|
+
* amount/merchant/nonce-bound EIP-3009 authorization the edge signer already
|
|
1680
|
+
* produced — the hosted server cannot mint or reuse signing authority.
|
|
1681
|
+
*
|
|
1682
|
+
* When the URL is MCP-shaped (`/mcp` path) or the quote-time transport context
|
|
1683
|
+
* says the merchant was Bazaar-discoverable, runs a fresh `initialize`
|
|
1684
|
+
* handshake (the quote-time session is gone once funding confirms; the x402
|
|
1685
|
+
* challenge is stateless w.r.t. the MCP session, so a fresh session is
|
|
1686
|
+
* accepted), threads the session + wallet headers, sets `X-PAYMENT`, and
|
|
1687
|
+
* collapses an SSE JSON-RPC response to its `result`.
|
|
1688
|
+
*/
|
|
1689
|
+
async completeX402MerchantCall(input) {
|
|
1690
|
+
const evidenceContext = await this.resolveX402MerchantCompletionContext({
|
|
1691
|
+
paymentId: input.paymentId,
|
|
1692
|
+
url: input.url
|
|
1693
|
+
});
|
|
1694
|
+
const shouldHandshakeMcp = isMcpUrl(input.url) || input.mcpTransport?.handshakeRequired === true;
|
|
1695
|
+
const x402Wallet = shouldHandshakeMcp ? await this.resolveX402WalletForMerchantCall() : this.x402PayerAddress();
|
|
1696
|
+
let mcpSessionId;
|
|
1697
|
+
if (shouldHandshakeMcp) {
|
|
1698
|
+
mcpSessionId = await this.mcpInitialize(input.url, input.init, x402Wallet);
|
|
1699
|
+
}
|
|
1700
|
+
let requestInit = this.withX402Wallet(input.init, x402Wallet) ?? {};
|
|
1701
|
+
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1702
|
+
const headers = new Headers(requestInit.headers);
|
|
1703
|
+
headers.set("X-PAYMENT", input.paymentHeader);
|
|
1704
|
+
requestInit = { ...requestInit, headers };
|
|
1705
|
+
const response = await globalThis.fetch(input.url, requestInit);
|
|
1706
|
+
const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
|
|
1707
|
+
const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
1708
|
+
const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
|
|
1709
|
+
const text = await surfaced.text();
|
|
1710
|
+
let body;
|
|
1711
|
+
try {
|
|
1712
|
+
body = text ? JSON.parse(text) : null;
|
|
1713
|
+
} catch {
|
|
1714
|
+
body = text;
|
|
1715
|
+
}
|
|
1716
|
+
if (!surfaced.ok) {
|
|
1717
|
+
await this.recordMerchantRetryRejected({
|
|
1718
|
+
rail: "x402",
|
|
1719
|
+
paymentId: evidenceContext.paymentId,
|
|
1720
|
+
txHash: evidenceContext.txHash,
|
|
1721
|
+
resourceUrl: evidenceContext.resourceUrl,
|
|
1722
|
+
merchant: {
|
|
1723
|
+
merchant_status: surfaced.status,
|
|
1724
|
+
merchant_status_text: surfaced.statusText,
|
|
1725
|
+
merchant_headers: Object.fromEntries(surfaced.headers.entries()),
|
|
1726
|
+
merchant_body: text
|
|
1727
|
+
},
|
|
1728
|
+
details: {
|
|
1729
|
+
merchant_to: evidenceContext.merchantAddress
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
} else {
|
|
1733
|
+
await this.reportMachinePaymentEvidence({
|
|
1734
|
+
paymentId: evidenceContext.paymentId,
|
|
1735
|
+
rail: "x402",
|
|
1736
|
+
txHash: evidenceContext.txHash,
|
|
1737
|
+
resourceUrl: evidenceContext.resourceUrl,
|
|
1738
|
+
merchantStatus: surfaced.status,
|
|
1739
|
+
paymentProofHeaderName: "X-PAYMENT",
|
|
1740
|
+
paymentProofHeader: input.paymentHeader,
|
|
1741
|
+
protocolReceiptHeaderName: protocolReceiptHeader ? "PAYMENT-RESPONSE" : void 0,
|
|
1742
|
+
protocolReceiptHeader
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return {
|
|
1746
|
+
status: surfaced.status,
|
|
1747
|
+
ok: surfaced.ok,
|
|
1748
|
+
body,
|
|
1749
|
+
settlementTxHash: settlement.settlementTxHash ?? void 0
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
async resolveX402MerchantCompletionContext(input) {
|
|
1753
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
1754
|
+
if (status.rail !== "x402") {
|
|
1755
|
+
throw new HavenPaymentStateError(
|
|
1756
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
1757
|
+
409,
|
|
1758
|
+
status
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
const readyForMerchantCompletion = status.nextAction === AgentPaymentNextAction.RetryOriginalX402Request || status.kind === "payment_intent" && status.status === "confirmed" && status.phase === AgentPaymentPhase.PaymentConfirmed && status.nextAction === AgentPaymentNextAction.None;
|
|
1762
|
+
if (!readyForMerchantCompletion) {
|
|
1763
|
+
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
1764
|
+
}
|
|
1765
|
+
if (!status.txHash) {
|
|
1766
|
+
throw new HavenApiError(
|
|
1767
|
+
`x402 payment ${status.paymentId} is ready for merchant completion but has no Haven transaction hash.`,
|
|
1768
|
+
502,
|
|
1769
|
+
status,
|
|
1770
|
+
status.paymentId
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const approvedResourceUrl = status.resourceUrl ?? status.x402?.resourceUrl ?? null;
|
|
1774
|
+
if (approvedResourceUrl && approvedResourceUrl !== input.url) {
|
|
1775
|
+
throw new HavenApiError(
|
|
1776
|
+
"x402 merchant completion does not match the approved resource URL.",
|
|
1777
|
+
409,
|
|
1778
|
+
{ status, url: input.url },
|
|
1779
|
+
status.paymentId
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
paymentId: status.paymentId,
|
|
1784
|
+
txHash: status.txHash,
|
|
1785
|
+
resourceUrl: approvedResourceUrl ?? input.url,
|
|
1786
|
+
merchantAddress: status.merchantAddress ?? status.x402?.merchantAddress ?? null
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
async resolveX402WalletForMerchantCall() {
|
|
1790
|
+
const localWallet = this.x402PayerAddress();
|
|
1791
|
+
if (localWallet) return localWallet;
|
|
1792
|
+
try {
|
|
1793
|
+
const agent = await this.getAgent();
|
|
1794
|
+
return agent.delegateAddress ?? void 0;
|
|
1795
|
+
} catch {
|
|
1796
|
+
return void 0;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1617
1799
|
async authorizeMachinePayment(challenge, options = {}) {
|
|
1618
1800
|
if (!this.delegateKey) {
|
|
1619
1801
|
throw new HavenSigningError(
|
|
@@ -2211,7 +2393,7 @@ var HavenClient = class {
|
|
|
2211
2393
|
headers
|
|
2212
2394
|
};
|
|
2213
2395
|
}
|
|
2214
|
-
buildX402Quote(paymentRequired, request, idempotencyKey) {
|
|
2396
|
+
buildX402Quote(paymentRequired, request, idempotencyKey, mcpTransport) {
|
|
2215
2397
|
const option = selectStandardPaymentOption(paymentRequired.accepts);
|
|
2216
2398
|
if (!option) {
|
|
2217
2399
|
throw new HavenApiError(
|
|
@@ -2226,6 +2408,7 @@ var HavenClient = class {
|
|
|
2226
2408
|
paymentRequired,
|
|
2227
2409
|
accepted: option,
|
|
2228
2410
|
request,
|
|
2411
|
+
...mcpTransport ? { mcpTransport } : {},
|
|
2229
2412
|
resourceUrl: paymentRequired.resource.url,
|
|
2230
2413
|
description: paymentRequired.resource.description ?? option.description ?? null,
|
|
2231
2414
|
mimeType: paymentRequired.resource.mimeType ?? option.mimeType ?? null,
|
|
@@ -2239,6 +2422,18 @@ var HavenClient = class {
|
|
|
2239
2422
|
maxTimeoutSeconds: option.maxTimeoutSeconds
|
|
2240
2423
|
};
|
|
2241
2424
|
}
|
|
2425
|
+
async detectX402McpTransport(url, paymentRequired, response) {
|
|
2426
|
+
if (isMcpUrl(url)) {
|
|
2427
|
+
return { handshakeRequired: true, source: "path" };
|
|
2428
|
+
}
|
|
2429
|
+
if (paymentRequired.extensions?.bazaar != null) {
|
|
2430
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
2431
|
+
}
|
|
2432
|
+
if (await responseHasBazaarExtension(response)) {
|
|
2433
|
+
return { handshakeRequired: true, source: "bazaar" };
|
|
2434
|
+
}
|
|
2435
|
+
return void 0;
|
|
2436
|
+
}
|
|
2242
2437
|
buildX402ResumeState(input) {
|
|
2243
2438
|
const token = resolveTokenFromAddress(input.accepted.asset, input.accepted.network);
|
|
2244
2439
|
return {
|
|
@@ -2791,8 +2986,8 @@ var toolDescriptions = {
|
|
|
2791
2986
|
discoverTools: {
|
|
2792
2987
|
summary: "Discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use.",
|
|
2793
2988
|
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
|
|
2989
|
+
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. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
|
|
2990
|
+
nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url (and tool_name for MCP merchants). Confirm the price from the live pay-tool result (not the catalog), and pass max_amount when the user has a cap."
|
|
2796
2991
|
},
|
|
2797
2992
|
sweep_delegate: {
|
|
2798
2993
|
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
@@ -3059,6 +3254,9 @@ the Haven MCP tools. Every payment is checked against the agent's on-chain
|
|
|
3059
3254
|
budget before money moves; payments above the remaining budget wait for the
|
|
3060
3255
|
user's approval in Haven.
|
|
3061
3256
|
|
|
3257
|
+
Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
|
|
3258
|
+
the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
|
|
3259
|
+
|
|
3062
3260
|
## When to use this skill
|
|
3063
3261
|
|
|
3064
3262
|
- The user asks to send money, pay someone, tip, donate, or transfer tokens.
|
|
@@ -3084,6 +3282,33 @@ normal, not an error.
|
|
|
3084
3282
|
the local Haven signer; follow the tool results \u2014 they tell you the next
|
|
3085
3283
|
action at every step. Retry the original request only when the result says
|
|
3086
3284
|
\`retry_original_x402_request\`.
|
|
3285
|
+
- **Paid MCP tool call:** \`mcp__haven__haven_pay_mcp_tool\` with the merchant
|
|
3286
|
+
URL, tool name, and arguments, then finish in two calls (fast path):
|
|
3287
|
+
\`mcp__haven-signer__haven_sign_x402\` on the local signer (pass
|
|
3288
|
+
\`payload_hash\`, \`x402_expected\` as the nested \`x402.expected\` object, and
|
|
3289
|
+
\`payment_required\`) returns \`{ signature, payment_header }\`; then
|
|
3290
|
+
\`mcp__haven__haven_settle_mcp_tool\` (pass \`payment_id\`, \`signature\`,
|
|
3291
|
+
\`payment_header\`, \`merchant_url\`, \`tool_name\`, \`arguments\`,
|
|
3292
|
+
\`mcp_transport\`) funds and settles in one step and returns the tool result.
|
|
3293
|
+
If it returns \`settled: false\`, funding is queued for the user's approval \u2014
|
|
3294
|
+
tell them and check status later, do not re-pay. Step-by-step alternative:
|
|
3295
|
+
\`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
|
|
3296
|
+
\`mcp__haven-signer__haven_x402_sign_header\` \u2192
|
|
3297
|
+
\`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
|
|
3298
|
+
\`arguments\`, and \`mcp_transport\` verbatim from the
|
|
3299
|
+
\`mcp__haven__haven_pay_mcp_tool\` result. The returned \`expires_at\` is the
|
|
3300
|
+
signing window; if a tool returns \`PAYMENT_WINDOW_EXPIRED\`, re-run
|
|
3301
|
+
\`mcp__haven__haven_pay_mcp_tool\` with the same
|
|
3302
|
+
\`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
|
|
3303
|
+
merchant leg for you.
|
|
3304
|
+
- **Prices:** show the user the live price from the pay-tool result, never a
|
|
3305
|
+
catalog price. \`haven_discover_tools\` prices are indicative
|
|
3306
|
+
(\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
|
|
3307
|
+
\`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
|
|
3308
|
+
merchant settles at or below \u2014 so present it as the most the user will pay.
|
|
3309
|
+
Pass \`max_amount\` (atomic units) to \`haven_pay_mcp_tool\` /
|
|
3310
|
+
\`haven_pay_x402_quote\` to reject a quote whose authorized amount is above the
|
|
3311
|
+
user's cap, before any funds move.
|
|
3087
3312
|
- **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
|
|
3088
3313
|
queued or in-flight payments. Do not poll in a tight loop.
|
|
3089
3314
|
|
|
@@ -3092,18 +3317,26 @@ normal, not an error.
|
|
|
3092
3317
|
- A result with \`pending_approval\` means the payment exceeded the remaining
|
|
3093
3318
|
budget and is waiting for the user in Haven. Tell the user, then check
|
|
3094
3319
|
status later.
|
|
3095
|
-
- Never ask the user for private keys
|
|
3096
|
-
|
|
3097
|
-
tell the user to re-run the Haven
|
|
3320
|
+
- Never ask the user for private keys. Signing happens only in the local Haven
|
|
3321
|
+
signer; the hosted Haven tools never receive the signing key. If a tool
|
|
3322
|
+
reports a missing or invalid credential, tell the user to re-run the Haven
|
|
3323
|
+
setup command.
|
|
3098
3324
|
|
|
3099
3325
|
## Failure handling
|
|
3100
3326
|
|
|
3101
|
-
Haven
|
|
3102
|
-
|
|
3327
|
+
Haven tool failures are shaped like \`{ success: false, code, message, ... }\`
|
|
3328
|
+
or older \`{ error, status, details? }\` responses. Branch on \`code\` when
|
|
3329
|
+
present and surface \`message\` or \`error\` verbatim. Common cases:
|
|
3103
3330
|
|
|
3104
3331
|
- \`pending_approval\`: queued for the user's approval (see above).
|
|
3105
3332
|
- \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
|
|
3106
3333
|
Suggest the user add funds in the Haven dashboard.
|
|
3334
|
+
- \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
|
|
3335
|
+
No funds moved; ask the user before retrying with a higher cap.
|
|
3336
|
+
- \`PAYMENT_WINDOW_EXPIRED\`: re-run \`mcp__haven__haven_pay_mcp_tool\` with the same
|
|
3337
|
+
\`idempotency_key\`, then sign the fresh \`payload_hash\`.
|
|
3338
|
+
- \`MERCHANT_REJECTED_AFTER_FUNDING\`: stop retrying the merchant and use
|
|
3339
|
+
\`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
|
|
3107
3340
|
- Budget exceeded: tell the user how much remains (from
|
|
3108
3341
|
\`haven_get_allowances\`) and that they can raise the budget in Haven.
|
|
3109
3342
|
|
|
@@ -3115,9 +3348,105 @@ for that credential.
|
|
|
3115
3348
|
`;
|
|
3116
3349
|
var SKILL_FOLDER_NAME = "haven-pay";
|
|
3117
3350
|
|
|
3351
|
+
// src/sweep.ts
|
|
3352
|
+
var SWEEP_BASE_CHAIN_ID = 8453;
|
|
3353
|
+
var SWEEP_BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
3354
|
+
var USDC_EIP712_DOMAIN_BY_CHAIN = {
|
|
3355
|
+
[SWEEP_BASE_CHAIN_ID]: {
|
|
3356
|
+
name: "USD Coin",
|
|
3357
|
+
version: "2",
|
|
3358
|
+
chainId: SWEEP_BASE_CHAIN_ID,
|
|
3359
|
+
verifyingContract: SWEEP_BASE_USDC_ADDRESS
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
var USDC_ADDRESS_BY_CHAIN = {
|
|
3363
|
+
[SWEEP_BASE_CHAIN_ID]: SWEEP_BASE_USDC_ADDRESS
|
|
3364
|
+
};
|
|
3365
|
+
var TRANSFER_WITH_AUTHORIZATION_TYPES = {
|
|
3366
|
+
TransferWithAuthorization: [
|
|
3367
|
+
{ name: "from", type: "address" },
|
|
3368
|
+
{ name: "to", type: "address" },
|
|
3369
|
+
{ name: "value", type: "uint256" },
|
|
3370
|
+
{ name: "validAfter", type: "uint256" },
|
|
3371
|
+
{ name: "validBefore", type: "uint256" },
|
|
3372
|
+
{ name: "nonce", type: "bytes32" }
|
|
3373
|
+
]
|
|
3374
|
+
};
|
|
3375
|
+
function sweepUsdcAddress(chainId) {
|
|
3376
|
+
const address = USDC_ADDRESS_BY_CHAIN[chainId];
|
|
3377
|
+
if (!address) {
|
|
3378
|
+
throw new HavenSigningError(
|
|
3379
|
+
`Sweep is only supported on Base (chainId ${SWEEP_BASE_CHAIN_ID}). Got chainId ${chainId}.`
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
return address;
|
|
3383
|
+
}
|
|
3384
|
+
function sweepUsdcDomain(chainId) {
|
|
3385
|
+
const domain = USDC_EIP712_DOMAIN_BY_CHAIN[chainId];
|
|
3386
|
+
if (!domain) {
|
|
3387
|
+
throw new HavenSigningError(
|
|
3388
|
+
`Sweep is only supported on Base (chainId ${SWEEP_BASE_CHAIN_ID}). Got chainId ${chainId}.`
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
return domain;
|
|
3392
|
+
}
|
|
3393
|
+
function sameAddress2(a, b) {
|
|
3394
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
3395
|
+
}
|
|
3396
|
+
function buildSweepTypedData(auth) {
|
|
3397
|
+
const domain = sweepUsdcDomain(auth.chainId);
|
|
3398
|
+
const expectedToken = sweepUsdcAddress(auth.chainId);
|
|
3399
|
+
if (!sameAddress2(auth.token, expectedToken)) {
|
|
3400
|
+
throw new HavenSigningError(
|
|
3401
|
+
`Sweep token ${auth.token} is not the canonical USDC contract for chain ${auth.chainId}.`
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3404
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(auth.nonce)) {
|
|
3405
|
+
throw new HavenSigningError("Sweep nonce must be a 0x-prefixed 32-byte hex string.");
|
|
3406
|
+
}
|
|
3407
|
+
return {
|
|
3408
|
+
domain,
|
|
3409
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
3410
|
+
primaryType: "TransferWithAuthorization",
|
|
3411
|
+
message: {
|
|
3412
|
+
from: auth.from,
|
|
3413
|
+
to: auth.to,
|
|
3414
|
+
value: BigInt(auth.value),
|
|
3415
|
+
validAfter: BigInt(auth.validAfter),
|
|
3416
|
+
validBefore: BigInt(auth.validBefore),
|
|
3417
|
+
nonce: auth.nonce
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
function buildSweepAuthorizationMessage(auth) {
|
|
3422
|
+
return `Haven sweep authorization v1
|
|
3423
|
+
${stableStringify2({
|
|
3424
|
+
version: 1,
|
|
3425
|
+
kind: "haven.sweep.authorization",
|
|
3426
|
+
from: auth.from.toLowerCase(),
|
|
3427
|
+
to: auth.to.toLowerCase(),
|
|
3428
|
+
value: auth.value,
|
|
3429
|
+
validAfter: auth.validAfter,
|
|
3430
|
+
validBefore: auth.validBefore,
|
|
3431
|
+
nonce: auth.nonce.toLowerCase(),
|
|
3432
|
+
token: auth.token.toLowerCase(),
|
|
3433
|
+
chainId: auth.chainId
|
|
3434
|
+
})}`;
|
|
3435
|
+
}
|
|
3436
|
+
function stableStringify2(value) {
|
|
3437
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
3438
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
3439
|
+
const object = value;
|
|
3440
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
|
|
3118
3444
|
exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
|
|
3119
3445
|
exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
|
|
3120
3446
|
exports.AGENT_PAYMENT_RAIL_VALUES = AGENT_PAYMENT_RAIL_VALUES;
|
|
3447
|
+
exports.AgentPaymentFailureCode = AgentPaymentFailureCode;
|
|
3448
|
+
exports.AgentPaymentFailureCodeDescriptions = AgentPaymentFailureCodeDescriptions;
|
|
3449
|
+
exports.AgentPaymentFailureCodeSchema = AgentPaymentFailureCodeSchema;
|
|
3121
3450
|
exports.AgentPaymentNextAction = AgentPaymentNextAction;
|
|
3122
3451
|
exports.AgentPaymentNextActionDescriptions = AgentPaymentNextActionDescriptions;
|
|
3123
3452
|
exports.AgentPaymentNextActionSchema = AgentPaymentNextActionSchema;
|
|
@@ -3135,8 +3464,13 @@ exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
|
3135
3464
|
exports.HavenSigningError = HavenSigningError;
|
|
3136
3465
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
3137
3466
|
exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
|
|
3467
|
+
exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
|
|
3468
|
+
exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
|
|
3469
|
+
exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
|
|
3138
3470
|
exports.addressFromKey = addressFromKey;
|
|
3139
3471
|
exports.buildMachinePaymentIdempotencyKey = buildMachinePaymentIdempotencyKey;
|
|
3472
|
+
exports.buildSweepAuthorizationMessage = buildSweepAuthorizationMessage;
|
|
3473
|
+
exports.buildSweepTypedData = buildSweepTypedData;
|
|
3140
3474
|
exports.buildX402ExpectedMessage = buildX402ExpectedMessage;
|
|
3141
3475
|
exports.composeDescription = composeDescription;
|
|
3142
3476
|
exports.decodeBase64Json = decodeBase64Json;
|
|
@@ -3153,6 +3487,8 @@ exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
|
3153
3487
|
exports.selectPaymentOption = selectPaymentOption;
|
|
3154
3488
|
exports.selectStandardPaymentOption = selectStandardPaymentOption;
|
|
3155
3489
|
exports.signHash = signHash;
|
|
3490
|
+
exports.sweepUsdcAddress = sweepUsdcAddress;
|
|
3491
|
+
exports.sweepUsdcDomain = sweepUsdcDomain;
|
|
3156
3492
|
exports.toStandardPaymentRequirements = toStandardPaymentRequirements;
|
|
3157
3493
|
exports.toolDescriptions = toolDescriptions;
|
|
3158
3494
|
exports.verifySignature = verifySignature;
|