@haven_ai/sdk 0.1.2 → 0.1.3
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 +46 -9
- package/dist/index.cjs +345 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -3
- package/dist/index.d.ts +49 -3
- package/dist/index.js +345 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,8 +97,13 @@ Production merchant acceptance, facilitator, settlement, fiat, or acquiring func
|
|
|
97
97
|
Haven natively supports the [x402](https://x402.org) payment protocol. When an API returns HTTP 402, the SDK evaluates the challenge against the agent's approved limits, uses the configured delegate key for the required signature, and retries automatically:
|
|
98
98
|
|
|
99
99
|
```typescript
|
|
100
|
-
// Automatic — fetch() intercepts 402, pays, and retries
|
|
101
|
-
|
|
100
|
+
// Automatic — fetch() intercepts 402, pays, and retries.
|
|
101
|
+
// Use a stable idempotencyKey when one user intent may need manual approval.
|
|
102
|
+
const response = await haven.fetch(
|
|
103
|
+
'https://paid-api.example.com/data',
|
|
104
|
+
undefined,
|
|
105
|
+
{ idempotencyKey: 'paid-api-data-2026-05-22' },
|
|
106
|
+
)
|
|
102
107
|
const data = await response.json()
|
|
103
108
|
|
|
104
109
|
// Manual — parse and authorize the 402 yourself
|
|
@@ -107,7 +112,9 @@ import { parsePaymentRequiredResponse } from '@haven_ai/sdk'
|
|
|
107
112
|
const apiResponse = await fetch('https://paid-api.example.com/data')
|
|
108
113
|
if (apiResponse.status === 402) {
|
|
109
114
|
const paymentRequired = await parsePaymentRequiredResponse(apiResponse)
|
|
110
|
-
const receipt = await haven.authorizeX402(paymentRequired
|
|
115
|
+
const receipt = await haven.authorizeX402(paymentRequired, {
|
|
116
|
+
idempotencyKey: 'paid-api-data-2026-05-22',
|
|
117
|
+
})
|
|
111
118
|
// Retry with { 'X-PAYMENT': receipt.paymentHeader }
|
|
112
119
|
console.log(receipt.explorerUrl)
|
|
113
120
|
}
|
|
@@ -152,6 +159,7 @@ for (const block of response.content) {
|
|
|
152
159
|
| `make_payment` | Request and sign a payment from the user-controlled Safe within approved limits |
|
|
153
160
|
| `get_payment_status` | Check the status of a payment intent or approval request |
|
|
154
161
|
| `authorize_x402_payment` | Authorize a policy-limited x402 payment and return a payment header for an HTTP 402 resource |
|
|
162
|
+
| `resume_x402_payment` | Resume an approved x402 payment and return a merchant payment header without creating a duplicate approval |
|
|
155
163
|
|
|
156
164
|
## Configuration
|
|
157
165
|
|
|
@@ -179,11 +187,12 @@ Surface that to the user: the payment isn't dead, it's waiting for a human to
|
|
|
179
187
|
sign off. Check `getPaymentStatus(payment_id)` or the `get_payment_status`
|
|
180
188
|
tool later instead of retrying in a tight loop.
|
|
181
189
|
|
|
182
|
-
For x402,
|
|
183
|
-
|
|
184
|
-
Haven
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
For x402, approval resume is explicit. If `authorizeX402()` or `haven.fetch()`
|
|
191
|
+
throws `HavenPaymentStateError` with `nextAction: 'wait_for_user_approval'`,
|
|
192
|
+
stop and tell the user the request is waiting in Haven. Do not loop. After the
|
|
193
|
+
user approves, call `getPaymentStatus(payment_id)`. When Haven reports
|
|
194
|
+
`nextAction: 'retry_original_x402_request'`, call `resumeX402Payment()` with the
|
|
195
|
+
same user-intent idempotency key and the original x402 details.
|
|
187
196
|
|
|
188
197
|
```typescript
|
|
189
198
|
try {
|
|
@@ -197,10 +206,38 @@ try {
|
|
|
197
206
|
|
|
198
207
|
const status = await haven.getPaymentStatus('approval-or-payment-id')
|
|
199
208
|
if (status.nextAction === 'retry_original_x402_request') {
|
|
200
|
-
|
|
209
|
+
const response = await haven.resumeX402Payment({
|
|
210
|
+
paymentId: status.paymentId,
|
|
211
|
+
url: 'https://paid-api.example.com/data',
|
|
212
|
+
paymentRequired,
|
|
213
|
+
idempotencyKey: 'paid-api-data-2026-05-22',
|
|
214
|
+
})
|
|
215
|
+
const data = await response.json()
|
|
201
216
|
}
|
|
202
217
|
```
|
|
203
218
|
|
|
219
|
+
For manual HTTP stacks, use `resumeAuthorizedX402()` to get the merchant header
|
|
220
|
+
without retrying the request for you:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const receipt = await haven.resumeAuthorizedX402({
|
|
224
|
+
paymentId: status.paymentId,
|
|
225
|
+
paymentRequired,
|
|
226
|
+
idempotencyKey: 'paid-api-data-2026-05-22',
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
await fetch('https://paid-api.example.com/data', {
|
|
230
|
+
headers: { 'X-PAYMENT': receipt.paymentHeader! },
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For MCP/SSE x402 tools, keep the same MCP session and JSON-RPC payload where the
|
|
235
|
+
merchant requires it: initialize, retain `mcp-session-id`, send the original
|
|
236
|
+
`tools/call`, parse the 402 challenge, wait for approval if needed, then resume
|
|
237
|
+
with the same `payment_id` and retry the original `tools/call` with
|
|
238
|
+
`X-PAYMENT`. Use a stable `idempotencyKey` for the user intent so fresh merchant
|
|
239
|
+
quotes or sessions do not become duplicate Haven approval requests.
|
|
240
|
+
|
|
204
241
|
## Error Handling
|
|
205
242
|
|
|
206
243
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -358,6 +358,9 @@ function buildExplorerUrl(chainId, txHash) {
|
|
|
358
358
|
const base = CHAIN_EXPLORER_TX[chainId ?? 8453] ?? CHAIN_EXPLORER_TX[8453];
|
|
359
359
|
return `${base}/${txHash}`;
|
|
360
360
|
}
|
|
361
|
+
function explorerUrlOrEmpty(chainId, txHash) {
|
|
362
|
+
return txHash ? buildExplorerUrl(chainId, txHash) : "";
|
|
363
|
+
}
|
|
361
364
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
362
365
|
var DEFAULT_CONFIRMATION_TIMEOUT = 9e4;
|
|
363
366
|
var DEFAULT_POLLING_INTERVAL = 3e3;
|
|
@@ -420,6 +423,28 @@ function messageForState(label, status, paymentId, nextAction) {
|
|
|
420
423
|
}
|
|
421
424
|
return `${label} is ${status}; next_action=${nextAction} (payment_id: ${paymentId}).`;
|
|
422
425
|
}
|
|
426
|
+
function sameAddress(a, b) {
|
|
427
|
+
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
428
|
+
}
|
|
429
|
+
function decimalFromUsdcAtomic(value) {
|
|
430
|
+
const amount = BigInt(value);
|
|
431
|
+
const whole = amount / 1000000n;
|
|
432
|
+
const fraction = (amount % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
433
|
+
return fraction ? `${whole}.${fraction}` : whole.toString();
|
|
434
|
+
}
|
|
435
|
+
function normalizeDecimal(value) {
|
|
436
|
+
if (!value.includes(".")) return value.replace(/^0+(?=\d)/, "") || "0";
|
|
437
|
+
const [whole, fraction = ""] = value.split(".");
|
|
438
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
439
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
440
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
441
|
+
}
|
|
442
|
+
function parseMerchantSettlement(header) {
|
|
443
|
+
if (!header) return {};
|
|
444
|
+
const parsed = parseProtocolReceiptHeader(header);
|
|
445
|
+
const tx = typeof parsed?.transaction === "string" ? parsed.transaction : typeof parsed?.txHash === "string" ? parsed.txHash : typeof parsed?.tx_hash === "string" ? parsed.tx_hash : null;
|
|
446
|
+
return { settlementTxHash: tx };
|
|
447
|
+
}
|
|
423
448
|
var HavenClient = class {
|
|
424
449
|
apiKey;
|
|
425
450
|
delegateKey;
|
|
@@ -563,7 +588,7 @@ var HavenClient = class {
|
|
|
563
588
|
*
|
|
564
589
|
* Requires `delegateKey` to be set in the client config.
|
|
565
590
|
*/
|
|
566
|
-
async authorizeX402(paymentRequired) {
|
|
591
|
+
async authorizeX402(paymentRequired, options = {}) {
|
|
567
592
|
if (!this.delegateKey) {
|
|
568
593
|
throw new HavenSigningError(
|
|
569
594
|
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
@@ -579,7 +604,7 @@ var HavenClient = class {
|
|
|
579
604
|
400
|
|
580
605
|
);
|
|
581
606
|
}
|
|
582
|
-
const idempotencyKey = buildX402IdempotencyKey(paymentRequired, option);
|
|
607
|
+
const idempotencyKey = options.idempotencyKey ?? buildX402IdempotencyKey(paymentRequired, option);
|
|
583
608
|
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
584
609
|
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
585
610
|
const inFlight = this.inFlightX402.get(idempotencyKey);
|
|
@@ -605,21 +630,13 @@ var HavenClient = class {
|
|
|
605
630
|
idempotencyKey
|
|
606
631
|
});
|
|
607
632
|
if (raw.success && raw.tx_hash) {
|
|
608
|
-
const receipt2 =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
resourceUrl: paymentRequired.resource.url,
|
|
616
|
-
explorerUrl: raw.explorer_url ?? (raw.tx_hash ? buildExplorerUrl(raw.chain_id, raw.tx_hash) : ""),
|
|
617
|
-
accepted: option,
|
|
618
|
-
paymentHeader,
|
|
619
|
-
merchantTo: raw.merchant_to ?? option.payTo,
|
|
620
|
-
payer: raw.payer ?? raw.safe_address,
|
|
621
|
-
chainId: raw.chain_id ?? chainIdFromNetwork(option.network)
|
|
622
|
-
};
|
|
633
|
+
const receipt2 = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw);
|
|
634
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
635
|
+
return receipt2;
|
|
636
|
+
}
|
|
637
|
+
const state = this.paymentStateFromRaw("x402 payment", raw);
|
|
638
|
+
if (state?.nextAction === "retry_original_x402_request") {
|
|
639
|
+
const receipt2 = this.mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, state);
|
|
623
640
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt2);
|
|
624
641
|
return receipt2;
|
|
625
642
|
}
|
|
@@ -635,24 +652,53 @@ var HavenClient = class {
|
|
|
635
652
|
if (execResult.status !== "confirmed") {
|
|
636
653
|
this.throwPaymentStateError("x402 payment", execResult);
|
|
637
654
|
}
|
|
638
|
-
const receipt =
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
const receipt = this.mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult);
|
|
656
|
+
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
657
|
+
return receipt;
|
|
658
|
+
}
|
|
659
|
+
async resumeAuthorizedX402(input) {
|
|
660
|
+
if (!this.delegateKey) {
|
|
661
|
+
throw new HavenSigningError(
|
|
662
|
+
"delegateKey is required for x402 payments. Pass it in the HavenClient config."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (!this.delegateAddress) {
|
|
666
|
+
throw new HavenSigningError("delegateAddress could not be derived from delegateKey.");
|
|
667
|
+
}
|
|
668
|
+
const option = selectStandardPaymentOption(input.paymentRequired.accepts);
|
|
669
|
+
if (!option) {
|
|
670
|
+
throw new HavenApiError(
|
|
671
|
+
"No compatible payment option found in x402 requirements. Haven supports standard x402 exact payments on Base USDC.",
|
|
672
|
+
400
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const idempotencyKey = input.idempotencyKey ?? buildX402IdempotencyKey(input.paymentRequired, option);
|
|
676
|
+
const cached = this.x402ReceiptCache.get(idempotencyKey);
|
|
677
|
+
if (cached && cached.expiresAt > Date.now()) return cached.receipt;
|
|
678
|
+
const status = await this.getPaymentStatus(input.paymentId);
|
|
679
|
+
this.assertCanResumeX402(status, input.paymentRequired, option);
|
|
680
|
+
const paymentHeader = await this.createStandardX402Header(input.paymentRequired, option);
|
|
681
|
+
const receipt = this.mapX402ReceiptFromStatus(input.paymentRequired, option, paymentHeader, status);
|
|
653
682
|
this.cacheX402Receipt(idempotencyKey, paymentHeader, receipt);
|
|
654
683
|
return receipt;
|
|
655
684
|
}
|
|
685
|
+
async resumeX402Payment(input) {
|
|
686
|
+
const initialInit = this.withX402Wallet(input.init, this.x402PayerAddress());
|
|
687
|
+
let paymentRequired = input.paymentRequired;
|
|
688
|
+
if (!paymentRequired) {
|
|
689
|
+
const response = await globalThis.fetch(input.url, initialInit);
|
|
690
|
+
if (response.status !== 402) {
|
|
691
|
+
throw new HavenApiError("Expected the original x402 request to return HTTP 402 before resuming.", 400);
|
|
692
|
+
}
|
|
693
|
+
paymentRequired = await parsePaymentRequiredResponse(response);
|
|
694
|
+
}
|
|
695
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
696
|
+
paymentId: input.paymentId,
|
|
697
|
+
paymentRequired,
|
|
698
|
+
idempotencyKey: input.idempotencyKey
|
|
699
|
+
});
|
|
700
|
+
return this.retryX402Request(input.url, initialInit, paymentRequired, receipt);
|
|
701
|
+
}
|
|
656
702
|
/**
|
|
657
703
|
* Fetch wrapper that automatically handles HTTP 402 responses.
|
|
658
704
|
*
|
|
@@ -666,7 +712,7 @@ var HavenClient = class {
|
|
|
666
712
|
*
|
|
667
713
|
* Requires `delegateKey` to be set in the client config.
|
|
668
714
|
*/
|
|
669
|
-
async fetch(url, init) {
|
|
715
|
+
async fetch(url, init, options = {}) {
|
|
670
716
|
const initialInit = this.withX402Wallet(init, this.x402PayerAddress());
|
|
671
717
|
const response = await globalThis.fetch(url, initialInit);
|
|
672
718
|
if (response.status !== 402) return response;
|
|
@@ -687,7 +733,10 @@ var HavenClient = class {
|
|
|
687
733
|
}
|
|
688
734
|
return this.fetchWithMachinePayment(url, initialInit, challenge);
|
|
689
735
|
}
|
|
690
|
-
const receipt = await this.authorizeX402(paymentRequired);
|
|
736
|
+
const receipt = await this.authorizeX402(paymentRequired, options);
|
|
737
|
+
return this.retryX402Request(url, initialInit, paymentRequired, receipt);
|
|
738
|
+
}
|
|
739
|
+
async retryX402Request(url, initialInit, paymentRequired, receipt) {
|
|
691
740
|
if (!receipt.accepted) {
|
|
692
741
|
throw new HavenApiError("No accepted x402 option was recorded for payment retry", 500);
|
|
693
742
|
}
|
|
@@ -725,6 +774,14 @@ var HavenClient = class {
|
|
|
725
774
|
}
|
|
726
775
|
);
|
|
727
776
|
}
|
|
777
|
+
const merchantSettlement = parseMerchantSettlement(retryResponse.headers.get("PAYMENT-RESPONSE"));
|
|
778
|
+
if (receipt.merchant && merchantSettlement.settlementTxHash) {
|
|
779
|
+
receipt.merchant.settlementTxHash = merchantSettlement.settlementTxHash;
|
|
780
|
+
receipt.merchant.settlementExplorerUrl = buildExplorerUrl(
|
|
781
|
+
receipt.chainId,
|
|
782
|
+
merchantSettlement.settlementTxHash
|
|
783
|
+
);
|
|
784
|
+
}
|
|
728
785
|
await this.reportMachinePaymentEvidence({
|
|
729
786
|
paymentId: receipt.paymentId,
|
|
730
787
|
rail: "x402",
|
|
@@ -827,6 +884,150 @@ var HavenClient = class {
|
|
|
827
884
|
});
|
|
828
885
|
return retryResponse;
|
|
829
886
|
}
|
|
887
|
+
assertCanResumeX402(status, paymentRequired, option) {
|
|
888
|
+
if (status.rail !== "x402") {
|
|
889
|
+
throw new HavenPaymentStateError(
|
|
890
|
+
`Payment ${status.paymentId} is ${status.rail}, not x402.`,
|
|
891
|
+
409,
|
|
892
|
+
status
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (status.nextAction !== "retry_original_x402_request") {
|
|
896
|
+
throw new HavenPaymentStateError(status.message, PAYMENT_STATE_STATUS_CODES[status.status] ?? 409, status);
|
|
897
|
+
}
|
|
898
|
+
if (!status.txHash) {
|
|
899
|
+
throw new HavenApiError(
|
|
900
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
901
|
+
502,
|
|
902
|
+
status,
|
|
903
|
+
status.paymentId
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (status.resourceUrl && status.resourceUrl !== paymentRequired.resource.url) {
|
|
907
|
+
throw new HavenApiError(
|
|
908
|
+
"x402 resume request does not match the approved resource URL.",
|
|
909
|
+
409,
|
|
910
|
+
{ status, paymentRequired },
|
|
911
|
+
status.paymentId
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
if (status.merchantAddress && !sameAddress(status.merchantAddress, option.payTo)) {
|
|
915
|
+
throw new HavenApiError(
|
|
916
|
+
"x402 resume request does not match the approved merchant.",
|
|
917
|
+
409,
|
|
918
|
+
{ status, selectedPayment: option },
|
|
919
|
+
status.paymentId
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const optionChainId = chainIdFromNetwork(option.network);
|
|
923
|
+
if (status.chainId && optionChainId && status.chainId !== optionChainId) {
|
|
924
|
+
throw new HavenApiError(
|
|
925
|
+
"x402 resume request does not match the approved network.",
|
|
926
|
+
409,
|
|
927
|
+
{ status, selectedPayment: option },
|
|
928
|
+
status.paymentId
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (status.token && status.token !== "USDC") {
|
|
932
|
+
throw new HavenApiError(
|
|
933
|
+
"x402 resume request does not match the approved token.",
|
|
934
|
+
409,
|
|
935
|
+
{ status, selectedPayment: option },
|
|
936
|
+
status.paymentId
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
const approvedAmount = status.amount ? normalizeDecimal(status.amount) : "";
|
|
940
|
+
const requestedAmount = normalizeDecimal(decimalFromUsdcAtomic(option.amount));
|
|
941
|
+
if (approvedAmount && approvedAmount !== requestedAmount) {
|
|
942
|
+
throw new HavenApiError(
|
|
943
|
+
"x402 resume request does not match the approved amount.",
|
|
944
|
+
409,
|
|
945
|
+
{ status, selectedPayment: option },
|
|
946
|
+
status.paymentId
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
mapX402ReceiptFromAuthorization(paymentRequired, option, paymentHeader, raw, execResult) {
|
|
951
|
+
const txHash = execResult?.tx_hash ?? raw.tx_hash ?? "";
|
|
952
|
+
const chainId = execResult?.chain_id ?? raw.chain_id ?? chainIdFromNetwork(option.network);
|
|
953
|
+
const token = execResult?.token ?? raw.token ?? "USDC";
|
|
954
|
+
const amount = execResult?.amount ?? raw.amount ?? decimalFromUsdcAtomic(option.amount);
|
|
955
|
+
const to = execResult?.to ?? raw.to ?? this.delegateAddress ?? "";
|
|
956
|
+
const explorerUrl = execResult?.explorer_url ?? raw.explorer_url ?? explorerUrlOrEmpty(chainId, txHash);
|
|
957
|
+
const merchantTo = execResult?.merchant_to ?? raw.merchant_to ?? option.payTo;
|
|
958
|
+
const payer = raw.payer ?? raw.safe_address ?? raw.sign_data?.components.safe;
|
|
959
|
+
return this.buildX402Receipt({
|
|
960
|
+
paymentId: raw.payment_id,
|
|
961
|
+
txHash,
|
|
962
|
+
token,
|
|
963
|
+
amount,
|
|
964
|
+
to,
|
|
965
|
+
resourceUrl: paymentRequired.resource.url,
|
|
966
|
+
explorerUrl,
|
|
967
|
+
accepted: option,
|
|
968
|
+
paymentHeader,
|
|
969
|
+
merchantTo,
|
|
970
|
+
payer,
|
|
971
|
+
chainId
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
mapX402ReceiptFromStatus(paymentRequired, option, paymentHeader, status) {
|
|
975
|
+
if (!status.txHash) {
|
|
976
|
+
throw new HavenApiError(
|
|
977
|
+
`x402 payment ${status.paymentId} is ready to retry but has no Haven transaction hash.`,
|
|
978
|
+
502,
|
|
979
|
+
status,
|
|
980
|
+
status.paymentId
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return this.buildX402Receipt({
|
|
984
|
+
paymentId: status.paymentId,
|
|
985
|
+
txHash: status.txHash,
|
|
986
|
+
token: status.token || "USDC",
|
|
987
|
+
amount: status.amount || decimalFromUsdcAtomic(option.amount),
|
|
988
|
+
to: this.delegateAddress ?? "",
|
|
989
|
+
resourceUrl: paymentRequired.resource.url,
|
|
990
|
+
explorerUrl: explorerUrlOrEmpty(status.chainId, status.txHash),
|
|
991
|
+
accepted: option,
|
|
992
|
+
paymentHeader,
|
|
993
|
+
merchantTo: status.merchantAddress ?? option.payTo,
|
|
994
|
+
payer: this.x402Wallet,
|
|
995
|
+
chainId: status.chainId || chainIdFromNetwork(option.network)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
buildX402Receipt(input) {
|
|
999
|
+
const fundingExplorerUrl = input.explorerUrl || explorerUrlOrEmpty(input.chainId, input.txHash);
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
paymentId: input.paymentId,
|
|
1003
|
+
txHash: input.txHash,
|
|
1004
|
+
token: input.token,
|
|
1005
|
+
amount: input.amount,
|
|
1006
|
+
to: input.to,
|
|
1007
|
+
resourceUrl: input.resourceUrl,
|
|
1008
|
+
explorerUrl: input.explorerUrl,
|
|
1009
|
+
accepted: input.accepted,
|
|
1010
|
+
paymentHeader: input.paymentHeader,
|
|
1011
|
+
merchantTo: input.merchantTo ?? input.accepted.payTo,
|
|
1012
|
+
payer: input.payer,
|
|
1013
|
+
chainId: input.chainId,
|
|
1014
|
+
haven: {
|
|
1015
|
+
paymentId: input.paymentId,
|
|
1016
|
+
fundingTxHash: input.txHash,
|
|
1017
|
+
fundingExplorerUrl
|
|
1018
|
+
},
|
|
1019
|
+
merchant: {
|
|
1020
|
+
payTo: input.merchantTo ?? input.accepted.payTo
|
|
1021
|
+
},
|
|
1022
|
+
x402: {
|
|
1023
|
+
amount: input.accepted.amount,
|
|
1024
|
+
token: input.token,
|
|
1025
|
+
network: input.accepted.network,
|
|
1026
|
+
asset: input.accepted.asset,
|
|
1027
|
+
resource: input.accepted.resource ?? input.resourceUrl
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
830
1031
|
async createStandardX402Header(paymentRequired, option) {
|
|
831
1032
|
if (!this.delegateKey) {
|
|
832
1033
|
throw new HavenSigningError("delegateKey is required to sign x402 payment headers.");
|
|
@@ -1010,36 +1211,26 @@ var HavenClient = class {
|
|
|
1010
1211
|
}
|
|
1011
1212
|
}
|
|
1012
1213
|
if (toolName === "authorize_x402_payment") {
|
|
1013
|
-
const { url, payTo, amount, asset, network, description } = input;
|
|
1214
|
+
const { url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
1014
1215
|
try {
|
|
1015
|
-
const receipt = await this.authorizeX402(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1216
|
+
const receipt = await this.authorizeX402(
|
|
1217
|
+
this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1218
|
+
{ idempotencyKey }
|
|
1219
|
+
);
|
|
1220
|
+
return this.x402ToolReceipt(receipt);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
return this.toolError(err);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (toolName === "resume_x402_payment") {
|
|
1226
|
+
const { payment_id, url, payTo, amount, asset, network, description, idempotencyKey } = input;
|
|
1227
|
+
try {
|
|
1228
|
+
const receipt = await this.resumeAuthorizedX402({
|
|
1229
|
+
paymentId: payment_id,
|
|
1230
|
+
paymentRequired: this.toolX402PaymentRequired({ url, payTo, amount, asset, network, description }),
|
|
1231
|
+
idempotencyKey
|
|
1028
1232
|
});
|
|
1029
|
-
return
|
|
1030
|
-
success: true,
|
|
1031
|
-
payment_id: receipt.paymentId,
|
|
1032
|
-
tx_hash: receipt.txHash,
|
|
1033
|
-
token: receipt.token,
|
|
1034
|
-
amount: receipt.amount,
|
|
1035
|
-
to: receipt.to,
|
|
1036
|
-
resource_url: receipt.resourceUrl,
|
|
1037
|
-
explorer_url: receipt.explorerUrl,
|
|
1038
|
-
payment_header: receipt.paymentHeader,
|
|
1039
|
-
merchant_to: receipt.merchantTo,
|
|
1040
|
-
payer: receipt.payer,
|
|
1041
|
-
chain_id: receipt.chainId
|
|
1042
|
-
};
|
|
1233
|
+
return this.x402ToolReceipt(receipt);
|
|
1043
1234
|
} catch (err) {
|
|
1044
1235
|
return this.toolError(err);
|
|
1045
1236
|
}
|
|
@@ -1089,6 +1280,41 @@ var HavenClient = class {
|
|
|
1089
1280
|
}
|
|
1090
1281
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
1091
1282
|
}
|
|
1283
|
+
toolX402PaymentRequired(input) {
|
|
1284
|
+
return {
|
|
1285
|
+
x402Version: 2,
|
|
1286
|
+
resource: { url: input.url, description: input.description },
|
|
1287
|
+
accepts: [
|
|
1288
|
+
{
|
|
1289
|
+
scheme: "exact",
|
|
1290
|
+
network: input.network,
|
|
1291
|
+
amount: input.amount,
|
|
1292
|
+
asset: input.asset,
|
|
1293
|
+
payTo: input.payTo,
|
|
1294
|
+
maxTimeoutSeconds: 30
|
|
1295
|
+
}
|
|
1296
|
+
]
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
x402ToolReceipt(receipt) {
|
|
1300
|
+
return {
|
|
1301
|
+
success: true,
|
|
1302
|
+
payment_id: receipt.paymentId,
|
|
1303
|
+
tx_hash: receipt.txHash,
|
|
1304
|
+
token: receipt.token,
|
|
1305
|
+
amount: receipt.amount,
|
|
1306
|
+
to: receipt.to,
|
|
1307
|
+
resource_url: receipt.resourceUrl,
|
|
1308
|
+
explorer_url: receipt.explorerUrl,
|
|
1309
|
+
payment_header: receipt.paymentHeader,
|
|
1310
|
+
merchant_to: receipt.merchantTo,
|
|
1311
|
+
payer: receipt.payer,
|
|
1312
|
+
chain_id: receipt.chainId,
|
|
1313
|
+
haven: receipt.haven,
|
|
1314
|
+
merchant: receipt.merchant,
|
|
1315
|
+
x402: receipt.x402
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1092
1318
|
toolError(err) {
|
|
1093
1319
|
if (err instanceof HavenPaymentStateError) {
|
|
1094
1320
|
return {
|
|
@@ -1297,10 +1523,52 @@ var authorizeX402Schema = {
|
|
|
1297
1523
|
description: {
|
|
1298
1524
|
type: "string",
|
|
1299
1525
|
description: "Description of the resource being paid for"
|
|
1526
|
+
},
|
|
1527
|
+
idempotencyKey: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
description: "Stable caller-supplied key for this user intent. Reuse it when resuming after user approval."
|
|
1300
1530
|
}
|
|
1301
1531
|
},
|
|
1302
1532
|
required: ["url", "payTo", "amount", "asset", "network"]
|
|
1303
1533
|
};
|
|
1534
|
+
var resumeX402Schema = {
|
|
1535
|
+
type: "object",
|
|
1536
|
+
properties: {
|
|
1537
|
+
payment_id: {
|
|
1538
|
+
type: "string",
|
|
1539
|
+
description: "The payment or approval request ID returned by authorize_x402_payment."
|
|
1540
|
+
},
|
|
1541
|
+
url: {
|
|
1542
|
+
type: "string",
|
|
1543
|
+
description: "The original URL that returned HTTP 402."
|
|
1544
|
+
},
|
|
1545
|
+
payTo: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
description: "Payment recipient address from the original 402 response."
|
|
1548
|
+
},
|
|
1549
|
+
amount: {
|
|
1550
|
+
type: "string",
|
|
1551
|
+
description: "Payment amount in atomic units from the original 402 response."
|
|
1552
|
+
},
|
|
1553
|
+
asset: {
|
|
1554
|
+
type: "string",
|
|
1555
|
+
description: "Token contract address from the original 402 response."
|
|
1556
|
+
},
|
|
1557
|
+
network: {
|
|
1558
|
+
type: "string",
|
|
1559
|
+
description: "CAIP-2 chain ID or x402 network from the original 402 response."
|
|
1560
|
+
},
|
|
1561
|
+
description: {
|
|
1562
|
+
type: "string",
|
|
1563
|
+
description: "Description of the resource being paid for."
|
|
1564
|
+
},
|
|
1565
|
+
idempotencyKey: {
|
|
1566
|
+
type: "string",
|
|
1567
|
+
description: "Stable caller-supplied key used for the original authorization."
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
required: ["payment_id", "url", "payTo", "amount", "asset", "network"]
|
|
1571
|
+
};
|
|
1304
1572
|
var authorizeMachinePaymentSchema = {
|
|
1305
1573
|
type: "object",
|
|
1306
1574
|
properties: {
|
|
@@ -1313,7 +1581,8 @@ var authorizeMachinePaymentSchema = {
|
|
|
1313
1581
|
};
|
|
1314
1582
|
var MAKE_PAYMENT_DESCRIPTION = "Request and sign a payment from the user-controlled Safe within approved on-chain limits. Haven authenticates the agent, validates the signed intent, and relays the Safe AllowanceModule transaction; it does not hold keys or control funds. Gnosis Chain tokens: EURe, USDC.e, xDAI. Base tokens: USDC, ETH.";
|
|
1315
1583
|
var GET_STATUS_DESCRIPTION = "Check the status of a previously initiated payment. Accepts payment intent IDs and approval request IDs. Returns the current status, phase, next_action, transaction hash if available, and payment details.";
|
|
1316
|
-
var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key and request a policy-limited Safe AllowanceModule top-up when needed. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. If this returns pending_approval, tell the user it is waiting in Haven, call get_payment_status later, and
|
|
1584
|
+
var AUTHORIZE_X402_DESCRIPTION = "Authorize payment for an HTTP 402 (Payment Required) response. When a paid API returns x402 payment requirements, use this tool to sign with the agent-owned delegate key and request a policy-limited Safe AllowanceModule top-up when needed. Haven relays signed transactions only; the agent key authorizes payment and on-chain limits enforce spend. If this returns pending_approval, tell the user it is waiting in Haven, call get_payment_status later, and use resume_x402_payment only when next_action is retry_original_x402_request. Do not loop retries while approval is pending. Use the returned payment_header as the X-PAYMENT header on the retry request when doing a manual HTTP retry.";
|
|
1585
|
+
var RESUME_X402_DESCRIPTION = "Resume an x402 payment after the user approved it in Haven. Use this only after get_payment_status returns next_action=retry_original_x402_request. It checks the approved payment, validates the original x402 details, and returns a merchant X-PAYMENT header without creating a new approval request.";
|
|
1317
1586
|
var AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION = "Authorize a Haven machine-payment challenge, currently for the internal MPP demo rail. The agent signs the payment, Haven relays it within the on-chain allowance, and the tool returns a proof header for the retry request.";
|
|
1318
1587
|
function claudeTools() {
|
|
1319
1588
|
return [
|
|
@@ -1332,6 +1601,11 @@ function claudeTools() {
|
|
|
1332
1601
|
description: AUTHORIZE_X402_DESCRIPTION,
|
|
1333
1602
|
input_schema: authorizeX402Schema
|
|
1334
1603
|
},
|
|
1604
|
+
{
|
|
1605
|
+
name: "resume_x402_payment",
|
|
1606
|
+
description: RESUME_X402_DESCRIPTION,
|
|
1607
|
+
input_schema: resumeX402Schema
|
|
1608
|
+
},
|
|
1335
1609
|
{
|
|
1336
1610
|
name: "authorize_machine_payment",
|
|
1337
1611
|
description: AUTHORIZE_MACHINE_PAYMENT_DESCRIPTION,
|
|
@@ -1365,6 +1639,14 @@ function openaiTools() {
|
|
|
1365
1639
|
parameters: authorizeX402Schema
|
|
1366
1640
|
}
|
|
1367
1641
|
},
|
|
1642
|
+
{
|
|
1643
|
+
type: "function",
|
|
1644
|
+
function: {
|
|
1645
|
+
name: "resume_x402_payment",
|
|
1646
|
+
description: RESUME_X402_DESCRIPTION,
|
|
1647
|
+
parameters: resumeX402Schema
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1368
1650
|
{
|
|
1369
1651
|
type: "function",
|
|
1370
1652
|
function: {
|