@haven_ai/sdk 0.1.20-alpha.0 → 0.1.22-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 +479 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +370 -7
- package/dist/index.d.ts +370 -7
- package/dist/index.js +470 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -64,6 +64,8 @@ var AgentPaymentNextAction = {
|
|
|
64
64
|
StopAndTellUser: "stop_and_tell_user",
|
|
65
65
|
/** Ask again only if the user still wants the payment after expiry. */
|
|
66
66
|
RequestAgainIfUserStillWantsIt: "request_again_if_user_still_wants_it",
|
|
67
|
+
/** #1307: retry the SAME tool call, supplying the explicit context fields the server could not rehydrate. */
|
|
68
|
+
RetryWithExplicitContext: "retry_with_explicit_context",
|
|
67
69
|
/**
|
|
68
70
|
* The x402 funding/quote window expired. Re-quote the same logical merchant
|
|
69
71
|
* operation with the same idempotency key to stay double-charge-safe.
|
|
@@ -88,7 +90,21 @@ var AgentPaymentFailureCode = {
|
|
|
88
90
|
/** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
|
|
89
91
|
PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
|
|
90
92
|
/** The Haven funding leg succeeded, but the merchant rejected the paid retry. */
|
|
91
|
-
MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING"
|
|
93
|
+
MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
|
|
94
|
+
/** #1300 review: funding is on-chain but the merchant never ANSWERED the
|
|
95
|
+
* paid retry within the timeout. NOT proof of rejection — the merchant
|
|
96
|
+
* holds a valid EIP-3009 authorization and may still settle late, so the
|
|
97
|
+
* guidance is verify-then-sweep, never blind sweep. */
|
|
98
|
+
MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
|
|
99
|
+
/**
|
|
100
|
+
* #1307: the caller omitted merchant_url/tool_name (asking Haven to
|
|
101
|
+
* rehydrate the stored MCP merchant-call context by payment_id), but no
|
|
102
|
+
* usable context was stored for this intent — either it was never an
|
|
103
|
+
* MCP-tool quote, or the stored context is incomplete. The fallback is
|
|
104
|
+
* mechanical: re-send merchant_url, tool_name, arguments, and
|
|
105
|
+
* mcp_transport explicitly (the version-skew path).
|
|
106
|
+
*/
|
|
107
|
+
MerchantCallContextUnavailable: "MERCHANT_CALL_CONTEXT_UNAVAILABLE"
|
|
92
108
|
};
|
|
93
109
|
var AgentPaymentRail = {
|
|
94
110
|
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
@@ -135,12 +151,46 @@ var AgentPaymentNextActionDescriptions = {
|
|
|
135
151
|
[AgentPaymentNextAction.RequestAgainIfUserStillWantsIt]: "Ask again only if the user still wants the payment after expiry.",
|
|
136
152
|
[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.",
|
|
137
153
|
[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.",
|
|
154
|
+
[AgentPaymentNextAction.RetryWithExplicitContext]: "Retry the same tool call, this time passing merchant_url, tool_name, arguments, and mcp_transport explicitly \u2014 the server had no stored context to rehydrate for this payment id.",
|
|
138
155
|
[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."
|
|
139
156
|
};
|
|
140
157
|
var AgentPaymentFailureCodeDescriptions = {
|
|
141
158
|
[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.",
|
|
142
159
|
[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.",
|
|
143
|
-
[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."
|
|
160
|
+
[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.",
|
|
161
|
+
[AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The Haven funding leg succeeded, but the merchant did not answer the paid retry before the timeout. The merchant may still settle late \u2014 check haven_get_payment_status (and retry haven_complete_mcp_tool once) BEFORE sweeping; sweep only if no settlement appears.",
|
|
162
|
+
[AgentPaymentFailureCode.MerchantCallContextUnavailable]: "merchant_url/tool_name were omitted and no stored merchant call context is available for this payment_id. Re-send merchant_url, tool_name, arguments, and mcp_transport explicitly."
|
|
163
|
+
};
|
|
164
|
+
var AgentPaymentWarningCode = {
|
|
165
|
+
/** No max_amount cap was supplied — the live quoted price was accepted as-is. */
|
|
166
|
+
MissingMaxAmount: "MISSING_MAX_AMOUNT",
|
|
167
|
+
/** The signing window closes soon; sign promptly or re-quote with the same idempotency key. */
|
|
168
|
+
QuoteExpiresSoon: "QUOTE_EXPIRES_SOON",
|
|
169
|
+
/** The merchant URL was resolved via discovery — pass the RESOLVED url forward. */
|
|
170
|
+
MerchantUrlDiscovered: "MERCHANT_URL_DISCOVERED",
|
|
171
|
+
/**
|
|
172
|
+
* #1306: the catalog's last-verified price_atomic differs from the LIVE
|
|
173
|
+
* merchant quote for a guided catalog purchase. The catalog price is only
|
|
174
|
+
* ever indicative; the live quote in the same response is authoritative.
|
|
175
|
+
*/
|
|
176
|
+
CatalogPriceDiffers: "CATALOG_PRICE_DIFFERS",
|
|
177
|
+
/**
|
|
178
|
+
* #1306: the rail-aware allowance/budget pre-check could not be read (RPC
|
|
179
|
+
* failure, etc). `sufficient` is reported as null rather than a fabricated
|
|
180
|
+
* true/false — the on-chain policy remains the actual gate either way.
|
|
181
|
+
*/
|
|
182
|
+
AllowanceCheckUnavailable: "ALLOWANCE_CHECK_UNAVAILABLE",
|
|
183
|
+
/**
|
|
184
|
+
* #1319: the delegation-rail read itself SUCCEEDED, but the remaining
|
|
185
|
+
* figure it returned is the #1145 fallback (the full configured budget)
|
|
186
|
+
* rather than a live ERC20PeriodTransferEnforcer read — `sufficient` is a
|
|
187
|
+
* real true/false, just computed from an optimistic number. Distinct from
|
|
188
|
+
* {@link AgentPaymentWarningCode.AllowanceCheckUnavailable}, which fires
|
|
189
|
+
* when the read failed outright and `sufficient` degrades to null. The
|
|
190
|
+
* on-chain policy re-checks at redemption either way; this only says the
|
|
191
|
+
* guidance shown here may be optimistic.
|
|
192
|
+
*/
|
|
193
|
+
AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
|
|
144
194
|
};
|
|
145
195
|
var AgentPaymentRailDescriptions = {
|
|
146
196
|
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled Safe through an approved delegate allowance.",
|
|
@@ -195,6 +245,20 @@ var HavenApiError = class extends HavenError {
|
|
|
195
245
|
}
|
|
196
246
|
body;
|
|
197
247
|
};
|
|
248
|
+
var MerchantTimeoutError = class extends HavenApiError {
|
|
249
|
+
merchantErrorCode = "merchant_timeout";
|
|
250
|
+
constructor(message) {
|
|
251
|
+
super(message, 504);
|
|
252
|
+
this.name = "MerchantTimeoutError";
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
var X402UnexpectedStatusError = class extends HavenApiError {
|
|
256
|
+
x402ErrorCode = "unexpected_non_402_status";
|
|
257
|
+
constructor(message, statusCode) {
|
|
258
|
+
super(message, statusCode);
|
|
259
|
+
this.name = "X402UnexpectedStatusError";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
198
262
|
var HavenPaymentStateError = class extends HavenApiError {
|
|
199
263
|
constructor(message, statusCode, state, body) {
|
|
200
264
|
super(message, statusCode, body, state.paymentId);
|
|
@@ -219,6 +283,25 @@ var HavenSigningError = class extends HavenError {
|
|
|
219
283
|
this.name = "HavenSigningError";
|
|
220
284
|
}
|
|
221
285
|
};
|
|
286
|
+
var SignerRefusalCode = {
|
|
287
|
+
/** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
|
|
288
|
+
UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
|
|
289
|
+
/** `SUPPORTED_SWEEP_BINDING_VERSIONS` in `@haven_ai/signer` does not include the received version. */
|
|
290
|
+
UnsupportedSweepBindingVersion: "UNSUPPORTED_SWEEP_BINDING_VERSION"
|
|
291
|
+
};
|
|
292
|
+
var SIGNER_UPDATE_FALLBACK = "Update @haven_ai/signer by rerunning `npx @haven_ai/connect@alpha`, which reinstalls the pinned MCP runtime, then retry the same signing call. Nothing was signed or spent \u2014 the quote or payment this version came from is unaffected and does not need to be re-quoted.";
|
|
293
|
+
var HavenUnsupportedSignerVersionError = class extends HavenError {
|
|
294
|
+
constructor(message, code, supportedVersions, receivedVersion, fallback) {
|
|
295
|
+
super(message, code);
|
|
296
|
+
this.supportedVersions = supportedVersions;
|
|
297
|
+
this.receivedVersion = receivedVersion;
|
|
298
|
+
this.fallback = fallback;
|
|
299
|
+
this.name = "HavenUnsupportedSignerVersionError";
|
|
300
|
+
}
|
|
301
|
+
supportedVersions;
|
|
302
|
+
receivedVersion;
|
|
303
|
+
fallback;
|
|
304
|
+
};
|
|
222
305
|
var HavenTimeoutError = class extends HavenError {
|
|
223
306
|
constructor(paymentId) {
|
|
224
307
|
super(
|
|
@@ -697,6 +780,8 @@ function explorerUrlOrEmpty(chainId, txHash) {
|
|
|
697
780
|
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
698
781
|
}
|
|
699
782
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
783
|
+
var DEFAULT_MERCHANT_TIMEOUT = 3e5;
|
|
784
|
+
var NOTIFY_TIMEOUT = 1e4;
|
|
700
785
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
701
786
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
702
787
|
function formatAtomicAmount(atomic, decimals) {
|
|
@@ -860,12 +945,32 @@ function x402TypedDataDigest(typedData) {
|
|
|
860
945
|
);
|
|
861
946
|
}
|
|
862
947
|
}
|
|
948
|
+
function mapCatalogEntry(entry) {
|
|
949
|
+
return {
|
|
950
|
+
id: entry.id,
|
|
951
|
+
name: entry.name,
|
|
952
|
+
description: entry.description,
|
|
953
|
+
category: entry.category,
|
|
954
|
+
resourceUrl: entry.resource_url,
|
|
955
|
+
rail: entry.rail,
|
|
956
|
+
protocol: entry.protocol,
|
|
957
|
+
toolName: entry.tool_name,
|
|
958
|
+
toolArguments: entry.tool_arguments ?? null,
|
|
959
|
+
priceDisplay: entry.price_display,
|
|
960
|
+
priceAtomic: entry.price_atomic,
|
|
961
|
+
asset: entry.asset,
|
|
962
|
+
network: entry.network,
|
|
963
|
+
status: entry.status,
|
|
964
|
+
verifiedAt: entry.verified_at
|
|
965
|
+
};
|
|
966
|
+
}
|
|
863
967
|
var HavenClient = class {
|
|
864
968
|
apiKey;
|
|
865
969
|
delegateKey;
|
|
866
970
|
baseUrl;
|
|
867
971
|
x402Wallet;
|
|
868
972
|
requestTimeout;
|
|
973
|
+
merchantTimeout;
|
|
869
974
|
confirmationTimeout;
|
|
870
975
|
pollingInterval;
|
|
871
976
|
chainRpcs;
|
|
@@ -895,6 +1000,7 @@ var HavenClient = class {
|
|
|
895
1000
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
896
1001
|
this.x402Wallet = config.x402Wallet;
|
|
897
1002
|
this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1003
|
+
this.merchantTimeout = config.merchantTimeout ?? DEFAULT_MERCHANT_TIMEOUT;
|
|
898
1004
|
this.confirmationTimeout = config.confirmationTimeout ?? DEFAULT_CONFIRMATION_TIMEOUT;
|
|
899
1005
|
this.pollingInterval = config.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
900
1006
|
this.chainRpcs = config.chainRpcs ?? {};
|
|
@@ -951,7 +1057,8 @@ var HavenClient = class {
|
|
|
951
1057
|
const raw = await this.post("/payments", {
|
|
952
1058
|
token: request.token,
|
|
953
1059
|
amount: request.amount,
|
|
954
|
-
to: request.to
|
|
1060
|
+
to: request.to,
|
|
1061
|
+
...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {}
|
|
955
1062
|
});
|
|
956
1063
|
if (raw.status === "pending_approval") {
|
|
957
1064
|
this.throwPaymentStateError("Payment", raw);
|
|
@@ -1001,7 +1108,9 @@ var HavenClient = class {
|
|
|
1001
1108
|
asset: option.asset,
|
|
1002
1109
|
network: option.network,
|
|
1003
1110
|
description: paymentRequired.resource.description,
|
|
1004
|
-
idempotencyKey
|
|
1111
|
+
idempotencyKey,
|
|
1112
|
+
// #1307: persisted so the settle leg can rehydrate it by payment_id.
|
|
1113
|
+
...options.mcpCallContext ? { mcpCallContext: options.mcpCallContext } : {}
|
|
1005
1114
|
});
|
|
1006
1115
|
if (raw.status !== "pending_signature") {
|
|
1007
1116
|
this.throwPaymentStateError("x402 payment", raw);
|
|
@@ -1140,7 +1249,12 @@ var HavenClient = class {
|
|
|
1140
1249
|
status: raw.status,
|
|
1141
1250
|
safeAddress: raw.safe_address,
|
|
1142
1251
|
delegateAddress: raw.delegate_address,
|
|
1143
|
-
chainId: raw.chain_id
|
|
1252
|
+
chainId: raw.chain_id,
|
|
1253
|
+
// Defensive normalization, not trust: the backend contract is exactly
|
|
1254
|
+
// 'legacy' | 'delegation' (#1306), but an older/mismatched backend
|
|
1255
|
+
// during a rollout window should degrade to the wider legacy bucket
|
|
1256
|
+
// rather than propagate an unrecognized string.
|
|
1257
|
+
executionRail: raw.execution_rail === "delegation" ? "delegation" : "legacy"
|
|
1144
1258
|
};
|
|
1145
1259
|
}
|
|
1146
1260
|
/**
|
|
@@ -1290,11 +1404,108 @@ var HavenClient = class {
|
|
|
1290
1404
|
resetTimeMin: allowance.onchain.reset_time_min,
|
|
1291
1405
|
lastResetMin: allowance.onchain.last_reset_min,
|
|
1292
1406
|
nonce: allowance.onchain.nonce,
|
|
1293
|
-
isResetPending: allowance.onchain.is_reset_pending
|
|
1407
|
+
isResetPending: allowance.onchain.is_reset_pending,
|
|
1408
|
+
remainingIsFromChain: allowance.onchain.remaining_is_from_chain
|
|
1294
1409
|
}
|
|
1295
1410
|
}))
|
|
1296
1411
|
};
|
|
1297
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Post-purchase allowance/budget summary for a settled payment (#1310).
|
|
1415
|
+
*
|
|
1416
|
+
* Reuses the EXACT rail-aware read path {@link getAllowances} / #1306's
|
|
1417
|
+
* catalog-purchase preflight `allowance` block use — `GET
|
|
1418
|
+
* /machine-payments/allowances`, with delegation-rail values coming from
|
|
1419
|
+
* the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
1420
|
+
* `agent_allowances` — so this can never disagree with
|
|
1421
|
+
* {@link getAllowances} for the same fixture. The settled token is
|
|
1422
|
+
* resolved from {@link getPaymentStatus} so callers pass only
|
|
1423
|
+
* `paymentId`, never a second haven_get_agent-style round trip.
|
|
1424
|
+
*
|
|
1425
|
+
* NEVER throws: any failed read (status lookup, agent lookup, or the
|
|
1426
|
+
* allowance/budget lookup itself) degrades to `{ allowance: null,
|
|
1427
|
+
* warnings: [ALLOWANCE_CHECK_UNAVAILABLE] }` rather than converting a
|
|
1428
|
+
* successful settlement into a failure — the on-chain policy remains the
|
|
1429
|
+
* actual spend gate regardless of whether this report can be produced.
|
|
1430
|
+
*
|
|
1431
|
+
* Freshness caveat (#1319): the delegation rail's on-chain enforcer read
|
|
1432
|
+
* can silently fall back to the optimistic full period budget without
|
|
1433
|
+
* throwing when the RPC read itself fails (#1145's fund-safe design,
|
|
1434
|
+
* unchanged here). {@link getAllowances}'s `onchain.remainingIsFromChain`
|
|
1435
|
+
* now carries that provenance on the wire, and the #1306 catalog-purchase
|
|
1436
|
+
* preflight (`haven_prepare_catalog_purchase`) surfaces it as a warning —
|
|
1437
|
+
* this summary does not (yet). `remaining_atomic` here reflects the last
|
|
1438
|
+
* successful chain read, not a guaranteed-live one, and callers should not
|
|
1439
|
+
* phrase it as guaranteed-fresh.
|
|
1440
|
+
*/
|
|
1441
|
+
async getPostPurchaseAllowanceSummary(paymentId) {
|
|
1442
|
+
const unavailable = (detail) => ({
|
|
1443
|
+
allowance: null,
|
|
1444
|
+
warnings: [
|
|
1445
|
+
{
|
|
1446
|
+
code: AgentPaymentWarningCode.AllowanceCheckUnavailable,
|
|
1447
|
+
message: `Could not read the post-purchase allowance/budget for payment ${paymentId} (${detail}). The payment itself succeeded \u2014 the on-chain policy remains the actual spend gate; this only affects the remaining-budget figure reported here.`
|
|
1448
|
+
}
|
|
1449
|
+
]
|
|
1450
|
+
});
|
|
1451
|
+
try {
|
|
1452
|
+
const [status, agent, allowanceSummary] = await Promise.all([
|
|
1453
|
+
this.getPaymentStatus(paymentId),
|
|
1454
|
+
this.getAgent(),
|
|
1455
|
+
this.getAllowances()
|
|
1456
|
+
]);
|
|
1457
|
+
const tokenAddress = status.asset ?? status.x402?.asset ?? null;
|
|
1458
|
+
if (!tokenAddress) {
|
|
1459
|
+
return unavailable("the settled payment does not carry a resolvable token address");
|
|
1460
|
+
}
|
|
1461
|
+
const rail = agent.executionRail;
|
|
1462
|
+
const source = rail === "delegation" ? "active_delegations" : "allowance_module";
|
|
1463
|
+
const match = allowanceSummary.allowances.find(
|
|
1464
|
+
(a) => a.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()
|
|
1465
|
+
);
|
|
1466
|
+
if (!match) {
|
|
1467
|
+
return unavailable("no allowance/budget row matches the settled token");
|
|
1468
|
+
}
|
|
1469
|
+
const token = resolveTokenFromAddress(match.tokenAddress);
|
|
1470
|
+
const remainingDisplay = token ? `${formatAtomicAmount(safeBigInt(match.onchain.remaining), token.decimals)} ${match.tokenSymbol}` : void 0;
|
|
1471
|
+
return {
|
|
1472
|
+
allowance: {
|
|
1473
|
+
rail,
|
|
1474
|
+
remaining_atomic: match.onchain.remaining,
|
|
1475
|
+
...remainingDisplay ? { remaining_display: remainingDisplay } : {},
|
|
1476
|
+
token_symbol: match.tokenSymbol,
|
|
1477
|
+
token_address: match.tokenAddress,
|
|
1478
|
+
reset_period: match.resetPeriodMin,
|
|
1479
|
+
source
|
|
1480
|
+
},
|
|
1481
|
+
warnings: []
|
|
1482
|
+
};
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
return unavailable(err instanceof Error ? err.message : String(err));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* `haven_get_payment_status` convenience: fetch status and, for a
|
|
1489
|
+
* genuinely SETTLED x402 payment, attach the same post-purchase
|
|
1490
|
+
* allowance/budget summary a settle response carries.
|
|
1491
|
+
*
|
|
1492
|
+
* #1310/#1311 parity: this is the ONE home for logic that was duplicated
|
|
1493
|
+
* verbatim in `packages/mcp-server/src/tools.ts` and `packages/mcp/src/tools.ts`
|
|
1494
|
+
* (both hosted and local `haven_get_payment_status` handlers) — extracted
|
|
1495
|
+
* here because both packages already depend on `@haven_ai/sdk` and call
|
|
1496
|
+
* methods on a `HavenClient` instance, so this needed no new dependency
|
|
1497
|
+
* edge. `funded_but_unsettled` is deliberately excluded: that phase means
|
|
1498
|
+
* the merchant did NOT accept the retry. Every other phase/rail returns
|
|
1499
|
+
* the status untouched.
|
|
1500
|
+
*/
|
|
1501
|
+
async getPaymentStatusWithPostPurchaseAllowance(paymentId) {
|
|
1502
|
+
const status = await this.getPaymentStatus(paymentId);
|
|
1503
|
+
if (status.rail === AgentPaymentRail.X402 && status.phase === AgentPaymentPhase.PaymentConfirmed) {
|
|
1504
|
+
const { allowance, warnings } = await this.getPostPurchaseAllowanceSummary(paymentId);
|
|
1505
|
+
return { ...status, allowance, ...warnings.length > 0 ? { warnings } : {} };
|
|
1506
|
+
}
|
|
1507
|
+
return status;
|
|
1508
|
+
}
|
|
1298
1509
|
/**
|
|
1299
1510
|
* Discover payable services from Haven's curated merchant catalog.
|
|
1300
1511
|
*
|
|
@@ -1308,22 +1519,20 @@ var HavenClient = class {
|
|
|
1308
1519
|
if (options.rail) params.set("rail", options.rail);
|
|
1309
1520
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
1310
1521
|
const raw = await this.get(`/catalog${query}`);
|
|
1311
|
-
return raw.entries.map(
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
verifiedAt: entry.verified_at
|
|
1326
|
-
}));
|
|
1522
|
+
return raw.entries.map(mapCatalogEntry);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Fetch one curated catalog entry by id (#1306).
|
|
1526
|
+
*
|
|
1527
|
+
* Chain-scoped for free by the backend's SQL when the client is
|
|
1528
|
+
* agent-authenticated (#1299): an unknown id and an id curated for a
|
|
1529
|
+
* DIFFERENT chain than this agent's both 404 identically — this method does
|
|
1530
|
+
* not (and must not) re-filter by chain in JS. Read-only, like
|
|
1531
|
+
* {@link discoverTools}.
|
|
1532
|
+
*/
|
|
1533
|
+
async getCatalogEntry(id) {
|
|
1534
|
+
const raw = await this.get(`/catalog/${encodeURIComponent(id)}`);
|
|
1535
|
+
return mapCatalogEntry(raw);
|
|
1327
1536
|
}
|
|
1328
1537
|
/**
|
|
1329
1538
|
* List recent machine-payment receipts/evidence for bookkeeping.
|
|
@@ -1422,9 +1631,9 @@ var HavenClient = class {
|
|
|
1422
1631
|
async quoteX402(url, init, options = {}) {
|
|
1423
1632
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
1424
1633
|
const request = this.snapshotX402Request(url, initialInit);
|
|
1425
|
-
const response = await
|
|
1634
|
+
const response = await this.merchantFetch(url, initialInit);
|
|
1426
1635
|
if (response.status !== 402) {
|
|
1427
|
-
throw new
|
|
1636
|
+
throw new X402UnexpectedStatusError(
|
|
1428
1637
|
`Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
1429
1638
|
response.status || 400
|
|
1430
1639
|
);
|
|
@@ -1436,6 +1645,34 @@ var HavenClient = class {
|
|
|
1436
1645
|
const mcpTransport = await this.detectX402McpTransport(url, paymentRequired, response);
|
|
1437
1646
|
return this.buildX402Quote(paymentRequired, request, options.idempotencyKey, mcpTransport);
|
|
1438
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Probe an MCP tool for its x402 quote without creating a payment.
|
|
1650
|
+
*
|
|
1651
|
+
* Unlike the generic {@link quoteX402} helper, this completes the
|
|
1652
|
+
* Streamable-HTTP MCP lifecycle before sending the unpaid `tools/call`.
|
|
1653
|
+
* Hosted MCP uses this path while remaining keyless: it resolves only the
|
|
1654
|
+
* agent's public delegate address for `x402-wallet`; signing remains local.
|
|
1655
|
+
* It refuses before the quote when the merchant does not establish a session;
|
|
1656
|
+
* callers that need a plain x402 endpoint must use {@link quoteX402}.
|
|
1657
|
+
*/
|
|
1658
|
+
async quoteMcpX402(url, init, options = {}) {
|
|
1659
|
+
const wallet = await this.resolveX402WalletForMerchantCall();
|
|
1660
|
+
const sessionId = await this.mcpInitialize(url, init, wallet);
|
|
1661
|
+
if (!sessionId) {
|
|
1662
|
+
throw new HavenApiError(
|
|
1663
|
+
"The merchant did not establish an MCP session before the x402 quote. No payment was created.",
|
|
1664
|
+
502,
|
|
1665
|
+
{ mcpSessionNotEstablished: true }
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
let requestInit = this.withX402Wallet(init, wallet);
|
|
1669
|
+
requestInit = this.withMcpHeaders(requestInit, sessionId);
|
|
1670
|
+
const quote = await this.quoteX402(url, requestInit, options);
|
|
1671
|
+
return {
|
|
1672
|
+
...quote,
|
|
1673
|
+
mcpTransport: quote.mcpTransport ?? { handshakeRequired: true, source: "path" }
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1439
1676
|
/**
|
|
1440
1677
|
* Pay a previously inspected x402 quote and retry the exact captured request.
|
|
1441
1678
|
*/
|
|
@@ -1541,7 +1778,7 @@ var HavenClient = class {
|
|
|
1541
1778
|
if (!url) {
|
|
1542
1779
|
throw new HavenApiError("x402 resume requires the original URL or a captured request snapshot.", 400);
|
|
1543
1780
|
}
|
|
1544
|
-
const response = await
|
|
1781
|
+
const response = await this.merchantFetch(url, initialInit);
|
|
1545
1782
|
if (response.status !== 402) {
|
|
1546
1783
|
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
1547
1784
|
}
|
|
@@ -1584,7 +1821,7 @@ var HavenClient = class {
|
|
|
1584
1821
|
}
|
|
1585
1822
|
let requestInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
1586
1823
|
if (mcpSessionId) requestInit = this.withMcpHeaders(requestInit, mcpSessionId);
|
|
1587
|
-
const response = await
|
|
1824
|
+
const response = await this.merchantFetch(url, requestInit);
|
|
1588
1825
|
if (response.status !== 402) {
|
|
1589
1826
|
return mcpSessionId ? this.surfaceMcpResult(response) : response;
|
|
1590
1827
|
}
|
|
@@ -1645,7 +1882,7 @@ var HavenClient = class {
|
|
|
1645
1882
|
headers.set("Content-Type", "application/json");
|
|
1646
1883
|
headers.set("Accept", MCP_ACCEPT);
|
|
1647
1884
|
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1648
|
-
const response = await
|
|
1885
|
+
const response = await this.merchantFetch(url, {
|
|
1649
1886
|
method: "POST",
|
|
1650
1887
|
headers,
|
|
1651
1888
|
body: JSON.stringify({
|
|
@@ -1682,11 +1919,15 @@ var HavenClient = class {
|
|
|
1682
1919
|
headers.set("Accept", MCP_ACCEPT);
|
|
1683
1920
|
headers.set("mcp-session-id", sessionId);
|
|
1684
1921
|
if (wallet && !headers.has("x402-wallet")) headers.set("x402-wallet", wallet);
|
|
1685
|
-
await
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1922
|
+
await this.merchantFetch(
|
|
1923
|
+
url,
|
|
1924
|
+
{
|
|
1925
|
+
method: "POST",
|
|
1926
|
+
headers,
|
|
1927
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1928
|
+
},
|
|
1929
|
+
NOTIFY_TIMEOUT
|
|
1930
|
+
);
|
|
1690
1931
|
} catch {
|
|
1691
1932
|
}
|
|
1692
1933
|
}
|
|
@@ -1752,7 +1993,7 @@ var HavenClient = class {
|
|
|
1752
1993
|
return this.buildMppQuote(challengeOrUrl, request2, options.idempotencyKey);
|
|
1753
1994
|
}
|
|
1754
1995
|
const request = this.snapshotX402Request(challengeOrUrl, init);
|
|
1755
|
-
const response = await
|
|
1996
|
+
const response = await this.merchantFetch(challengeOrUrl, init);
|
|
1756
1997
|
if (response.status !== 402) {
|
|
1757
1998
|
throw new HavenApiError(
|
|
1758
1999
|
`Expected an MPP quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
@@ -1794,7 +2035,7 @@ var HavenClient = class {
|
|
|
1794
2035
|
}
|
|
1795
2036
|
const retryHeaders = new Headers(initialInit?.headers);
|
|
1796
2037
|
retryHeaders.set("X-PAYMENT", receipt.paymentHeader);
|
|
1797
|
-
const retryResponse = await
|
|
2038
|
+
const retryResponse = await this.merchantFetch(url, {
|
|
1798
2039
|
...initialInit,
|
|
1799
2040
|
headers: retryHeaders
|
|
1800
2041
|
});
|
|
@@ -1927,7 +2168,7 @@ var HavenClient = class {
|
|
|
1927
2168
|
const headers = new Headers(requestInit.headers);
|
|
1928
2169
|
headers.set("X-PAYMENT", input.paymentHeader);
|
|
1929
2170
|
requestInit = { ...requestInit, headers };
|
|
1930
|
-
const response = await
|
|
2171
|
+
const response = await this.merchantFetch(input.url, requestInit);
|
|
1931
2172
|
const surfaced = mcpSessionId ? await this.surfaceMcpResult(response) : response;
|
|
1932
2173
|
const protocolReceiptHeader = surfaced.headers.get("PAYMENT-RESPONSE") ?? void 0;
|
|
1933
2174
|
const settlement = parseMerchantSettlement(protocolReceiptHeader ?? null);
|
|
@@ -1975,6 +2216,33 @@ var HavenClient = class {
|
|
|
1975
2216
|
settlementTxHash: settlement.settlementTxHash ?? void 0
|
|
1976
2217
|
};
|
|
1977
2218
|
}
|
|
2219
|
+
/**
|
|
2220
|
+
* GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
|
|
2221
|
+
* sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
|
|
2222
|
+
* context (merchant_url, tool_name, arguments, mcp_transport) recorded at
|
|
2223
|
+
* quote time, so `haven_settle_mcp_tool` / `haven_complete_mcp_tool` can
|
|
2224
|
+
* omit those fields and let Haven rehydrate them by payment_id instead of
|
|
2225
|
+
* the caller re-threading them. Throws `HavenApiError` (404 unknown/foreign
|
|
2226
|
+
* payment_id, 409 no stored context, 410 expired) — the caller decides the
|
|
2227
|
+
* fallback (re-send the full context explicitly).
|
|
2228
|
+
*/
|
|
2229
|
+
async getX402MerchantCallContext(paymentId) {
|
|
2230
|
+
const raw = await this.get(
|
|
2231
|
+
`/x402/${paymentId}/merchant-call-context`
|
|
2232
|
+
);
|
|
2233
|
+
return {
|
|
2234
|
+
paymentId: raw.payment_id,
|
|
2235
|
+
merchantUrl: raw.merchant_url,
|
|
2236
|
+
toolName: raw.tool_name,
|
|
2237
|
+
arguments: raw.arguments ?? {},
|
|
2238
|
+
...raw.mcp_transport ? {
|
|
2239
|
+
mcpTransport: {
|
|
2240
|
+
handshakeRequired: raw.mcp_transport.handshake_required,
|
|
2241
|
+
source: raw.mcp_transport.source
|
|
2242
|
+
}
|
|
2243
|
+
} : {}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
1978
2246
|
async resolveX402MerchantCompletionContext(input) {
|
|
1979
2247
|
const status = await this.getPaymentStatus(input.paymentId);
|
|
1980
2248
|
if (status.rail !== "x402") {
|
|
@@ -2090,7 +2358,7 @@ var HavenClient = class {
|
|
|
2090
2358
|
if (!url) {
|
|
2091
2359
|
throw new HavenApiError("MPP resume requires the original URL or a captured request snapshot.", 400);
|
|
2092
2360
|
}
|
|
2093
|
-
const response = await
|
|
2361
|
+
const response = await this.merchantFetch(url, initialInit);
|
|
2094
2362
|
if (response.status !== 402) {
|
|
2095
2363
|
throw new HavenApiError("Expected the original MPP request to return HTTP 402 before resuming.", 400);
|
|
2096
2364
|
}
|
|
@@ -2123,7 +2391,7 @@ var HavenClient = class {
|
|
|
2123
2391
|
async retryMppRequest(url, initialInit, challenge, receipt) {
|
|
2124
2392
|
const retryHeaders = new Headers(initialInit?.headers);
|
|
2125
2393
|
retryHeaders.set("MACHINE-PAYMENT-PROOF", receipt.proofHeader);
|
|
2126
|
-
const retryResponse = await
|
|
2394
|
+
const retryResponse = await this.merchantFetch(url, {
|
|
2127
2395
|
...initialInit,
|
|
2128
2396
|
headers: retryHeaders
|
|
2129
2397
|
});
|
|
@@ -2975,6 +3243,26 @@ var HavenClient = class {
|
|
|
2975
3243
|
async get(path) {
|
|
2976
3244
|
return this.request("GET", path);
|
|
2977
3245
|
}
|
|
3246
|
+
/**
|
|
3247
|
+
* #1300: every MERCHANT-facing fetch goes through here. Haven API calls
|
|
3248
|
+
* have always been bounded (request() below); the merchant probes/retries
|
|
3249
|
+
* called globalThis.fetch bare, so a slow-loris merchant could hold a tool
|
|
3250
|
+
* call open forever. A caller-supplied signal still applies (combined via
|
|
3251
|
+
* AbortSignal.any); a timeout abort surfaces as a clear HavenApiError 504
|
|
3252
|
+
* naming the URL rather than a bare AbortError.
|
|
3253
|
+
*/
|
|
3254
|
+
async merchantFetch(url, init = {}, timeoutMs = this.merchantTimeout) {
|
|
3255
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3256
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
3257
|
+
try {
|
|
3258
|
+
return await globalThis.fetch(url, { ...init, signal });
|
|
3259
|
+
} catch (err) {
|
|
3260
|
+
if (timeoutSignal.aborted) {
|
|
3261
|
+
throw new MerchantTimeoutError(`Merchant request timed out after ${timeoutMs}ms: ${url}`);
|
|
3262
|
+
}
|
|
3263
|
+
throw err;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
2978
3266
|
async request(method, path, body) {
|
|
2979
3267
|
const url = `${this.baseUrl}${path}`;
|
|
2980
3268
|
const controller = new AbortController();
|
|
@@ -3228,16 +3516,16 @@ var toolDescriptions = {
|
|
|
3228
3516
|
nextActionGuidance: ""
|
|
3229
3517
|
},
|
|
3230
3518
|
payMcpTool: {
|
|
3231
|
-
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip.",
|
|
3519
|
+
summary: "Call a named tool on an MCP merchant that requires an x402 payment, handling the full initialize \u2192 pay \u2192 retry round trip in one call.",
|
|
3232
3520
|
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.",
|
|
3233
|
-
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
|
|
3521
|
+
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, returning 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 \u2014 follow the response's nextAction when present.",
|
|
3234
3522
|
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."
|
|
3235
3523
|
},
|
|
3236
3524
|
discoverTools: {
|
|
3237
|
-
summary: "
|
|
3525
|
+
summary: "Step 1 of a purchase: discover payable services from Haven's curated merchant catalog \u2014 names, prices, and which pay tool to use next.",
|
|
3238
3526
|
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.",
|
|
3239
|
-
behavior: "Read-only lookup against Haven's curated catalog
|
|
3240
|
-
nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url
|
|
3527
|
+
behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, and suggested_tool. The catalog price (price_display/price_atomic, marked price_is_indicative) is a last-verified hint, NOT authoritative \u2014 the real price comes from the merchant's live 402 at pay time. Never creates a payment, signature, or approval.",
|
|
3528
|
+
nextActionGuidance: "Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass max_amount when the user has a cap."
|
|
3241
3529
|
},
|
|
3242
3530
|
sweep_delegate: {
|
|
3243
3531
|
summary: "Sweep stranded USDC and/or ETH from the delegate wallet back to the originating Safe.",
|
|
@@ -3506,6 +3794,9 @@ user's approval in Haven.
|
|
|
3506
3794
|
|
|
3507
3795
|
Hosted tools run in the \`mcp__haven__\` namespace. Local signing tools run in
|
|
3508
3796
|
the \`mcp__haven-signer__\` namespace and keep the delegate key on this machine.
|
|
3797
|
+
Tool results carry the exact next step (\`next_action\`, \`next_tool\`,
|
|
3798
|
+
\`next_arguments\`) \u2014 follow those fields first; the prose below is fallback
|
|
3799
|
+
and orientation, not the source of truth.
|
|
3509
3800
|
|
|
3510
3801
|
## When to use this skill
|
|
3511
3802
|
|
|
@@ -3530,11 +3821,11 @@ Before any payment, confirm the *live remaining* budget with the tools \u2014
|
|
|
3530
3821
|
\`agent.json\` shows the configured budget, not what is left after recent
|
|
3531
3822
|
spending:
|
|
3532
3823
|
|
|
3533
|
-
- \`
|
|
3534
|
-
plus a readiness signal (\`ready\` / \`needs_approval\` /
|
|
3535
|
-
remaining per-token allowance, in one shot.
|
|
3536
|
-
- \`
|
|
3537
|
-
reset window) when you need more than the summary.
|
|
3824
|
+
- \`mcp__haven__haven_get_agent\` \u2014 the recommended first call: identity
|
|
3825
|
+
(wallet, network) plus a readiness signal (\`ready\` / \`needs_approval\` /
|
|
3826
|
+
\`revoked\`) and live remaining per-token allowance, in one shot.
|
|
3827
|
+
- \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
|
|
3828
|
+
(configured, spent, reset window) when you need more than the summary.
|
|
3538
3829
|
|
|
3539
3830
|
Budgets reset on a period the user chose. If a payment exceeds the remaining
|
|
3540
3831
|
budget it is queued for the user to approve in the Haven dashboard \u2014 this is
|
|
@@ -3542,47 +3833,79 @@ normal, not an error.
|
|
|
3542
3833
|
|
|
3543
3834
|
## Paying
|
|
3544
3835
|
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3836
|
+
**Catalog purchases \u2014 the primary path for MCP merchants:**
|
|
3837
|
+
|
|
3838
|
+
1. \`mcp__haven__haven_discover_tools\` to find a payable service and its
|
|
3839
|
+
\`catalog_id\`.
|
|
3840
|
+
2. \`mcp__haven__haven_prepare_catalog_purchase\` with \`catalog_id\` and
|
|
3841
|
+
\`max_amount\`. \`max_amount\` (atomic units) is REQUIRED on this tool, and
|
|
3842
|
+
is best practice on every paid call below too \u2014 it caps what the LIVE
|
|
3843
|
+
merchant quote may charge, checked before any funding intent is created.
|
|
3844
|
+
3. Then FOLLOW THE RESPONSE'S GUIDANCE FIELDS: \`next_action\`, \`next_tool\`,
|
|
3845
|
+
and \`next_arguments\` name the exact next call \u2014 act on those first; the
|
|
3846
|
+
prose in this section is fallback and debugging detail. If the catalog
|
|
3847
|
+
entry is missing or degraded, the response instead names
|
|
3848
|
+
\`mcp__haven__haven_pay_mcp_tool\` (merchant URL, tool name, arguments) as
|
|
3849
|
+
the manual fallback.
|
|
3850
|
+
|
|
3851
|
+
**Signing:** \`mcp__haven-signer__haven_sign_x402\` with \`payment_id\` and
|
|
3852
|
+
\`payment_required\` ONLY \u2014 the local signer fetches the exact signing bytes
|
|
3853
|
+
itself, so never relay \`typed_data\` yourself. Fallback for an older signer
|
|
3854
|
+
or backend: re-run the quote/prepare tool with the SAME \`idempotency_key\`
|
|
3855
|
+
plus \`include_signing_payload=true\`, then pass \`payload_hash\`,
|
|
3856
|
+
\`x402_expected\` (the nested \`x402.expected\` object), and
|
|
3857
|
+
\`typed_data\`/\`typed_data_b64\` through unchanged.
|
|
3858
|
+
|
|
3859
|
+
**Settle:** \`mcp__haven__haven_settle_mcp_tool\` with \`payment_id\`,
|
|
3860
|
+
\`signature\`, and \`payment_header\` ONLY \u2014 Haven rehydrates the merchant call
|
|
3861
|
+
context (\`merchant_url\`, \`tool_name\`, \`arguments\`, \`mcp_transport\`)
|
|
3862
|
+
server-side from \`payment_id\`. Pass those four fields explicitly only as a
|
|
3863
|
+
version-skew fallback when Haven has no stored context for the id \u2014 both or
|
|
3864
|
+
none together, never just one. If the settle result carries \`settled: false\`,
|
|
3865
|
+
funding is queued for the user's approval \u2014 tell them and check status later,
|
|
3866
|
+
do not re-pay.
|
|
3867
|
+
|
|
3868
|
+
Step-by-step alternative (also key-safe; for an older signer or backend, or
|
|
3869
|
+
when you already have a merchant URL and tool name instead of a
|
|
3870
|
+
\`catalog_id\`): \`mcp__haven__haven_pay_mcp_tool\` then
|
|
3871
|
+
\`mcp__haven-signer__haven_sign\` \u2192 \`mcp__haven__haven_submit\` \u2192
|
|
3872
|
+
\`mcp__haven-signer__haven_x402_sign_header\` \u2192
|
|
3873
|
+
\`mcp__haven__haven_complete_mcp_tool\`. Pass \`payment_required\`,
|
|
3874
|
+
\`arguments\`, and \`mcp_transport\` verbatim from the quote/prepare result.
|
|
3875
|
+
The returned \`expires_at\` is the signing window; if a tool returns
|
|
3876
|
+
\`PAYMENT_WINDOW_EXPIRED\`, re-run the same quote/prepare tool with the same
|
|
3877
|
+
\`idempotency_key\`. Do not call the merchant yourself \u2014 Haven completes the
|
|
3878
|
+
merchant leg for you.
|
|
3879
|
+
|
|
3880
|
+
**Direct transfer / non-MCP paywall:** \`mcp__haven__haven_pay\` with
|
|
3881
|
+
recipient, amount, and token for a plain transfer. For an arbitrary,
|
|
3882
|
+
non-MCP x402 paywall: \`mcp__haven__haven_quote_x402\` to get a quote, then
|
|
3883
|
+
\`mcp__haven__haven_pay_x402_quote\` \u2014 follow the result's guidance fields
|
|
3884
|
+
first, sign in the local Haven signer, and retry the original request only
|
|
3885
|
+
when the result says \`retry_original_x402_request\`.
|
|
3886
|
+
|
|
3887
|
+
**Catalog tool arguments:** when \`haven_discover_tools\` returns
|
|
3888
|
+
\`tool_arguments\`, pass that object unchanged as the pay tool's
|
|
3889
|
+
\`arguments\` field (for example
|
|
3890
|
+
\`tool_arguments: { "tier": "50gb" }\` -> \`arguments: { "tier": "50gb" }\`).
|
|
3891
|
+
|
|
3892
|
+
**Prices:** show the user the live price from the pay-tool result, never a
|
|
3893
|
+
catalog price. \`haven_discover_tools\` prices are indicative
|
|
3894
|
+
(\`price_is_indicative\`) and can be stale. The pay-tool result's \`amount\` /
|
|
3895
|
+
\`amount_atomic\` is the amount Haven authorizes for the call \u2014 a ceiling the
|
|
3896
|
+
merchant settles at or below \u2014 so present it as the most the user will pay.
|
|
3897
|
+
|
|
3898
|
+
**Status:** \`mcp__haven__haven_get_payment_status\` with a \`payment_id\` to
|
|
3899
|
+
check on queued or in-flight payments. Do not poll in a tight loop.
|
|
3580
3900
|
|
|
3581
3901
|
## Approval semantics
|
|
3582
3902
|
|
|
3583
3903
|
- A result with \`pending_approval\` means the payment exceeded the remaining
|
|
3584
3904
|
budget and is waiting for the user in Haven. Tell the user, then check
|
|
3585
3905
|
status later.
|
|
3906
|
+
- \`safe_to_continue: false\` on a guidance block is the same signal in
|
|
3907
|
+
machine-readable form: stop and involve the user before calling anything
|
|
3908
|
+
else for this payment.
|
|
3586
3909
|
- Never ask the user for private keys. Signing happens only in the local Haven
|
|
3587
3910
|
signer; the hosted Haven tools never receive the signing key. If a tool
|
|
3588
3911
|
reports a missing or invalid credential, tell the user to re-run the Haven
|
|
@@ -3599,12 +3922,28 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
|
|
|
3599
3922
|
Suggest the user add funds in the Haven dashboard.
|
|
3600
3923
|
- \`PRICE_EXCEEDS_MAX\`: the live merchant price exceeded your \`max_amount\`.
|
|
3601
3924
|
No funds moved; ask the user before retrying with a higher cap.
|
|
3602
|
-
- \`PAYMENT_WINDOW_EXPIRED\`: re-run
|
|
3603
|
-
\`idempotency_key\`, then sign the fresh
|
|
3604
|
-
- \`MERCHANT_REJECTED_AFTER_FUNDING\`:
|
|
3925
|
+
- \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
|
|
3926
|
+
\`idempotency_key\`, then sign the fresh payload.
|
|
3927
|
+
- \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
|
|
3928
|
+
Stop-and-sweep \u2014 stop retrying the merchant and use
|
|
3605
3929
|
\`mcp__haven__haven_sweep_delegate\` to recover stranded delegate funds.
|
|
3930
|
+
- \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: funding confirmed on-chain, but the
|
|
3931
|
+
merchant never answered the paid retry. This is NOT proof of rejection \u2014 the
|
|
3932
|
+
merchant may still settle late. Verify-then-sweep, never a blind sweep:
|
|
3933
|
+
check \`mcp__haven__haven_get_payment_status\`, retry
|
|
3934
|
+
\`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
|
|
3935
|
+
\`mcp__haven__haven_sweep_delegate\` if no settlement appears.
|
|
3606
3936
|
- Budget exceeded: tell the user how much remains (from
|
|
3607
|
-
\`
|
|
3937
|
+
\`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
|
|
3938
|
+
Haven.
|
|
3939
|
+
|
|
3940
|
+
## Reporting after a purchase
|
|
3941
|
+
|
|
3942
|
+
A settled \`mcp__haven__haven_settle_mcp_tool\` response carries
|
|
3943
|
+
\`agent_summary\` and the remaining post-purchase allowance in \`allowance\` \u2014
|
|
3944
|
+
report the amount paid and what is left from those fields directly. Do not
|
|
3945
|
+
call \`haven_get_agent\` or \`haven_get_allowances\` again just to report a
|
|
3946
|
+
purchase you already made.
|
|
3608
3947
|
|
|
3609
3948
|
## Revoke
|
|
3610
3949
|
|
|
@@ -3758,6 +4097,50 @@ function stableStringify2(value) {
|
|
|
3758
4097
|
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify2(object[key])}`).join(",")}}`;
|
|
3759
4098
|
}
|
|
3760
4099
|
|
|
4100
|
+
// src/merchant-discovery.ts
|
|
4101
|
+
var MERCHANT_DISCOVERY_PATHS = ["/.well-known/haven-demo-merchant", "/"];
|
|
4102
|
+
var DISCOVERY_MAX_BYTES = 64 * 1024;
|
|
4103
|
+
async function discoverMerchantMcpUrl(inputUrl) {
|
|
4104
|
+
let input;
|
|
4105
|
+
try {
|
|
4106
|
+
input = new URL(inputUrl);
|
|
4107
|
+
} catch {
|
|
4108
|
+
return null;
|
|
4109
|
+
}
|
|
4110
|
+
for (const path of MERCHANT_DISCOVERY_PATHS) {
|
|
4111
|
+
try {
|
|
4112
|
+
const res = await globalThis.fetch(`${input.origin}${path}`, {
|
|
4113
|
+
method: "GET",
|
|
4114
|
+
headers: { accept: "application/json" },
|
|
4115
|
+
redirect: "error",
|
|
4116
|
+
signal: AbortSignal.timeout(5e3)
|
|
4117
|
+
});
|
|
4118
|
+
if (!res.ok) continue;
|
|
4119
|
+
const contentLength = Number(res.headers.get("content-length") ?? 0);
|
|
4120
|
+
if (contentLength > DISCOVERY_MAX_BYTES) continue;
|
|
4121
|
+
const text = await res.text();
|
|
4122
|
+
if (text.length > DISCOVERY_MAX_BYTES) continue;
|
|
4123
|
+
const doc = JSON.parse(text);
|
|
4124
|
+
if (typeof doc.mcp_url !== "string") continue;
|
|
4125
|
+
const resolved = new URL(doc.mcp_url);
|
|
4126
|
+
if (resolved.origin !== input.origin) continue;
|
|
4127
|
+
return resolved.toString();
|
|
4128
|
+
} catch {
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
return null;
|
|
4133
|
+
}
|
|
4134
|
+
function sameUrl(a, b) {
|
|
4135
|
+
try {
|
|
4136
|
+
const ua = new URL(a);
|
|
4137
|
+
const ub = new URL(b);
|
|
4138
|
+
return ua.origin === ub.origin && ua.pathname.replace(/\/+$/, "") === ub.pathname.replace(/\/+$/, "");
|
|
4139
|
+
} catch {
|
|
4140
|
+
return false;
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
3761
4144
|
exports.AGENT_PAYMENT_FAILURE_CODE_VALUES = AGENT_PAYMENT_FAILURE_CODE_VALUES;
|
|
3762
4145
|
exports.AGENT_PAYMENT_NEXT_ACTION_VALUES = AGENT_PAYMENT_NEXT_ACTION_VALUES;
|
|
3763
4146
|
exports.AGENT_PAYMENT_PHASE_VALUES = AGENT_PAYMENT_PHASE_VALUES;
|
|
@@ -3774,6 +4157,8 @@ exports.AgentPaymentPhaseSchema = AgentPaymentPhaseSchema;
|
|
|
3774
4157
|
exports.AgentPaymentRail = AgentPaymentRail;
|
|
3775
4158
|
exports.AgentPaymentRailDescriptions = AgentPaymentRailDescriptions;
|
|
3776
4159
|
exports.AgentPaymentRailSchema = AgentPaymentRailSchema;
|
|
4160
|
+
exports.AgentPaymentWarningCode = AgentPaymentWarningCode;
|
|
4161
|
+
exports.DISCOVERY_MAX_BYTES = DISCOVERY_MAX_BYTES;
|
|
3777
4162
|
exports.HAVEN_MINIMUM_NODE_VERSION = HAVEN_MINIMUM_NODE_VERSION;
|
|
3778
4163
|
exports.HAVEN_SKILL_MD = HAVEN_SKILL_MD;
|
|
3779
4164
|
exports.HavenApiError = HavenApiError;
|
|
@@ -3782,13 +4167,19 @@ exports.HavenError = HavenError;
|
|
|
3782
4167
|
exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
3783
4168
|
exports.HavenSigningError = HavenSigningError;
|
|
3784
4169
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
4170
|
+
exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
|
|
4171
|
+
exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
|
|
4172
|
+
exports.MerchantTimeoutError = MerchantTimeoutError;
|
|
3785
4173
|
exports.RECEIPT_VERSION = RECEIPT_VERSION;
|
|
4174
|
+
exports.SIGNER_UPDATE_FALLBACK = SIGNER_UPDATE_FALLBACK;
|
|
3786
4175
|
exports.SKILL_FOLDER_NAME = SKILL_FOLDER_NAME;
|
|
3787
4176
|
exports.SWEEP_BASE_CHAIN_ID = SWEEP_BASE_CHAIN_ID;
|
|
3788
4177
|
exports.SWEEP_BASE_SEPOLIA_CHAIN_ID = SWEEP_BASE_SEPOLIA_CHAIN_ID;
|
|
3789
4178
|
exports.SWEEP_BASE_SEPOLIA_USDC_ADDRESS = SWEEP_BASE_SEPOLIA_USDC_ADDRESS;
|
|
3790
4179
|
exports.SWEEP_BASE_USDC_ADDRESS = SWEEP_BASE_USDC_ADDRESS;
|
|
4180
|
+
exports.SignerRefusalCode = SignerRefusalCode;
|
|
3791
4181
|
exports.TRANSFER_WITH_AUTHORIZATION_TYPES = TRANSFER_WITH_AUTHORIZATION_TYPES;
|
|
4182
|
+
exports.X402UnexpectedStatusError = X402UnexpectedStatusError;
|
|
3792
4183
|
exports.X402_MAX_AUTHORIZATION_WINDOW_SECONDS = X402_MAX_AUTHORIZATION_WINDOW_SECONDS;
|
|
3793
4184
|
exports.X402_SETTLEMENT_FORWARD_MARGIN_SECONDS = X402_SETTLEMENT_FORWARD_MARGIN_SECONDS;
|
|
3794
4185
|
exports.addressFromKey = addressFromKey;
|
|
@@ -3800,6 +4191,7 @@ exports.compareNodeVersions = compareNodeVersions;
|
|
|
3800
4191
|
exports.composeDescription = composeDescription;
|
|
3801
4192
|
exports.decodeBase64Json = decodeBase64Json;
|
|
3802
4193
|
exports.decodeBase64Utf8 = decodeBase64Utf8;
|
|
4194
|
+
exports.discoverMerchantMcpUrl = discoverMerchantMcpUrl;
|
|
3803
4195
|
exports.encodeBase64Json = encodeBase64Json;
|
|
3804
4196
|
exports.encodeBase64Utf8 = encodeBase64Utf8;
|
|
3805
4197
|
exports.encodeMachinePaymentProof = encodeMachinePaymentProof;
|
|
@@ -3811,6 +4203,7 @@ exports.parseMachinePaymentChallenge = parseMachinePaymentChallenge;
|
|
|
3811
4203
|
exports.parseMachinePaymentChallengeResponse = parseMachinePaymentChallengeResponse;
|
|
3812
4204
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
3813
4205
|
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|
|
4206
|
+
exports.sameUrl = sameUrl;
|
|
3814
4207
|
exports.selectPaymentOption = selectPaymentOption;
|
|
3815
4208
|
exports.selectStandardPaymentOption = selectStandardPaymentOption;
|
|
3816
4209
|
exports.signHash = signHash;
|