@haven_ai/sdk 0.2.0-alpha.0 → 0.2.1-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 +13 -6
- package/dist/index.cjs +201 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +260 -76
- package/dist/index.d.ts +260 -76
- package/dist/index.js +200 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,9 +85,13 @@ shows up in your Haven dashboard activity feed.
|
|
|
85
85
|
## Step-by-Step API
|
|
86
86
|
|
|
87
87
|
Discovery and listing: `discoverTools({ verified?: 'any' | 'verified' | 'operator' })` returns the merged
|
|
88
|
-
catalog — operator-curated plus
|
|
89
|
-
`domainVerified` and `verifiedPayable`. `
|
|
90
|
-
|
|
88
|
+
catalog — operator-curated rows plus self-submitted directory entries (epic #1717), each with `source`,
|
|
89
|
+
`domainVerified` and `verifiedPayable`. `verified: 'verified'` (#2978) filters on the badge, not the
|
|
90
|
+
source: it returns any entry, from either source, whose endpoint Haven watched answer a live quote
|
|
91
|
+
probe (`verifiedPayable === true`); `domainVerified` stays true only for entries that also proved
|
|
92
|
+
domain ownership. `verified: 'operator'` filters on provenance alone. `submitCatalogEntry(resourceUrl)`
|
|
93
|
+
submits a merchant endpoint to the Verified Payable Directory (queue-only; the seller still must prove
|
|
94
|
+
domain ownership before listing),
|
|
91
95
|
and `getCatalogSubmissionStatus(id)` returns coarse status plus the ownership-proof instructions while
|
|
92
96
|
the submission can still prove ownership.
|
|
93
97
|
|
|
@@ -358,7 +362,8 @@ Terminal from any non-confirmed phase:
|
|
|
358
362
|
|
|
359
363
|
x402 tool-window failures:
|
|
360
364
|
expired funding/quote window → PAYMENT_WINDOW_EXPIRED → re-quote with same idempotency_key
|
|
361
|
-
merchant rejection after funding → MERCHANT_REJECTED_AFTER_FUNDING → haven_sweep_delegate
|
|
365
|
+
merchant rejection after funding → MERCHANT_REJECTED_AFTER_FUNDING → haven_sweep_delegate (eip3009 only; erc7710: nothing to sweep, follow the message)
|
|
366
|
+
merchant timeout after funding → MERCHANT_UNRESPONSIVE_AFTER_FUNDING → check_status_later; eip3009: retry once, then haven_sweep_delegate; erc7710: no retry, check status after the window, re-quote only if unsettled
|
|
362
367
|
```
|
|
363
368
|
|
|
364
369
|
### `phase` reference
|
|
@@ -399,11 +404,13 @@ Hosted MCP and signer tools also return stable `code` values on recoverable x402
|
|
|
399
404
|
|
|
400
405
|
| `code` | Meaning | Agent recovery |
|
|
401
406
|
|--------|---------|----------------|
|
|
402
|
-
| `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's spending cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher one. |
|
|
407
|
+
| `PRICE_EXCEEDS_MAX` | The merchant-authoritative x402 price is above the caller's spending cap. No funding transfer was created. | Tell the user the live price exceeded the cap and retry only after they confirm a higher one. Payloads carry `next_action: stop_and_tell_user` and `retry_with_new_quote: true` — the latter means any retry needs a fresh quote, not that one should be attempted unattended. |
|
|
403
408
|
| `AMBIGUOUS_MAX_AMOUNT` | Both `max_amount` (atomic units) and `max_amount_human` (whole tokens) were sent for one purchase. Nothing was contacted and nothing was spent. | Re-send with exactly one — `max_amount_human` for a cap the user stated in tokens, `max_amount` for an exact atomic figure. |
|
|
404
409
|
| `MAX_AMOUNT_UNCONVERTIBLE` | `max_amount_human` could not be converted against this quote's asset — its decimals are unknown to Haven, or the cap has more decimal places than the asset supports. Nothing was spent. | Round the cap to the asset's decimals, or re-send it as an exact atomic `max_amount`. |
|
|
410
|
+
| `MERCHANT_NOT_READY` | The merchant answered the quote probe with its own `503 { error: 'merchant_not_ready', reason_code, retry_after_s }` instead of a 402 — it cannot settle right now (e.g. its settlement wallet is out of gas). No payment was created. | Tell the user and retry later (`retry_after_s` in the message when given); do not treat it as a wrong endpoint. Payloads carry `next_action: stop_and_tell_user` and `retry_with_new_quote: true`. |
|
|
405
411
|
| `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`. |
|
|
406
|
-
| `MERCHANT_REJECTED_AFTER_FUNDING` | Haven's funding leg succeeded
|
|
412
|
+
| `MERCHANT_REJECTED_AFTER_FUNDING` | The merchant rejected the paid retry. On eip3009 Haven's funding leg had succeeded; on erc7710 there is no funding leg (#2983). | eip3009: stop retrying the merchant and call `haven_sweep_delegate` so the user can recover stranded delegate USDC. erc7710: nothing to sweep — follow the message (re-quote later if the merchant declined to settle; otherwise check `haven_get_payment_status` after the payment window before re-quoting). |
|
|
413
|
+
| `MERCHANT_UNRESPONSIVE_AFTER_FUNDING` | The merchant did not answer the paid retry before the timeout — NOT proof of rejection, the merchant may still settle late. On eip3009 Haven's funding leg had succeeded; on erc7710 there is no funding leg (#3000). | eip3009: check `haven_get_payment_status`, retry `haven_complete_mcp_tool` once, and only then call `haven_sweep_delegate` if no settlement appears. erc7710: nothing to sweep and `haven_complete_mcp_tool` has no erc7710 branch — do not retry it; check `haven_get_payment_status` after the payment window and re-quote only if it shows no settlement. |
|
|
407
414
|
|
|
408
415
|
## Payments outside the agent's budget
|
|
409
416
|
|
package/dist/index.cjs
CHANGED
|
@@ -125,7 +125,26 @@ var AgentPaymentNextAction = {
|
|
|
125
125
|
* settled to the merchant. The wallet owner should initiate a sweep to
|
|
126
126
|
* return those funds to the originating Safe.
|
|
127
127
|
*/
|
|
128
|
-
SweepStrandedFunds: "sweep_stranded_funds"
|
|
128
|
+
SweepStrandedFunds: "sweep_stranded_funds",
|
|
129
|
+
/**
|
|
130
|
+
* #2970: a `submitted` erc7710 x402 intent whose settlement window has
|
|
131
|
+
* passed with no on-chain settlement evidence Haven could verify. Distinct
|
|
132
|
+
* from {@link CheckStatusLater}, which this REPLACES once the window is
|
|
133
|
+
* past — but it is not futile: Haven's settlement sweep (120s tick) scans
|
|
134
|
+
* each candidate over its own window plus a 120s clock-skew allowance, so
|
|
135
|
+
* it can still attribute the settlement for a short while after this value
|
|
136
|
+
* first appears. Poll {@link CheckStatusLater}'s tool
|
|
137
|
+
* (`haven_get_payment_status`) once more, roughly two minutes later; if it
|
|
138
|
+
* still shows no evidence, tell the user the goods were delivered but
|
|
139
|
+
* Haven holds no verified settlement evidence for this payment. If the
|
|
140
|
+
* agent holds the merchant's real settlement transaction hash (from
|
|
141
|
+
* `PAYMENT-RESPONSE`'s `transaction` field, or a prior settle/complete
|
|
142
|
+
* result's `settlement_tx_hash`), report it with the hosted
|
|
143
|
+
* `haven_report_settlement_evidence` tool instead of waiting —
|
|
144
|
+
* `haven_report_x402_outcome` takes no hash and refuses a non-`confirmed`
|
|
145
|
+
* intent.
|
|
146
|
+
*/
|
|
147
|
+
AwaitingSettlementEvidence: "awaiting_settlement_evidence"
|
|
129
148
|
};
|
|
130
149
|
var AgentPaymentNextActionAccountAlias = {
|
|
131
150
|
/** Account-vocabulary twin of `fund_safe_or_raise_allowance`; same meaning. */
|
|
@@ -145,12 +164,14 @@ var AgentPaymentFailureCode = {
|
|
|
145
164
|
PriceExceedsMax: "PRICE_EXCEEDS_MAX",
|
|
146
165
|
/** The x402 funding/quote window expired before the signer or hosted settle step could finish. */
|
|
147
166
|
PaymentWindowExpired: "PAYMENT_WINDOW_EXPIRED",
|
|
148
|
-
/** The
|
|
167
|
+
/** The merchant rejected the paid retry. On eip3009 the funding leg had succeeded (sweep);
|
|
168
|
+
* on erc7710 there is no funding leg — nothing to sweep, follow the message (#2983). */
|
|
149
169
|
MerchantRejectedAfterFunding: "MERCHANT_REJECTED_AFTER_FUNDING",
|
|
150
170
|
/** #1300 review: funding is on-chain but the merchant never ANSWERED the
|
|
151
171
|
* paid retry within the timeout. NOT proof of rejection — the merchant
|
|
152
|
-
*
|
|
153
|
-
*
|
|
172
|
+
* may still settle late, so the guidance is verify-then-act. On eip3009
|
|
173
|
+
* the funding leg had succeeded (verify-then-sweep); on erc7710 there is
|
|
174
|
+
* no funding leg — nothing to sweep, follow the message (#3000). */
|
|
154
175
|
MerchantUnresponsiveAfterFunding: "MERCHANT_UNRESPONSIVE_AFTER_FUNDING",
|
|
155
176
|
/**
|
|
156
177
|
* #1307: the caller omitted merchant_url/tool_name (asking Haven to
|
|
@@ -176,7 +197,17 @@ var AgentPaymentFailureCode = {
|
|
|
176
197
|
* asset can represent (truncating it would silently change the user's cap).
|
|
177
198
|
* The fallback is the exact atomic `max_amount`.
|
|
178
199
|
*/
|
|
179
|
-
MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE"
|
|
200
|
+
MaxAmountUnconvertible: "MAX_AMOUNT_UNCONVERTIBLE",
|
|
201
|
+
/**
|
|
202
|
+
* #2979: the merchant answered a `tools/call` probe with its own
|
|
203
|
+
* machine-readable "cannot settle right now" refusal (HTTP 503,
|
|
204
|
+
* `{ error: 'merchant_not_ready', reason_code, ... }`) instead of a 402
|
|
205
|
+
* challenge — e.g. its settlement wallet is out of gas. No 402 was ever
|
|
206
|
+
* issued and no payment was created; this is honest and (per
|
|
207
|
+
* `retry_after_s`, when present) usually transient, unlike a permanent
|
|
208
|
+
* endpoint miss.
|
|
209
|
+
*/
|
|
210
|
+
MerchantNotReady: "MERCHANT_NOT_READY"
|
|
180
211
|
};
|
|
181
212
|
var AgentPaymentRail = {
|
|
182
213
|
/** Standard Haven payment from the user's Safe through an approved delegate allowance. */
|
|
@@ -224,16 +255,18 @@ var AgentPaymentNextActionDescriptions = {
|
|
|
224
255
|
[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.",
|
|
225
256
|
[AgentPaymentNextAction.FundSafeOrRaiseAllowance]: "Stop and tell the user that the account needs to be funded or the agent budget raised before the payment can succeed.",
|
|
226
257
|
[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.",
|
|
227
|
-
[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 account."
|
|
258
|
+
[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 account.",
|
|
259
|
+
[AgentPaymentNextAction.AwaitingSettlementEvidence]: "The settlement window passed with no verified on-chain evidence yet. If you hold the merchant's real settlement transaction hash, report it with haven_report_settlement_evidence. Otherwise, Haven's settlement sweep may still attribute it within about two minutes \u2014 poll getPaymentStatus once more, then tell the user the goods were delivered but unverified if it still shows nothing."
|
|
228
260
|
};
|
|
229
261
|
var AgentPaymentFailureCodeDescriptions = {
|
|
230
262
|
[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.",
|
|
231
263
|
[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.",
|
|
232
|
-
[AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The
|
|
233
|
-
[AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The
|
|
264
|
+
[AgentPaymentFailureCode.MerchantRejectedAfterFunding]: "The merchant rejected the paid retry. eip3009: the funding leg succeeded \u2014 stop retrying the merchant and reconcile stranded delegate funds with haven_sweep_delegate. erc7710: no funding leg, nothing to sweep \u2014 follow the message (re-quote later, or check haven_get_payment_status after the window first).",
|
|
265
|
+
[AgentPaymentFailureCode.MerchantUnresponsiveAfterFunding]: "The merchant did not answer the paid retry before the timeout. The merchant may still settle late. eip3009: the funding leg succeeded \u2014 check haven_get_payment_status, retry haven_complete_mcp_tool once, sweep only if no settlement appears. erc7710: no funding leg, nothing to sweep, and haven_complete_mcp_tool has no erc7710 branch \u2014 do not retry it; check haven_get_payment_status after the payment window and re-quote only if it shows no settlement.",
|
|
234
266
|
[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.",
|
|
235
267
|
[AgentPaymentFailureCode.AmbiguousMaxAmount]: "Both max_amount (atomic units) and max_amount_human (whole tokens) were supplied for one purchase. Nothing was contacted and nothing was spent. Re-send with exactly ONE: max_amount_human for a cap the user stated in tokens, max_amount for an exact atomic figure.",
|
|
236
|
-
[AgentPaymentFailureCode.MaxAmountUnconvertible]: "max_amount_human could not be converted to atomic units against this quote's asset \u2014 either its decimals are unknown to Haven or the cap has more decimal places than the asset supports. Nothing was spent. Round the cap, or re-send it as an exact atomic max_amount."
|
|
268
|
+
[AgentPaymentFailureCode.MaxAmountUnconvertible]: "max_amount_human could not be converted to atomic units against this quote's asset \u2014 either its decimals are unknown to Haven or the cap has more decimal places than the asset supports. Nothing was spent. Round the cap, or re-send it as an exact atomic max_amount.",
|
|
269
|
+
[AgentPaymentFailureCode.MerchantNotReady]: 'The merchant refused the probe with its own "cannot settle right now" signal instead of a 402 challenge. No payment was created. Often transient \u2014 retry later (see retry_after_s in the message, if given) rather than treating this as a broken or wrong endpoint.'
|
|
237
270
|
};
|
|
238
271
|
var AgentPaymentWarningCode = {
|
|
239
272
|
/** No max_amount cap was supplied — the live quoted price was accepted as-is. */
|
|
@@ -264,7 +297,24 @@ var AgentPaymentWarningCode = {
|
|
|
264
297
|
* on-chain policy re-checks at redemption either way; this only says the
|
|
265
298
|
* guidance shown here may be optimistic.
|
|
266
299
|
*/
|
|
267
|
-
AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC"
|
|
300
|
+
AllowanceReadOptimistic: "ALLOWANCE_READ_OPTIMISTIC",
|
|
301
|
+
/**
|
|
302
|
+
* #2991: the quote tools' `expected_settlement_scheme` prediction of what
|
|
303
|
+
* `haven_prepare_catalog_purchase` / `haven_pay_mcp_tool` will actually
|
|
304
|
+
* select could not be computed — the agent's execution rail could not be
|
|
305
|
+
* read from Haven, so `expected_settlement_scheme` is `null` rather than a
|
|
306
|
+
* guess. `accepted_scheme` (the merchant's offer) is unaffected.
|
|
307
|
+
*/
|
|
308
|
+
X402SchemeUnknown: "X402_SCHEME_UNKNOWN",
|
|
309
|
+
/**
|
|
310
|
+
* #2968: the merchant answered 200 and handed over goods, but Haven holds NO
|
|
311
|
+
* on-chain evidence that the payment moved. `settled: false` beside this code
|
|
312
|
+
* is not a failure — it is the absence of proof, and the two must travel
|
|
313
|
+
* together so an agent can tell "the user has the goods" apart from "the
|
|
314
|
+
* money moved". Carries the intent's `expires_at`: after that instant the
|
|
315
|
+
* settlement can no longer land at all.
|
|
316
|
+
*/
|
|
317
|
+
SettlementUnconfirmed: "SETTLEMENT_UNCONFIRMED"
|
|
268
318
|
};
|
|
269
319
|
var AgentPaymentRailDescriptions = {
|
|
270
320
|
[AgentPaymentRail.Direct]: "Standard Haven payment from the user-controlled account, redeeming the agent's on-chain budget delegation.",
|
|
@@ -328,8 +378,18 @@ var MerchantTimeoutError = class extends HavenApiError {
|
|
|
328
378
|
};
|
|
329
379
|
var X402UnexpectedStatusError = class extends HavenApiError {
|
|
330
380
|
x402ErrorCode = "unexpected_non_402_status";
|
|
331
|
-
|
|
332
|
-
|
|
381
|
+
/**
|
|
382
|
+
* #2979: `body` is the merchant's own JSON, when the non-402 response
|
|
383
|
+
* carried one — e.g. the demo merchant's `/mcp` readiness gate answers
|
|
384
|
+
* `503 { error: 'merchant_not_ready', reason_code, ... }`. Optional and
|
|
385
|
+
* best-effort: a non-JSON or unreadable body leaves this `undefined`, same
|
|
386
|
+
* as before this field existed. Consumers key on it (not on the message
|
|
387
|
+
* string) to distinguish an honest, machine-readable merchant refusal from
|
|
388
|
+
* a genuine "this is not the x402 endpoint" miss, which otherwise look
|
|
389
|
+
* identical — both are just "some non-402 status".
|
|
390
|
+
*/
|
|
391
|
+
constructor(message, statusCode, body) {
|
|
392
|
+
super(message, statusCode, body);
|
|
333
393
|
this.name = "X402UnexpectedStatusError";
|
|
334
394
|
}
|
|
335
395
|
};
|
|
@@ -368,6 +428,17 @@ var HavenSigningError = class extends HavenError {
|
|
|
368
428
|
this.name = "HavenSigningError";
|
|
369
429
|
}
|
|
370
430
|
};
|
|
431
|
+
var HavenZeroSettlementHashError = class extends HavenError {
|
|
432
|
+
constructor(paymentId) {
|
|
433
|
+
super(
|
|
434
|
+
"settlement_tx_hash is the zero hash (0x00\u202600), which is never a real settlement transaction \u2014 refused before any report was sent.",
|
|
435
|
+
"ZERO_SETTLEMENT_HASH",
|
|
436
|
+
400,
|
|
437
|
+
paymentId
|
|
438
|
+
);
|
|
439
|
+
this.name = "HavenZeroSettlementHashError";
|
|
440
|
+
}
|
|
441
|
+
};
|
|
371
442
|
var SignerRefusalCode = {
|
|
372
443
|
/** `SUPPORTED_X402_EXPECTED_VERSIONS` in `@haven_ai/signer` does not include the received version. */
|
|
373
444
|
UnsupportedExpectedContextVersion: "UNSUPPORTED_EXPECTED_CONTEXT_VERSION",
|
|
@@ -1116,6 +1187,15 @@ var HavenApiTransport = class {
|
|
|
1116
1187
|
};
|
|
1117
1188
|
|
|
1118
1189
|
// src/payment-mappers.ts
|
|
1190
|
+
function mapParties(raw) {
|
|
1191
|
+
if (!raw) return void 0;
|
|
1192
|
+
return {
|
|
1193
|
+
treasuryAccount: raw.treasury_account,
|
|
1194
|
+
delegate: raw.delegate,
|
|
1195
|
+
delegateAccount: raw.delegate_account,
|
|
1196
|
+
merchant: raw.merchant
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1119
1199
|
function mapPaymentResult(raw, buildExplorerUrl2) {
|
|
1120
1200
|
return {
|
|
1121
1201
|
paymentId: raw.payment_id,
|
|
@@ -1155,6 +1235,7 @@ function mapPaymentStatusResult(raw) {
|
|
|
1155
1235
|
resourceUrl: raw.resource_url,
|
|
1156
1236
|
merchantAddress: raw.merchant_address,
|
|
1157
1237
|
payerAddress: raw.payer_address ?? null,
|
|
1238
|
+
parties: mapParties(raw.parties),
|
|
1158
1239
|
txHash: raw.tx_hash,
|
|
1159
1240
|
expiresAt: raw.expires_at,
|
|
1160
1241
|
chainId: raw.chain_id,
|
|
@@ -1188,10 +1269,13 @@ function mapPaymentReceipt(raw) {
|
|
|
1188
1269
|
rail: raw.rail,
|
|
1189
1270
|
proofStatus: raw.proof_status,
|
|
1190
1271
|
txHash: raw.tx_hash,
|
|
1272
|
+
fundingTxHash: raw.funding_tx_hash ?? null,
|
|
1273
|
+
settlementTxHash: raw.settlement_tx_hash ?? null,
|
|
1191
1274
|
chainId: raw.chain_id,
|
|
1192
1275
|
resourceUrl: raw.resource_url,
|
|
1193
1276
|
merchantAddress: raw.merchant_address,
|
|
1194
1277
|
payerAddress: raw.payer_address,
|
|
1278
|
+
parties: mapParties(raw.parties),
|
|
1195
1279
|
settlementAddress: raw.settlement_address,
|
|
1196
1280
|
tokenSymbol: raw.token_symbol,
|
|
1197
1281
|
tokenAddress: raw.token_address,
|
|
@@ -2915,15 +2999,57 @@ var MerchantCompletion = class {
|
|
|
2915
2999
|
for (let attempt = 0; ; attempt += 1) {
|
|
2916
3000
|
try {
|
|
2917
3001
|
await this.post("/machine-payments/evidence", body);
|
|
2918
|
-
return;
|
|
3002
|
+
return { outcome: "confirmed" };
|
|
2919
3003
|
} catch (err) {
|
|
2920
|
-
const
|
|
2921
|
-
|
|
3004
|
+
const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
|
|
3005
|
+
const retryable = statusCode === EVIDENCE_RETRYABLE_STATUS;
|
|
3006
|
+
if (!retryable) {
|
|
3007
|
+
return { outcome: "refused", statusCode: statusCode ?? 0 };
|
|
3008
|
+
}
|
|
3009
|
+
if (attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
|
|
3010
|
+
return { outcome: "retryable", statusCode };
|
|
3011
|
+
}
|
|
2922
3012
|
await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
|
|
2923
3013
|
}
|
|
2924
3014
|
}
|
|
2925
3015
|
}
|
|
3016
|
+
/**
|
|
3017
|
+
* #2972: report the merchant's REAL settlement transaction hash for an
|
|
3018
|
+
* erc7710 x402 payment out of band — the remedy #2970's guidance could not
|
|
3019
|
+
* name, because no hosted tool accepted a hash. An agent reaches this after
|
|
3020
|
+
* `haven_settle_mcp_tool` / `haven_complete_mcp_tool` answered
|
|
3021
|
+
* `DELIVERED_UNSETTLED` or `SETTLEMENT_PENDING`, or after
|
|
3022
|
+
* `haven_get_payment_status` reports `awaiting_settlement_evidence` — in
|
|
3023
|
+
* every one of those cases the agent may be holding the merchant's own
|
|
3024
|
+
* `PAYMENT-RESPONSE.transaction` while Haven has nothing.
|
|
3025
|
+
*
|
|
3026
|
+
* Reuses `reportEvidence` — same backend seam
|
|
3027
|
+
* (`POST /machine-payments/evidence` → `observeErc7710Settlement`,
|
|
3028
|
+
* fail-closed — see `settlement-observed.ts`), same three-outcome contract.
|
|
3029
|
+
* `resourceUrl` and `merchantStatus` are omitted: this call has no fresh
|
|
3030
|
+
* merchant HTTP exchange to read either from, and both are optional at the
|
|
3031
|
+
* backend (see the parameter doc on `reportEvidence`).
|
|
3032
|
+
*
|
|
3033
|
+
* The zero hash is refused HERE, client-side, before any network call —
|
|
3034
|
+
* never posted. `isZeroSettlementTxHash` is the same recognizer the #2970
|
|
3035
|
+
* gate uses, so a caller cannot "fix" a missing hash by reporting the demo
|
|
3036
|
+
* merchant's own marker and getting a different verdict than the settle
|
|
3037
|
+
* path already gave it.
|
|
3038
|
+
*/
|
|
3039
|
+
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
3040
|
+
if (isZeroSettlementTxHash(settlementTxHash)) {
|
|
3041
|
+
throw new HavenZeroSettlementHashError(paymentId);
|
|
3042
|
+
}
|
|
3043
|
+
return this.reportEvidence({
|
|
3044
|
+
paymentId,
|
|
3045
|
+
rail: "x402",
|
|
3046
|
+
txHash: settlementTxHash
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
2926
3049
|
};
|
|
3050
|
+
function isZeroSettlementTxHash(hash) {
|
|
3051
|
+
return typeof hash === "string" && /^0x0+$/i.test(hash);
|
|
3052
|
+
}
|
|
2927
3053
|
function parseMerchantSettlement(header) {
|
|
2928
3054
|
if (!header) return {};
|
|
2929
3055
|
const parsed = parseProtocolReceiptHeader(header);
|
|
@@ -3425,7 +3551,7 @@ var HavenClient = class {
|
|
|
3425
3551
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
3426
3552
|
const raw = await this.get(`/catalog${query}`);
|
|
3427
3553
|
let entries = raw.entries.map(mapCatalogEntry);
|
|
3428
|
-
if (options.verified === "verified") entries = entries.filter((e) => e.
|
|
3554
|
+
if (options.verified === "verified") entries = entries.filter((e) => e.verifiedPayable === true);
|
|
3429
3555
|
if (options.verified === "operator") entries = entries.filter((e) => e.source === "operator");
|
|
3430
3556
|
return entries;
|
|
3431
3557
|
}
|
|
@@ -3562,9 +3688,16 @@ var HavenClient = class {
|
|
|
3562
3688
|
const request = snapshotX402Request(url, initialInit);
|
|
3563
3689
|
const response = await this.merchantTransport.fetch(url, initialInit);
|
|
3564
3690
|
if (response.status !== 402) {
|
|
3691
|
+
let body;
|
|
3692
|
+
try {
|
|
3693
|
+
body = await response.clone().json();
|
|
3694
|
+
} catch {
|
|
3695
|
+
body = void 0;
|
|
3696
|
+
}
|
|
3565
3697
|
throw new X402UnexpectedStatusError(
|
|
3566
3698
|
`Expected an x402 quote response with HTTP 402, got HTTP ${response.status}.`,
|
|
3567
|
-
response.status || 400
|
|
3699
|
+
response.status || 400,
|
|
3700
|
+
body
|
|
3568
3701
|
);
|
|
3569
3702
|
}
|
|
3570
3703
|
if (response.headers.get("MACHINE-PAYMENT-CHALLENGE")) {
|
|
@@ -3853,6 +3986,7 @@ var HavenClient = class {
|
|
|
3853
3986
|
} catch {
|
|
3854
3987
|
body = text;
|
|
3855
3988
|
}
|
|
3989
|
+
let evidenceOutcome;
|
|
3856
3990
|
if (!surfaced.ok) {
|
|
3857
3991
|
if (!input.noFundingLeg && fundingTxHash) {
|
|
3858
3992
|
await this.merchantCompletion.recordRetryRejected({
|
|
@@ -3872,9 +4006,10 @@ var HavenClient = class {
|
|
|
3872
4006
|
});
|
|
3873
4007
|
}
|
|
3874
4008
|
} else {
|
|
3875
|
-
const
|
|
4009
|
+
const rawEvidenceTxHash = input.noFundingLeg ? settlement.settlementTxHash ?? void 0 : fundingTxHash ?? void 0;
|
|
4010
|
+
const evidenceTxHash = rawEvidenceTxHash && !isZeroSettlementTxHash(rawEvidenceTxHash) ? rawEvidenceTxHash : void 0;
|
|
3876
4011
|
if (evidenceTxHash) {
|
|
3877
|
-
await this.merchantCompletion.reportEvidence({
|
|
4012
|
+
evidenceOutcome = await this.merchantCompletion.reportEvidence({
|
|
3878
4013
|
paymentId: evidenceContext.paymentId,
|
|
3879
4014
|
rail: "x402",
|
|
3880
4015
|
txHash: evidenceTxHash,
|
|
@@ -3892,7 +4027,8 @@ var HavenClient = class {
|
|
|
3892
4027
|
status: surfaced.status,
|
|
3893
4028
|
ok: surfaced.ok,
|
|
3894
4029
|
body,
|
|
3895
|
-
settlementTxHash: settlement.settlementTxHash ?? void 0
|
|
4030
|
+
settlementTxHash: settlement.settlementTxHash ?? void 0,
|
|
4031
|
+
evidenceOutcome
|
|
3896
4032
|
};
|
|
3897
4033
|
}
|
|
3898
4034
|
/**
|
|
@@ -3907,6 +4043,19 @@ var HavenClient = class {
|
|
|
3907
4043
|
async reportX402MerchantOutcome(input) {
|
|
3908
4044
|
return await this.merchantCompletion.reportMerchantOutcome(input);
|
|
3909
4045
|
}
|
|
4046
|
+
/**
|
|
4047
|
+
* #2972: report the merchant's real settlement transaction hash for an
|
|
4048
|
+
* erc7710 x402 payment — the remedy for `DELIVERED_UNSETTLED` /
|
|
4049
|
+
* `SETTLEMENT_PENDING` / `awaiting_settlement_evidence` when the agent
|
|
4050
|
+
* holds the hash (`PAYMENT-RESPONSE.transaction`, or a prior settle/
|
|
4051
|
+
* complete result's `settlement_tx_hash`) and Haven does not. See
|
|
4052
|
+
* `MerchantCompletion.reportSettlementEvidence` for the fail-closed
|
|
4053
|
+
* verification this posts into (`observeErc7710Settlement`) and the
|
|
4054
|
+
* client-side zero-hash refusal.
|
|
4055
|
+
*/
|
|
4056
|
+
async reportSettlementEvidence(paymentId, settlementTxHash) {
|
|
4057
|
+
return await this.merchantCompletion.reportSettlementEvidence(paymentId, settlementTxHash);
|
|
4058
|
+
}
|
|
3910
4059
|
/**
|
|
3911
4060
|
* GET /x402/:id/merchant-call-context — the settle-leg twin of #1263's
|
|
3912
4061
|
* sign-context fetch (#1307). Re-serves the stored merchant MCP-tool call
|
|
@@ -4081,8 +4230,8 @@ var toolDescriptions = {
|
|
|
4081
4230
|
// MACHINE-PAYMENT-CHALLENGE was never produced by anything besides the now
|
|
4082
4231
|
// deleted `/demo/mpp/*` route. Use the x402 fragments above instead.
|
|
4083
4232
|
getPaymentStatus: {
|
|
4084
|
-
summary: "Fetch structured Haven payment status
|
|
4085
|
-
behavior: "
|
|
4233
|
+
summary: "Fetch structured Haven payment status for agent recovery.",
|
|
4234
|
+
behavior: "State: phase, nextAction, rail, amount, merchant, resource, idempotency, message; parties: treasury/delegate/delegateAccount/merchant. awaiting_settlement_evidence: poll once, else unverified.",
|
|
4086
4235
|
nextActionGuidance: ""
|
|
4087
4236
|
},
|
|
4088
4237
|
getResumeState: {
|
|
@@ -4122,7 +4271,7 @@ var toolDescriptions = {
|
|
|
4122
4271
|
},
|
|
4123
4272
|
discoverTools: {
|
|
4124
4273
|
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.",
|
|
4125
|
-
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. Use verified=verified
|
|
4274
|
+
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. Use verified=verified for entries Haven watched pass a live quote probe (operator-curated or self-submitted) \u2014 domain_verified is the only ownership claim; never treat these badges as proof of merchant honesty, quality, or reliability. 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.",
|
|
4126
4275
|
behavior: "Use each entry's suggested_tool field first \u2014 it names the exact next call. Read-only lookup against Haven's curated catalog; entries are periodically re-verified against the live merchant and degraded entries are flagged. Use category for a case-insensitive category filter (for example, VPN or vpn), or search for a product name, category, or description term. Returns name, description, price, rail, resource URL, tool_name, tool_arguments, suggested_tool, and the provenance badges source/domain_verified/verified_payable. 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.",
|
|
4127
4276
|
nextActionGuidance: `Pick an entry and pay it with the tool named in suggested_tool, passing the entry's resource_url, tool_name, and tool_arguments for MCP merchants. Confirm the price from the live pay-tool result (not the catalog), and pass the user's cap as max_amount_human in whole tokens ("no more than 1 USDC" \u2192 max_amount_human: "1") \u2014 never convert it to atomic units by hand.`
|
|
4128
4277
|
},
|
|
@@ -4143,6 +4292,11 @@ var toolDescriptions = {
|
|
|
4143
4292
|
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 \u2014 use haven_pay_x402 instead. Do NOT use for read-only allowance, budget, or what-can-I-spend questions \u2014 use haven_get_allowances.",
|
|
4144
4293
|
behavior: "Sends the requested amount by redeeming the agent's on-chain budget delegation, account to recipient with no funding leg. Budget, recipient and expiry are enforced on-chain while the transfer is prepared, so a request outside them is declined before any money moves and before the agent is asked to sign \u2014 it is never queued for a human to approve later. The agent's signing key signs the account's typed data; Haven never receives the key.",
|
|
4145
4294
|
nextActionGuidance: "On a decline, report the reason to the user and ask them to grant or raise the budget in Haven \u2014 there is nothing to poll and no approval will arrive. After a successful send, poll haven_get_payment_status until nextAction=none."
|
|
4295
|
+
},
|
|
4296
|
+
reportSettlementEvidence: {
|
|
4297
|
+
summary: "Report an erc7710 payment's real settlement transaction hash so Haven can verify it on-chain and confirm the payment.",
|
|
4298
|
+
behavior: "Pass payment_id and settlement_tx_hash (0x + 64 hex chars) \u2014 from PAYMENT-RESPONSE or a prior settlement_tx_hash. Haven verifies on-chain before confirming; a zero, mismatched, or reverted hash is refused. Your own payments only.",
|
|
4299
|
+
nextActionGuidance: "code DELIVERED_UNSETTLED: did not verify, do not retry \u2014 poll haven_get_payment_status. code SETTLEMENT_PENDING (retryable:true): not mined or RPC unreachable \u2014 report the same hash again shortly."
|
|
4146
4300
|
}
|
|
4147
4301
|
};
|
|
4148
4302
|
|
|
@@ -4745,15 +4899,30 @@ present and surface \`message\` or \`error\` verbatim. Common cases:
|
|
|
4745
4899
|
Round the cap, or send an exact atomic \`max_amount\`.
|
|
4746
4900
|
- \`PAYMENT_WINDOW_EXPIRED\`: re-run the quote/prepare tool with the same
|
|
4747
4901
|
\`idempotency_key\`, then sign the fresh payload.
|
|
4902
|
+
- \`MERCHANT_NOT_READY\`: the merchant refused the quote with its own
|
|
4903
|
+
"cannot settle right now" signal (a 503 \`merchant_not_ready\` with a
|
|
4904
|
+
\`reason_code\`) instead of a 402. No payment was created. Tell the user;
|
|
4905
|
+
retry later (the message carries \`retry_after_s\` when the merchant gave
|
|
4906
|
+
one) \u2014 this is not a wrong or broken endpoint.
|
|
4748
4907
|
- \`MERCHANT_REJECTED_AFTER_FUNDING\`: the merchant refused the paid retry.
|
|
4749
|
-
Stop-and-sweep \u2014 stop retrying the
|
|
4750
|
-
\`mcp__haven__haven_sweep_delegate\` to recover stranded
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4908
|
+
On eip3009 (\`rail\` not \`erc7710\`): Stop-and-sweep \u2014 stop retrying the
|
|
4909
|
+
merchant and use \`mcp__haven__haven_sweep_delegate\` to recover stranded
|
|
4910
|
+
delegate funds. On erc7710 there is no funding leg and nothing to sweep:
|
|
4911
|
+
follow the message \u2014 it says whether the merchant declined to settle
|
|
4912
|
+
(re-quote later) or whether to check \`haven_get_payment_status\` after
|
|
4913
|
+
the payment window first.
|
|
4914
|
+
- \`MERCHANT_UNRESPONSIVE_AFTER_FUNDING\`: the merchant never answered the paid
|
|
4915
|
+
retry. This is NOT proof of rejection \u2014 the merchant may still settle late.
|
|
4916
|
+
On eip3009 (\`rail\` not \`erc7710\`), funding confirmed on-chain: Verify-then-sweep,
|
|
4917
|
+
never a blind sweep \u2014 check \`mcp__haven__haven_get_payment_status\`, retry
|
|
4755
4918
|
\`mcp__haven__haven_complete_mcp_tool\` ONCE, and only sweep with
|
|
4756
|
-
\`mcp__haven__haven_sweep_delegate\` if no settlement appears.
|
|
4919
|
+
\`mcp__haven__haven_sweep_delegate\` if no settlement appears. On erc7710
|
|
4920
|
+
there is no funding leg and nothing to sweep, and
|
|
4921
|
+
\`mcp__haven__haven_complete_mcp_tool\` has no erc7710 branch (it refuses a
|
|
4922
|
+
submitted intent) \u2014 do not retry it: the merchant may still redeem the
|
|
4923
|
+
settlement authorization within the payment window, so check
|
|
4924
|
+
\`mcp__haven__haven_get_payment_status\` after that window and re-quote only
|
|
4925
|
+
if it shows no settlement.
|
|
4757
4926
|
- Budget exceeded: tell the user how much remains (from
|
|
4758
4927
|
\`mcp__haven__haven_get_allowances\`) and that they can raise the budget in
|
|
4759
4928
|
Haven.
|
|
@@ -4904,6 +5073,7 @@ exports.HavenPaymentStateError = HavenPaymentStateError;
|
|
|
4904
5073
|
exports.HavenSigningError = HavenSigningError;
|
|
4905
5074
|
exports.HavenTimeoutError = HavenTimeoutError;
|
|
4906
5075
|
exports.HavenUnsupportedSignerVersionError = HavenUnsupportedSignerVersionError;
|
|
5076
|
+
exports.HavenZeroSettlementHashError = HavenZeroSettlementHashError;
|
|
4907
5077
|
exports.MERCHANT_DISCOVERY_PATHS = MERCHANT_DISCOVERY_PATHS;
|
|
4908
5078
|
exports.MerchantTimeoutError = MerchantTimeoutError;
|
|
4909
5079
|
exports.RECEIPT_VERSION = RECEIPT_VERSION;
|
|
@@ -4947,6 +5117,7 @@ exports.isErc7710Option = isErc7710Option;
|
|
|
4947
5117
|
exports.isFundAccountOrRaiseAllowance = isFundAccountOrRaiseAllowance;
|
|
4948
5118
|
exports.isSupportedNodeVersion = isSupportedNodeVersion;
|
|
4949
5119
|
exports.isSweepableChain = isSweepableChain;
|
|
5120
|
+
exports.isZeroSettlementTxHash = isZeroSettlementTxHash;
|
|
4950
5121
|
exports.normalizePaymentRequired = normalizePaymentRequired;
|
|
4951
5122
|
exports.parsePaymentRequired = parsePaymentRequired;
|
|
4952
5123
|
exports.parsePaymentRequiredResponse = parsePaymentRequiredResponse;
|