@haven_ai/sdk 0.11.0-alpha.0 → 0.12.0-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 +37 -4
- package/dist/{base64-D4VZhgUg.d.ts → base64-C5OrJLij.d.ts} +10 -2
- package/dist/{base64-DR4Gl8cD.d.cts → base64-COaV1Sis.d.cts} +10 -2
- package/dist/{chunk-DXBAN4SN.cjs → chunk-4JTLJVBS.cjs} +4 -4
- package/dist/chunk-4JTLJVBS.cjs.map +1 -0
- package/dist/{chunk-ZDJ55LR7.js → chunk-7M3CIW7W.js} +7 -6
- package/dist/chunk-7M3CIW7W.js.map +1 -0
- package/dist/{chunk-I7SL4A6X.js → chunk-UPEDQLAL.js} +55 -5
- package/dist/chunk-UPEDQLAL.js.map +1 -0
- package/dist/{chunk-IKQIIMM6.cjs → chunk-V2ODMLDD.cjs} +38 -37
- package/dist/chunk-V2ODMLDD.cjs.map +1 -0
- package/dist/{chunk-34YJQFCM.js → chunk-WFTGZS24.js} +3 -3
- package/dist/chunk-WFTGZS24.js.map +1 -0
- package/dist/{chunk-MZKLAHBK.cjs → chunk-ZA3NOSO2.cjs} +56 -4
- package/dist/chunk-ZA3NOSO2.cjs.map +1 -0
- package/dist/edge.cjs +93 -93
- package/dist/edge.d.cts +11 -8
- package/dist/edge.d.ts +11 -8
- package/dist/edge.js +4 -4
- package/dist/index.cjs +441 -355
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -15
- package/dist/index.d.ts +22 -15
- package/dist/index.js +125 -47
- package/dist/index.js.map +1 -1
- package/dist/{receipt-_bWCELEH.d.cts → receipt-Dy77vCea.d.cts} +5 -4
- package/dist/{receipt-CbD8L0WL.d.ts → receipt-DyUInpvF.d.ts} +5 -4
- package/dist/test-support.cjs +21 -21
- package/dist/test-support.d.cts +2 -2
- package/dist/test-support.d.ts +2 -2
- package/dist/test-support.js +2 -2
- package/dist/{types-DKmvjXK6.d.cts → types-CAvsjRKi.d.cts} +144 -2
- package/dist/{types-DKmvjXK6.d.ts → types-CAvsjRKi.d.ts} +144 -2
- package/package.json +1 -1
- package/dist/chunk-34YJQFCM.js.map +0 -1
- package/dist/chunk-DXBAN4SN.cjs.map +0 -1
- package/dist/chunk-I7SL4A6X.js.map +0 -1
- package/dist/chunk-IKQIIMM6.cjs.map +0 -1
- package/dist/chunk-MZKLAHBK.cjs.map +0 -1
- package/dist/chunk-ZDJ55LR7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { decodeBase64Json, selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-
|
|
2
|
-
export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-
|
|
3
|
-
import { verifyPaymentReceipt } from './chunk-
|
|
4
|
-
export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-
|
|
5
|
-
import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-
|
|
6
|
-
export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-
|
|
1
|
+
import { decodeBase64Json, selectStandardPaymentOption, buildX402IdempotencyKey, x402AuthorizationAmount, assertBoundDirectPaymentUserOp, assertFundingLegPaysDelegate, HavenTypedDataRefusedError, assertOwnSettlementChild, parsePaymentRequiredResponse, x402PaymentHeaderNamesSent, SDK_CLIENT_IDENTITY, readClientUpdate, HAVEN_CLIENT_HEADER, isSweepableChain, sweepUsdcAddress, x402PaymentHeaderNamesFor, X402_PAYMENT_HEADER_NAMES, toStandardPaymentRequirements, deriveX402PaymentNonce, encodeBase64Json, x402V2PaymentEnvelope, isErc7710Option, buildSignedTaxDeclaration, encodeTaxDeclarationHeader, selectX402SettlementScheme, selectErc7710PaymentOption, x402AssetTransferMethod, resolveTokenFromAddress, normalizePaymentRequired, X_TAX_DECLARATION_HEADER } from './chunk-7M3CIW7W.js';
|
|
2
|
+
export { DEFAULT_NEXT_TOOL_BY_ACTION, ERC7710_ASSET_TRANSFER_METHOD, HAVEN_CLIENT_HEADER, HAVEN_MINIMUM_NODE_VERSION, HavenTypedDataRefusedError, MAX_TASK_BUDGET_TTL_SECONDS, NEXT_TOOL_SERVER_NAMES, NEXT_TOOL_SERVER_ROLES, SDK_CLIENT_IDENTITY, SDK_VERSION, SWEEP_BASE_CHAIN_ID, SWEEP_BASE_SEPOLIA_CHAIN_ID, SWEEP_BASE_SEPOLIA_USDC_ADDRESS, SWEEP_BASE_USDC_ADDRESS, TRANSFER_WITH_AUTHORIZATION_TYPES, TYPED_DATA_NOT_ALLOWED, X402PaymentHeaderValidationError, X402_LEGACY_PAYMENT_HEADER_NAME, X402_MAX_AUTHORIZATION_WINDOW_SECONDS, X402_PAYMENT_HEADER_NAME, X402_PAYMENT_HEADER_NAMES_SENT, X402_PAYMENT_NONCE_TAG, X402_PAYMENT_REQUIRED_HEADER_NAME, X402_PAYMENT_RESPONSE_HEADER_NAME, X402_SETTLEMENT_FORWARD_MARGIN_SECONDS, assertBoundDirectPaymentUserOp, assertOwnTaskBudgetCloseUserOp, assertOwnTaskChild, buildSweepAuthorizationMessage, buildSweepTypedData, buildX402ExpectedMessage, compareNodeVersions, createNextStepBuilder, decodeBase64Json, decodeBase64Utf8, defaultNextToolFor, deriveX402PaymentNonce, encodeBase64Json, encodeBase64Utf8, encodePaymentProof, hashDelegation, havenClientIdentity, isErc7710Option, isSupportedNodeVersion, isSweepableChain, isTaskChildTypedData, normalizePaymentRequired, parseNextTool, parsePaymentRequired, parsePaymentRequiredResponse, readClientUpdate, renderNextTool, resolveTokenBySymbol, resolveTokenFromAddress, selectErc7710PaymentOption, selectPaymentOption, selectStandardPaymentOption, selectX402SettlementScheme, sweepUsdcAddress, sweepUsdcDomain, toStandardPaymentRequirements, unsupportedNodeVersionMessage, validateStandardX402PaymentHeader, x402AssetTransferMethod, x402AuthorizationAmount, x402FacilitatorAddresses, x402PaymentHeaderNamesFor, x402RetryHeadersFor, x402V2PaymentEnvelope } from './chunk-7M3CIW7W.js';
|
|
3
|
+
import { verifyPaymentReceipt } from './chunk-WFTGZS24.js';
|
|
4
|
+
export { RECEIPT_VERSION, verifyPaymentReceipt } from './chunk-WFTGZS24.js';
|
|
5
|
+
import { HavenSigningError, HavenError, DEFAULT_CONFIRMATION_TIMEOUT_MS, HavenApiError, assertUserOpTypedDataBinding, AgentPaymentRail, AgentPaymentPhase, HavenTimeoutError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, X402AlreadySettledError, MerchantTimeoutError, AgentPaymentWarningCode, AgentPaymentNextAction, FundingConfirmationUnknownError, FundingRevertedError, HavenPaymentStateError, HavenZeroSettlementHashError, X402Erc7710AlreadySettledError, chainIdForNetwork } from './chunk-UPEDQLAL.js';
|
|
6
|
+
export { AGENT_PAYMENT_FAILURE_CODE_VALUES, AGENT_PAYMENT_NEXT_ACTION_VALUES, AGENT_PAYMENT_PHASE_VALUES, AGENT_PAYMENT_RAIL_VALUES, AgentPaymentFailureCode, AgentPaymentFailureCodeDescriptions, AgentPaymentFailureCodeSchema, AgentPaymentNextAction, AgentPaymentNextActionDescriptions, AgentPaymentNextActionSchema, AgentPaymentPhase, AgentPaymentPhaseDescriptions, AgentPaymentPhaseSchema, AgentPaymentRail, AgentPaymentRailDescriptions, AgentPaymentRailSchema, AgentPaymentWarningCode, CONNECTOR_PACKAGE_NAME, DEFAULT_CONFIRMATION_TIMEOUT_MS, DIRECT_SIGN_CONTEXT_VERSION, ENTRY_POINT_V07, FundingConfirmationUnknownError, FundingRevertedError, HAVEN_CONNECTOR_CHANNEL, HYBRID_DELEGATOR_DOMAIN_NAME, HYBRID_DELEGATOR_DOMAIN_VERSION, HavenApiError, HavenError, HavenPaymentStateError, HavenSigningError, HavenTimeoutError, HavenUnsupportedSignerVersionError, HavenUserOpBindingError, HavenZeroSettlementHashError, MerchantTimeoutError, PACKED_USER_OPERATION_FIELDS, ROOT_AUTHORITY, SIGNER_UPDATE_FALLBACK, SignerRefusalCode, TASK_SIGN_CONTEXT_VERSION, X402AlreadySettledError, X402Erc7710AlreadySettledError, X402TaskBudgetMismatchError, X402UnexpectedStatusError, assertUserOpTypedDataBinding, connectorRerunCommand, connectorSpec, connectorUpgradeCommand, deriveDelegateAccountAddress, isConnectorChannel, isPackedUserOperationTypedData, packedUserOperationHash, resolveConnectorChannel, signerUpdateFallback, verifySettlementChild } from './chunk-UPEDQLAL.js';
|
|
7
7
|
import { hashTypedData } from 'viem';
|
|
8
8
|
import { ethers } from 'ethers';
|
|
9
9
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
@@ -143,12 +143,16 @@ var HavenApiTransport = class {
|
|
|
143
143
|
return data;
|
|
144
144
|
} catch (err) {
|
|
145
145
|
if (err instanceof HavenApiError) throw err;
|
|
146
|
+
const failureCause = { kind: "no_answer", method };
|
|
146
147
|
if (err instanceof Error && err.name === "AbortError") {
|
|
147
|
-
throw new HavenApiError(`Request to ${path} timed out`, 408);
|
|
148
|
+
throw new HavenApiError(`Request to ${path} timed out`, 408, void 0, void 0, failureCause);
|
|
148
149
|
}
|
|
149
150
|
throw new HavenApiError(
|
|
150
151
|
`Request to ${path} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
151
|
-
0
|
|
152
|
+
0,
|
|
153
|
+
void 0,
|
|
154
|
+
void 0,
|
|
155
|
+
failureCause
|
|
152
156
|
);
|
|
153
157
|
} finally {
|
|
154
158
|
clearTimeout(timeout);
|
|
@@ -354,6 +358,24 @@ function mapPaymentReceipt(raw) {
|
|
|
354
358
|
if ("approval_request_id" in raw) {
|
|
355
359
|
receipt.approvalRequestId = raw.approval_request_id ?? null;
|
|
356
360
|
}
|
|
361
|
+
if ("settlement_scheme" in raw) {
|
|
362
|
+
receipt.settlementScheme = raw.settlement_scheme ?? null;
|
|
363
|
+
}
|
|
364
|
+
if ("budget_delegation_hash" in raw) {
|
|
365
|
+
receipt.budgetDelegationHash = raw.budget_delegation_hash ?? null;
|
|
366
|
+
}
|
|
367
|
+
if ("delivery_quality" in raw) {
|
|
368
|
+
receipt.deliveryQuality = raw.delivery_quality ?? null;
|
|
369
|
+
}
|
|
370
|
+
if ("delivery_note" in raw) {
|
|
371
|
+
receipt.deliveryNote = raw.delivery_note ?? null;
|
|
372
|
+
}
|
|
373
|
+
if ("delivery_reported_at" in raw) {
|
|
374
|
+
receipt.deliveryReportedAt = raw.delivery_reported_at ?? null;
|
|
375
|
+
}
|
|
376
|
+
if ("delivery_reference" in raw) {
|
|
377
|
+
receipt.deliveryReference = raw.delivery_reference ?? null;
|
|
378
|
+
}
|
|
357
379
|
return receipt;
|
|
358
380
|
}
|
|
359
381
|
|
|
@@ -473,7 +495,7 @@ function throwPaymentStateError(label, raw) {
|
|
|
473
495
|
throw new HavenPaymentStateError(state.message, statusCode, state, raw);
|
|
474
496
|
}
|
|
475
497
|
if (raw.status === "pending_approval") {
|
|
476
|
-
throw
|
|
498
|
+
throw HavenApiError.deterministic(
|
|
477
499
|
`${label} exceeds the agent's on-chain budget and was declined; no approval is pending and none will arrive (payment_id: ${raw.payment_id}).`,
|
|
478
500
|
statusCode,
|
|
479
501
|
raw
|
|
@@ -488,6 +510,7 @@ function throwPaymentStateError(label, raw) {
|
|
|
488
510
|
}
|
|
489
511
|
const paymentId = raw.payment_id ? ` (payment_id: ${raw.payment_id})` : "";
|
|
490
512
|
const message = raw.error ?? `${label} ${raw.status}${paymentId}`;
|
|
513
|
+
if (statusCode >= 500) throw HavenApiError.deterministic(message, statusCode, raw);
|
|
491
514
|
throw new HavenApiError(message, statusCode, raw);
|
|
492
515
|
}
|
|
493
516
|
|
|
@@ -1346,6 +1369,17 @@ function deriveReadiness(status, allowances) {
|
|
|
1346
1369
|
if (status !== "active") return "revoked";
|
|
1347
1370
|
return allowances.some((allowance) => safeBigInt(allowance.remainingAtomic) > 0n) ? "ready" : "needs_approval";
|
|
1348
1371
|
}
|
|
1372
|
+
function deriveBudgetRefillsAt(readiness, allowances) {
|
|
1373
|
+
if (readiness !== "needs_approval" || allowances.length === 0) return null;
|
|
1374
|
+
const times = allowances.map((allowance) => allowance.nextRefillAt).filter((at) => at !== null && !Number.isNaN(Date.parse(at)));
|
|
1375
|
+
if (times.length === 0) return null;
|
|
1376
|
+
return times.reduce((earliest, at) => Date.parse(at) < Date.parse(earliest) ? at : earliest);
|
|
1377
|
+
}
|
|
1378
|
+
function deriveBudgetEndedAt(readiness, allowances, budgetEndedAt) {
|
|
1379
|
+
if (readiness !== "needs_approval" || allowances.length > 0) return null;
|
|
1380
|
+
if (!budgetEndedAt || Number.isNaN(Date.parse(budgetEndedAt))) return null;
|
|
1381
|
+
return budgetEndedAt;
|
|
1382
|
+
}
|
|
1349
1383
|
function formatRemainingDisplay(tokenAddress, tokenSymbol, remainingAtomic) {
|
|
1350
1384
|
const token = resolveTokenFromAddress(tokenAddress);
|
|
1351
1385
|
return token ? `${formatAtomicAmount(safeBigInt(remainingAtomic), token.decimals)} ${tokenSymbol}` : `${remainingAtomic} ${tokenSymbol} (atomic; unknown decimals)`;
|
|
@@ -1514,11 +1548,23 @@ var AccountReads = class {
|
|
|
1514
1548
|
isResetPending: allowance.onchain.isResetPending,
|
|
1515
1549
|
// #3731: same provenance as the detailed read's field — the compact
|
|
1516
1550
|
// row is a projection of the same allowance, never a second source.
|
|
1517
|
-
fundsCoverRemaining: allowance.fundsCoverRemaining
|
|
1551
|
+
fundsCoverRemaining: allowance.fundsCoverRemaining,
|
|
1552
|
+
nextRefillAt: allowance.nextRefillAt
|
|
1518
1553
|
};
|
|
1519
1554
|
});
|
|
1520
1555
|
const readiness = deriveReadiness(agent.status, allowances);
|
|
1521
|
-
|
|
1556
|
+
const budgetRefillsAt = deriveBudgetRefillsAt(readiness, allowances);
|
|
1557
|
+
const budgetEndedAt = deriveBudgetEndedAt(readiness, allowances, allowanceSummary.budgetEndedAt);
|
|
1558
|
+
return {
|
|
1559
|
+
...agent,
|
|
1560
|
+
readiness,
|
|
1561
|
+
spend_authority_readiness: readiness,
|
|
1562
|
+
budgetRefillsAt,
|
|
1563
|
+
budgetEndedAt,
|
|
1564
|
+
allowances,
|
|
1565
|
+
taskBudgets,
|
|
1566
|
+
pendingSubBudgetSignatures
|
|
1567
|
+
};
|
|
1522
1568
|
}
|
|
1523
1569
|
/**
|
|
1524
1570
|
* #3329: `GET /task-budgets?status=open`, mapped to the condensed summary
|
|
@@ -1614,6 +1660,9 @@ var AccountReads = class {
|
|
|
1614
1660
|
accountAddress: raw.account_address,
|
|
1615
1661
|
delegateAddress: raw.delegate_address,
|
|
1616
1662
|
chainId: raw.chain_id,
|
|
1663
|
+
// #4352: a non-empty parseable date → itself; anything else (absent on
|
|
1664
|
+
// an older backend, null when no budget has ended) → null.
|
|
1665
|
+
budgetEndedAt: typeof raw.budget_ended_at === "string" && !Number.isNaN(Date.parse(raw.budget_ended_at)) ? raw.budget_ended_at : null,
|
|
1617
1666
|
supersededBy: mapSupersession(raw.superseded_by),
|
|
1618
1667
|
allowances: raw.allowances.map((allowance) => ({
|
|
1619
1668
|
id: allowance.id,
|
|
@@ -1632,6 +1681,9 @@ var AccountReads = class {
|
|
|
1632
1681
|
// "unverifiable", not a missing key. The key is absent on the wire
|
|
1633
1682
|
// when the remaining is 0; that maps to null here too.
|
|
1634
1683
|
fundsCoverRemaining: allowance.funds_cover_remaining === true || allowance.funds_cover_remaining === false ? allowance.funds_cover_remaining : null,
|
|
1684
|
+
// #4355: a non-empty string → itself; anything else (absent on an
|
|
1685
|
+
// older backend, null when the budget never refills) → null.
|
|
1686
|
+
nextRefillAt: typeof allowance.next_refill_at === "string" && allowance.next_refill_at !== "" ? allowance.next_refill_at : null,
|
|
1635
1687
|
remainingDisplay: formatRemainingDisplay(allowance.token_address, allowance.token_symbol, allowance.onchain.remaining),
|
|
1636
1688
|
onchain: {
|
|
1637
1689
|
amount: allowance.onchain.amount,
|
|
@@ -2257,7 +2309,7 @@ var X402FundingLeg = class {
|
|
|
2257
2309
|
}
|
|
2258
2310
|
this.assertSignableAuthorizationState("x402 payment", raw);
|
|
2259
2311
|
if (!raw.sign_data?.hash) {
|
|
2260
|
-
throw
|
|
2312
|
+
throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
|
|
2261
2313
|
}
|
|
2262
2314
|
const sig = await this.signForData(raw.sign_data, {
|
|
2263
2315
|
asset: option.asset,
|
|
@@ -2379,20 +2431,27 @@ var X402FundingLeg = class {
|
|
|
2379
2431
|
* Skipped when `chainRpcs` does not include the chain; in that case Haven's
|
|
2380
2432
|
* backend has already confirmed on-chain submission and callers accept the
|
|
2381
2433
|
* small propagation window as a trade-off for not configuring an RPC URL.
|
|
2434
|
+
*
|
|
2435
|
+
* #4470: the two outcomes are typed apart. ethers 6 `waitForTransaction`
|
|
2436
|
+
* REJECTS on its timeout (and on an RPC failure) rather than resolving
|
|
2437
|
+
* null, so that path is `FundingConfirmationUnknownError` — funding may
|
|
2438
|
+
* have moved. A mined receipt with `status !== 1` is `FundingRevertedError`.
|
|
2382
2439
|
*/
|
|
2383
2440
|
async waitForFundingTx(txHash, chainId, timeoutMs = 3e4) {
|
|
2384
2441
|
if (!txHash || !chainId) return;
|
|
2385
2442
|
const rpcUrl = this.chainRpcs[chainId];
|
|
2386
2443
|
if (!rpcUrl) return;
|
|
2387
2444
|
const provider = createJsonRpcProvider(rpcUrl);
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2445
|
+
let onChainReceipt;
|
|
2446
|
+
try {
|
|
2447
|
+
onChainReceipt = await provider.waitForTransaction(txHash, 1, timeoutMs);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
throw new FundingConfirmationUnknownError(txHash, chainId, err instanceof Error ? err.message : String(err));
|
|
2450
|
+
}
|
|
2451
|
+
if (!onChainReceipt) {
|
|
2452
|
+
throw new FundingConfirmationUnknownError(txHash, chainId, "no receipt within the timeout window");
|
|
2395
2453
|
}
|
|
2454
|
+
if (onChainReceipt.status !== 1) throw new FundingRevertedError(txHash, chainId);
|
|
2396
2455
|
}
|
|
2397
2456
|
/**
|
|
2398
2457
|
* Can the delegate EOA still fund an authorization for `amountAtomic`?
|
|
@@ -2574,7 +2633,7 @@ var X402Erc7710 = class {
|
|
|
2574
2633
|
...options.subBudgetId ? { subBudgetId: options.subBudgetId } : {}
|
|
2575
2634
|
});
|
|
2576
2635
|
if (!raw.payment_id) {
|
|
2577
|
-
throw
|
|
2636
|
+
throw HavenApiError.deterministic("No payment_id returned from x402/authorize", 500, raw);
|
|
2578
2637
|
}
|
|
2579
2638
|
if (raw.status === "confirmed" && raw.tx_hash) {
|
|
2580
2639
|
const requestedUrl = options.resourceUrl ?? paymentRequired.resource?.url;
|
|
@@ -2591,7 +2650,7 @@ var X402Erc7710 = class {
|
|
|
2591
2650
|
}
|
|
2592
2651
|
const signData = raw.sign_data;
|
|
2593
2652
|
if (signData?.signature_scheme !== "eip712_delegation" || !signData.typed_data) {
|
|
2594
|
-
throw
|
|
2653
|
+
throw HavenApiError.deterministic(
|
|
2595
2654
|
`x402/authorize did not return an erc7710 settlement child (signature_scheme was ${JSON.stringify(signData?.signature_scheme)}). Refusing to sign a payload this path did not ask for.`,
|
|
2596
2655
|
500,
|
|
2597
2656
|
raw
|
|
@@ -2634,7 +2693,7 @@ var X402Erc7710 = class {
|
|
|
2634
2693
|
throw err;
|
|
2635
2694
|
}
|
|
2636
2695
|
if (!settled.payment_header) {
|
|
2637
|
-
throw
|
|
2696
|
+
throw HavenApiError.deterministic(
|
|
2638
2697
|
"x402 settle returned no payment_header \u2014 the merchant cannot be retried.",
|
|
2639
2698
|
500,
|
|
2640
2699
|
settled
|
|
@@ -2805,10 +2864,10 @@ var MerchantCompletion = class {
|
|
|
2805
2864
|
}
|
|
2806
2865
|
async retryRequest(url, initialInit, paymentRequired, receipt) {
|
|
2807
2866
|
if (!receipt.accepted) {
|
|
2808
|
-
throw
|
|
2867
|
+
throw HavenApiError.deterministic("No accepted x402 option was recorded for payment retry", 500);
|
|
2809
2868
|
}
|
|
2810
2869
|
if (!receipt.paymentHeader) {
|
|
2811
|
-
throw
|
|
2870
|
+
throw HavenApiError.deterministic("No x402 payment header was returned for payment retry", 500);
|
|
2812
2871
|
}
|
|
2813
2872
|
let retryInit = initialInit;
|
|
2814
2873
|
if (this.getTaxDeclarationHeader && this.delegateKey) {
|
|
@@ -3194,15 +3253,16 @@ var MerchantCompletion = class {
|
|
|
3194
3253
|
};
|
|
3195
3254
|
for (let attempt = 0; ; attempt += 1) {
|
|
3196
3255
|
const result = await this.postEvidenceOnce(body);
|
|
3197
|
-
if (result.outcome !== "retryable" || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
|
|
3256
|
+
if (result.outcome !== "retryable" || result.statusCode !== EVIDENCE_RETRYABLE_STATUS || attempt >= EVIDENCE_RETRY_DELAYS_MS.length) {
|
|
3198
3257
|
return result;
|
|
3199
3258
|
}
|
|
3200
3259
|
await this.sleep(EVIDENCE_RETRY_DELAYS_MS[attempt]);
|
|
3201
3260
|
}
|
|
3202
3261
|
}
|
|
3203
3262
|
/**
|
|
3204
|
-
* ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503
|
|
3205
|
-
*
|
|
3263
|
+
* ONE evidence attempt — the body of `reportEvidence`'s loop above. A 503,
|
|
3264
|
+
* or no verdict at all (a non-answer or a 408, #4470), answers `retryable`
|
|
3265
|
+
* immediately; the caller decides whether to sleep and
|
|
3206
3266
|
* re-attempt (`reportEvidence`, and — #4215 — the local settlement report
|
|
3207
3267
|
* in `retryRequest`) or hand the outcome to its consumer (#3764 D2, the
|
|
3208
3268
|
* hosted settlement report).
|
|
@@ -3213,7 +3273,8 @@ var MerchantCompletion = class {
|
|
|
3213
3273
|
return { outcome: "confirmed" };
|
|
3214
3274
|
} catch (err) {
|
|
3215
3275
|
const statusCode = err instanceof HavenApiError ? err.statusCode : void 0;
|
|
3216
|
-
|
|
3276
|
+
const noVerdict = statusCode === 408 || err instanceof HavenApiError && err.failureCause?.kind === "no_answer";
|
|
3277
|
+
if (statusCode !== EVIDENCE_RETRYABLE_STATUS && !noVerdict) {
|
|
3217
3278
|
const reason = refusalReasonFromBody(err);
|
|
3218
3279
|
const errorCode = refusalErrorCodeFromBody(err);
|
|
3219
3280
|
return {
|
|
@@ -3634,7 +3695,7 @@ var HavenClient = class {
|
|
|
3634
3695
|
}
|
|
3635
3696
|
const fundingTo = options.delegateAddress ?? (await this.getAgent()).delegateAddress;
|
|
3636
3697
|
if (!fundingTo) {
|
|
3637
|
-
throw
|
|
3698
|
+
throw HavenApiError.deterministic("Authenticated agent has no delegate address registered.", 502);
|
|
3638
3699
|
}
|
|
3639
3700
|
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
3640
3701
|
const raw = await this.post("/x402", {
|
|
@@ -3672,7 +3733,7 @@ var HavenClient = class {
|
|
|
3672
3733
|
throwPaymentStateError("x402 payment", raw);
|
|
3673
3734
|
}
|
|
3674
3735
|
if (!raw.sign_data?.hash) {
|
|
3675
|
-
throw
|
|
3736
|
+
throw HavenApiError.deterministic("No sign_hash returned from x402/authorize", 500, raw);
|
|
3676
3737
|
}
|
|
3677
3738
|
if (raw.sign_data.signature_scheme !== void 0 && !raw.sign_data.typed_data) {
|
|
3678
3739
|
throw new HavenSigningError(
|
|
@@ -3680,7 +3741,7 @@ var HavenClient = class {
|
|
|
3680
3741
|
);
|
|
3681
3742
|
}
|
|
3682
3743
|
if (!raw.x402_expected_auth) {
|
|
3683
|
-
throw
|
|
3744
|
+
throw HavenApiError.deterministic("No x402 expected-context binding returned from x402/authorize", 500, raw);
|
|
3684
3745
|
}
|
|
3685
3746
|
return {
|
|
3686
3747
|
paymentId: raw.payment_id,
|
|
@@ -3910,6 +3971,12 @@ var HavenClient = class {
|
|
|
3910
3971
|
* degrade-to-warning path and the ledger row would still land while the
|
|
3911
3972
|
* purchase proceeded.
|
|
3912
3973
|
*
|
|
3974
|
+
* #4458: an agent whose only budget for the token has ENDED is refused the
|
|
3975
|
+
* same way with `error_code: 'delegation_expired'` and `budget_ended_at`
|
|
3976
|
+
* (ISO 8601) — also thrown, also recorded (`source: 'hosted_prepare'`).
|
|
3977
|
+
* The remedy differs: the owner sets a NEW budget, waiting for a period to
|
|
3978
|
+
* reset cannot help.
|
|
3979
|
+
*
|
|
3913
3980
|
* #3518: without `merchantTo`, an agent whose budgets for the token are all
|
|
3914
3981
|
* merchant-locked gets 409 `budget_requires_recipient` (thrown, nothing
|
|
3915
3982
|
* recorded) naming the pins in `budget_recipient_addresses` — repeat the
|
|
@@ -3934,12 +4001,13 @@ var HavenClient = class {
|
|
|
3934
4001
|
/**
|
|
3935
4002
|
* Post-purchase allowance/budget summary for a settled payment (#1310).
|
|
3936
4003
|
*
|
|
3937
|
-
* Reuses the EXACT rail-aware read path {@link getAllowances}
|
|
3938
|
-
*
|
|
3939
|
-
*
|
|
3940
|
-
* the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
4004
|
+
* Reuses the EXACT rail-aware read path {@link getAllowances} uses —
|
|
4005
|
+
* `GET /machine-payments/allowances`, with delegation-rail values coming
|
|
4006
|
+
* from the #1090 `deriveDelegationBudgets`-backed enforcer read, never
|
|
3941
4007
|
* `agent_allowances` — so this can never disagree with
|
|
3942
|
-
* {@link getAllowances} for the same fixture.
|
|
4008
|
+
* {@link getAllowances} for the same fixture. (#1306's catalog-purchase
|
|
4009
|
+
* preflight read the same path until #3054; it now calls
|
|
4010
|
+
* `POST /machine-payments/budget-precheck` instead.) The settled token is
|
|
3943
4011
|
* resolved from {@link getPaymentStatus} so callers pass only
|
|
3944
4012
|
* `paymentId`, never a second haven_get_agent-style round trip.
|
|
3945
4013
|
*
|
|
@@ -4964,7 +5032,7 @@ var toolDescriptions = {
|
|
|
4964
5032
|
getAgent: {
|
|
4965
5033
|
summary: "Return the authenticated agent identity AND its live spend authority in one call: Haven wallet, delegate, chain, raw status, spend_authority_readiness, per-token remaining allowance (atomic + human-readable). The recommended first call in a new session.",
|
|
4966
5034
|
selectionGuidance: "Use this as the session bootstrap, or to confirm identity together with whether the agent has spend authority right now. For per-token detail (configured vs spent vs reset window) use haven_get_allowances.",
|
|
4967
|
-
behavior: `Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. Readiness is AUTHORITY
|
|
5035
|
+
behavior: `Reads identity plus the live spend-authority snapshot \u2014 the active on-chain budget delegation. spend_authority_readiness (readiness is a deprecated alias, same value) is "ready" when at least one token has remaining spend authority, "needs_approval" when the agent is active but has none, and "revoked" when the credential is not active. needs_approval with no allowances[] rows and budgetEndedAt set: the budget ENDED on that date \u2014 ask the owner to set a new one, waiting will not help; with no rows and no budgetEndedAt: nothing approved yet; with rows: spent for this period, refilling at budgetRefillsAt (null: it ends first). Readiness is AUTHORITY \u2014 a budget is live, not that funds are held. Each allowances[] row carries fundsCoverRemaining: false when the account's balance cannot back that row's whole remaining budget \u2014 a heads-up to mention, not a refusal (budgets above the balance are normal; an owner may top up weekly) \u2014 and null when the coverage read failed or the remaining was not read live; the key is absent at remaining 0, and rows for one token are compared alone. Covers hosted identity + on-chain spend authority ONLY \u2014 the hosted server cannot see the LOCAL signer, so "ready" does not mean the signer can start; verify it with a signer tool call or connect --doctor. An over-budget payment is declined before any money moves; there is no approval queue \u2014 ask the owner to grant or raise the budget. allowances[] carries id, tokenAddress, remainingAtomic, remainingDisplay, nextRefillAt per token. taskBudgets[] rows carry maxAtomic/maxDisplay plus spentAtomic/remainingAtomic/remainingDisplay \u2014 what the chain still allows that budget \u2014 and remainingIsFromChain: false marks a failed read (figures null, never the cap); only live rows are listed, each with its status (pending | open | closing) and isExpired. pendingSubBudgetSignatures[] lists sub-budget rows awaiting YOUR signature, with the haven_sign step. Identity fields: id, name, status, accountAddress, delegateAddress, chainId.`,
|
|
4968
5036
|
nextActionGuidance: ""
|
|
4969
5037
|
},
|
|
4970
5038
|
getAllowances: {
|
|
@@ -5286,7 +5354,7 @@ You are an AI agent whose user has no Haven account yet, or has one but has not
|
|
|
5286
5354
|
|
|
5287
5355
|
## What Haven is, in three sentences
|
|
5288
5356
|
|
|
5289
|
-
Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
|
|
5357
|
+
Your user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. The budget also ENDS on a date your user chooses \u2014 30, 90 (default), 180 or 365 days out \u2014 and that day is enforced on-chain too. When the budget ends, your authority to pay ends with it; ask your user to renew it (a new approval on the agent's page, or a new \`budget grant\`). Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.
|
|
5290
5358
|
|
|
5291
5359
|
## The sequence
|
|
5292
5360
|
|
|
@@ -5359,7 +5427,7 @@ If the approval wait times out, nothing is lost \u2014 your agent is registered
|
|
|
5359
5427
|
Call \`haven_get_agent\`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus \`spend_authority_readiness\`:
|
|
5360
5428
|
|
|
5361
5429
|
- \`ready\` \u2014 a budget is live: you have the authority to pay. It does not say the account holds funds \u2014 the \`allowances[]\` rows carry \`funds_cover_remaining\`, and \`false\` there is a heads-up to mention to your user, not a refusal.
|
|
5362
|
-
- \`needs_approval\` \u2014
|
|
5430
|
+
- \`needs_approval\` \u2014 no budget can pay. \`allowances[]\` empty with \`budgetEndedAt\` set: your budget ENDED on that date; waiting will not help \u2014 ask your user to set a new one in their Haven tab. \`allowances[]\` empty with \`budgetEndedAt\` null: nobody approved one yet; ask your user again \u2014 there is no queue. Rows, all spent: it refills at \`budgetRefillsAt\`; tell your user, then wait or ask them to raise it (\`null\`: it ends first; ask them to renew it).
|
|
5363
5431
|
- \`revoked\` \u2014 the credential is not active; ask your user to create a new agent.
|
|
5364
5432
|
|
|
5365
5433
|
\`ready\` covers hosted identity and the budget only, not your local signer. Check that with \`npx -y @haven_ai/connect@<channel> --doctor\`, the same tag your prompt named \u2014 a separate command, so the command-modification rule does not bind it.
|
|
@@ -5395,7 +5463,7 @@ Then:
|
|
|
5395
5463
|
1. Run \`<packages.cli.one_liner> login\`. That command is the manifest's \`packages.cli.one_liner\` (\`/.well-known/haven.json\`) \u2014 run it as given: \`packages.cli.channel\` serves the full spec. It prints a link and a code and does not need my password \u2014 it must never ask for it. Give me the link straight away and wait for me to approve it in my browser.
|
|
5396
5464
|
2. Once I have approved, run \`haven agents connect --name <a name you choose> --budget <amount> --token USDC --period <minutes>\` with the budget I tell you. If I have not given you one, ask me before running it. Add \`--run\` to complete the connection in the same step.
|
|
5397
5465
|
3. ${AGENT_APPROVAL_RELAY_JSON_SENTENCE}
|
|
5398
|
-
4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` means my approval has not landed yet.
|
|
5466
|
+
4. Once I have approved the budget, verify with the \`haven_get_agent\` tool: \`ready\` means I have given you the authority to pay \u2014 it does not confirm the account holds funds, so if its allowances rows carry \`funds_cover_remaining: false\`, tell me and still try the payment; \`needs_approval\` with no allowances rows and \`budgetEndedAt\` set means my budget ended on that date \u2014 tell me, and I will set a new one; with no rows and no \`budgetEndedAt\`, my approval has not landed yet; with rows, the budget is spent \u2014 it refills at \`budgetRefillsAt\` (\`null\`: it ends first), so tell me, and I can raise or renew it.
|
|
5399
5467
|
|
|
5400
5468
|
Two things only I can do: approving that budget with my passkey, and funding the account with USDC \u2014 no ETH, Haven sponsors the gas. Tell me if either is missing rather than working around it.
|
|
5401
5469
|
|
|
@@ -5515,8 +5583,10 @@ Do not guess the wallet address, network, or budget.
|
|
|
5515
5583
|
|
|
5516
5584
|
For instant orientation at the start of a session, read the non-secret
|
|
5517
5585
|
\`agent.json\` the connector wrote to your Haven credential directory (typically
|
|
5518
|
-
\`~/.haven/agents/<
|
|
5519
|
-
|
|
5586
|
+
\`~/.haven/agents/<slug>/agent.json\`, where \`<slug>\` is the wiring name of the
|
|
5587
|
+
\`haven-<slug>\` pair \u2014 only a \`--bare\` run, or a \`--replace\` without \`--name\`,
|
|
5588
|
+
names the directory by the agent id instead; if you don't know the folder, list
|
|
5589
|
+
\`~/.haven/agents/\` to find it). It
|
|
5520
5590
|
holds your agent id, Haven wallet address, network, and *configured* per-token
|
|
5521
5591
|
budget, and contains no keys \u2014 the fastest way to answer "who am I and what may
|
|
5522
5592
|
I spend" with no round trip. If that file is absent (some setups don't write
|
|
@@ -5537,7 +5607,13 @@ spending:
|
|
|
5537
5607
|
the user, not a refusal \u2014 a budget above the balance is a normal setup, so
|
|
5538
5608
|
still try the payment; \`null\` means the coverage read failed or the remaining
|
|
5539
5609
|
figure was not read live; the key is absent when the remaining is 0. Rows for
|
|
5540
|
-
one token are compared alone.
|
|
5610
|
+
one token are compared alone. \`needs_approval\` with no \`allowances[]\`
|
|
5611
|
+
rows and \`budgetEndedAt\` set means the budget ENDED on that date \u2014 waiting
|
|
5612
|
+
will not help, ask the owner to set a new one; with no rows and no
|
|
5613
|
+
\`budgetEndedAt\`, nobody approved a budget yet; with rows (all at remaining 0) the
|
|
5614
|
+
budget is spent for this period, and \`budgetRefillsAt\` says when it refills
|
|
5615
|
+
(\`null\`: it ends before it refills) \u2014 tell the user, then wait or ask them
|
|
5616
|
+
to raise it.
|
|
5541
5617
|
- \`mcp__haven__haven_get_allowances\` \u2014 detailed per-token breakdown
|
|
5542
5618
|
(configured, spent, reset window) when you need more than the summary.
|
|
5543
5619
|
- \`mcp__haven__haven_check_funds\` \u2014 whether the account actually HOLDS at
|
|
@@ -5751,8 +5827,10 @@ for example, an invoice carrying an \`x402_payment_url\` to POST with its
|
|
|
5751
5827
|
report it, the merchant's settlement evidence. (A deposit transfer is bound
|
|
5752
5828
|
to its payee and amount on-chain too; the difference is the record, not
|
|
5753
5829
|
the binding.)
|
|
5754
|
-
- **If the quote says stop, stop.** The quote checks the agent's
|
|
5755
|
-
budget at quote time, and it can check a price you state
|
|
5830
|
+
- **If the quote says stop, stop.** The hosted quote checks the agent's
|
|
5831
|
+
remaining budget at quote time, and it can check a price you state
|
|
5832
|
+
(\`expected_amount\`). On the local runtime (\`@haven_ai/mcp\`) the quote
|
|
5833
|
+
checks only a price you state; the budget is checked when you pay.
|
|
5756
5834
|
When it returns \`safe_to_continue: false\` \u2014 the amount exceeds the budget, or
|
|
5757
5835
|
the price does not match what you expected \u2014 it prefills no cap and names no
|
|
5758
5836
|
next tool: stop and tell the user. If the x402 challenge itself is wrong (a
|
|
@@ -5897,8 +5975,8 @@ only part of it was usable), and an optional \`note\` (max 2000 characters)
|
|
|
5897
5975
|
saying what was wrong. It is evidence only: it moves no money, never changes
|
|
5898
5976
|
\`settled\` or any money field, and works only on your own settled
|
|
5899
5977
|
payments \u2014 another agent's payment is refused. A re-report replaces your
|
|
5900
|
-
earlier verdict; the receipt then carries it beside the payment, so
|
|
5901
|
-
|
|
5978
|
+
earlier verdict; the receipt then carries it beside the payment, so a junk
|
|
5979
|
+
delivery does not read as a success.
|
|
5902
5980
|
### Relay a delivered code or credential to the owner \u2014 immediately, verbatim
|
|
5903
5981
|
|
|
5904
5982
|
If a merchant response carries what the user PAID FOR \u2014 a redemption code, gift
|