@x402jp/safe-buy 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pay.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { x402Client, x402HTTPClient } from "@x402/core/client";
2
+ import { type Chain } from "./constants.js";
3
+ import { type Accept, type Parsed402 } from "./x402.js";
4
+ export type Signer = {
5
+ evmPrivateKey?: string;
6
+ solanaSecretKey?: string;
7
+ };
8
+ export type Payer = {
9
+ http: x402HTTPClient;
10
+ chains: Chain[] | null;
11
+ };
12
+ /**
13
+ * Payer-side identifier of a payment, known before it is sent. Solana: our own signature
14
+ * inside the tx. Base: the EIP-3009 nonce and the SHA-256 of the signature (the raw
15
+ * signature is never exposed: with it anyone could submit the transfer).
16
+ */
17
+ export type PayerKey = {
18
+ chain: "solana";
19
+ payer: string;
20
+ payer_signature: string;
21
+ payment_identifier?: string;
22
+ } | {
23
+ chain: "base";
24
+ payer: string;
25
+ authorization_nonce: string;
26
+ authorization_signature_sha256: string;
27
+ payment_identifier?: string;
28
+ };
29
+ /** Build a paying client from caller-supplied keys; the library spend cap is set to maxAtomic. */
30
+ export declare function makePayer(signer: Signer, maxAtomic: bigint, rpc: Record<Chain, string>): Promise<Payer>;
31
+ /** Wrap a caller-built x402Client (its own policies/caps apply in addition to ours). */
32
+ export declare function payerFromClient(client: x402Client): Payer;
33
+ /** Why this accept cannot be paid by safe-buy, or null. Price is checked separately. */
34
+ export declare function refuseReason(a: Accept | undefined): string | null;
35
+ /** EIP-712 domain of official USDC on Base (name() / version() of 0x8335…2913). */
36
+ export declare const BASE_USDC_EIP712: {
37
+ readonly name: "USD Coin";
38
+ readonly version: "2";
39
+ };
40
+ /** Domain fill for official Base USDC when the 402's `extra` lacks name/version. Never for other assets. */
41
+ export declare function eip712Fill(a: Accept): Record<string, string> | null;
42
+ export type Signed = {
43
+ /** Headers to send with the paid request (PAYMENT-SIGNATURE or X-PAYMENT). Never logged. */
44
+ headers: Record<string, string>;
45
+ payer_key: PayerKey;
46
+ /** The 402 named official Base USDC without EIP-712 name/version; the known values were filled in. */
47
+ eip712_filled: boolean;
48
+ };
49
+ /** Sign a payment for exactly this accept (the seller's other options are dropped). */
50
+ export declare function signFor(payer: Payer, parsed: Parsed402, a: Accept): Promise<Signed>;
51
+ export type Settlement = {
52
+ success: boolean;
53
+ transaction: string | null;
54
+ network: string | null;
55
+ error: string | null;
56
+ };
57
+ /** The seller's PAYMENT-RESPONSE / X-PAYMENT-RESPONSE header, decoded; null when absent or unreadable. */
58
+ export declare function settleFrom(http: x402HTTPClient, getHeader: (n: string) => string | null): Settlement | null;
package/dist/pay.js ADDED
@@ -0,0 +1,123 @@
1
+ // Signing an x402 payment with the official x402 client libraries. Keys are supplied by
2
+ // the caller and only handed to the scheme signers; they are never stored or logged here.
3
+ // Ported from seller-probe src/pay.ts.
4
+ import { createHash } from "node:crypto";
5
+ import { x402Client, x402HTTPClient } from "@x402/core/client";
6
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
7
+ import { ExactEvmSchemeV1 } from "@x402/evm/v1";
8
+ import { PAYMENT_IDENTIFIER, appendPaymentIdentifierToExtensions } from "@x402/extensions/payment-identifier";
9
+ import { ExactSvmScheme } from "@x402/svm/exact/client";
10
+ import { ExactSvmSchemeV1 } from "@x402/svm/v1";
11
+ import { privateKeyToAccount } from "viem/accounts";
12
+ import { evmAuthorization } from "./chains/base.js";
13
+ import { payerSignature, solanaSignerFrom } from "./chains/solana.js";
14
+ import { NETWORKS } from "./constants.js";
15
+ import { atomicToUsd } from "./price.js";
16
+ import { isOfficialUsdc } from "./x402.js";
17
+ /** Build a paying client from caller-supplied keys; the library spend cap is set to maxAtomic. */
18
+ export async function makePayer(signer, maxAtomic, rpc) {
19
+ const client = x402Client.fromConfig({
20
+ schemes: [],
21
+ // Library-side cap as a second fence; safe-buy's own price check runs first.
22
+ spendControls: { maxAmountPerPayment: atomicToUsd(maxAtomic) },
23
+ });
24
+ const chains = [];
25
+ if (signer.evmPrivateKey) {
26
+ const key = signer.evmPrivateKey.trim();
27
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key))
28
+ throw new Error("evmPrivateKey must be a 0x-prefixed 32-byte hex key");
29
+ const account = privateKeyToAccount(key);
30
+ client.register(NETWORKS.base.caip2, new ExactEvmScheme(account, { rpcUrl: rpc.base }));
31
+ for (const n of NETWORKS.base.v1)
32
+ client.registerV1(n, new ExactEvmSchemeV1(account));
33
+ chains.push("base");
34
+ }
35
+ if (signer.solanaSecretKey) {
36
+ const s = await solanaSignerFrom(signer.solanaSecretKey);
37
+ client.register(NETWORKS.solana.caip2, new ExactSvmScheme(s, { rpcUrl: rpc.solana }));
38
+ for (const n of NETWORKS.solana.v1)
39
+ client.registerV1(n, new ExactSvmSchemeV1(s, { rpcUrl: rpc.solana }));
40
+ chains.push("solana");
41
+ }
42
+ if (!chains.length)
43
+ throw new Error("no signer key given (signer.evmPrivateKey / signer.solanaSecretKey)");
44
+ return { http: new x402HTTPClient(client), chains };
45
+ }
46
+ /** Wrap a caller-built x402Client (its own policies/caps apply in addition to ours). */
47
+ export function payerFromClient(client) {
48
+ return { http: new x402HTTPClient(client), chains: null };
49
+ }
50
+ /** Why this accept cannot be paid by safe-buy, or null. Price is checked separately. */
51
+ export function refuseReason(a) {
52
+ if (!a)
53
+ return "no exact official-USDC accept on the requested chain";
54
+ if (a.scheme !== "exact")
55
+ return `scheme ${a.scheme} (only exact)`;
56
+ if (!a.chain)
57
+ return `network ${a.network}`;
58
+ if (!isOfficialUsdc(a))
59
+ return `asset ${a.asset} is not official USDC`;
60
+ if (!a.amount || !/^\d+$/.test(a.amount))
61
+ return `bad amount ${a.amount}`;
62
+ if (BigInt(a.amount) <= 0n)
63
+ return `amount ${a.amount} (nothing to buy)`;
64
+ if (!a.payTo)
65
+ return "payTo missing";
66
+ if (a.chain === "base" && a.extra?.assetTransferMethod && a.extra.assetTransferMethod !== "eip3009")
67
+ return `asset transfer method ${String(a.extra.assetTransferMethod)} (only EIP-3009)`;
68
+ return null;
69
+ }
70
+ /** EIP-712 domain of official USDC on Base (name() / version() of 0x8335…2913). */
71
+ export const BASE_USDC_EIP712 = { name: "USD Coin", version: "2" };
72
+ /** Domain fill for official Base USDC when the 402's `extra` lacks name/version. Never for other assets. */
73
+ export function eip712Fill(a) {
74
+ if (a.chain !== "base" || !isOfficialUsdc(a))
75
+ return null;
76
+ const extra = a.raw?.extra ?? {};
77
+ if (extra.name && extra.version)
78
+ return null;
79
+ return { name: extra.name || BASE_USDC_EIP712.name, version: extra.version || BASE_USDC_EIP712.version };
80
+ }
81
+ /** Sign a payment for exactly this accept (the seller's other options are dropped). */
82
+ export async function signFor(payer, parsed, a) {
83
+ const doc = parsed.raw;
84
+ const fill = eip712Fill(a);
85
+ const raw = fill ? { ...a.raw, extra: { ...(a.raw?.extra ?? {}), ...fill } } : a.raw;
86
+ const payload = await payer.http.createPaymentPayload({ ...doc, accepts: [raw] });
87
+ // x402 `payment-identifier` extension: when the seller declares it, send a fresh id.
88
+ let payment_identifier;
89
+ const pid = doc?.extensions?.[PAYMENT_IDENTIFIER];
90
+ if (pid) {
91
+ const ext = { ...(payload.extensions ?? {}), [PAYMENT_IDENTIFIER]: payload.extensions?.[PAYMENT_IDENTIFIER] ?? pid };
92
+ appendPaymentIdentifierToExtensions(ext);
93
+ payload.extensions = ext;
94
+ payment_identifier = ext[PAYMENT_IDENTIFIER]?.info?.id ?? undefined;
95
+ }
96
+ let payer_key;
97
+ if (a.chain === "solana") {
98
+ const ps = payerSignature(payload);
99
+ payer_key = { chain: "solana", payer: ps.payer, payer_signature: ps.signature };
100
+ }
101
+ else {
102
+ const auth = evmAuthorization(payload);
103
+ payer_key = {
104
+ chain: "base",
105
+ payer: auth.from,
106
+ authorization_nonce: auth.nonce,
107
+ authorization_signature_sha256: createHash("sha256").update(auth.signature).digest("hex"),
108
+ };
109
+ }
110
+ if (payment_identifier)
111
+ payer_key.payment_identifier = payment_identifier;
112
+ return { headers: payer.http.encodePaymentSignatureHeader(payload), payer_key, eip712_filled: !!fill };
113
+ }
114
+ /** The seller's PAYMENT-RESPONSE / X-PAYMENT-RESPONSE header, decoded; null when absent or unreadable. */
115
+ export function settleFrom(http, getHeader) {
116
+ try {
117
+ const s = http.getPaymentSettleResponse(getHeader);
118
+ return { success: !!s.success, transaction: s.transaction || null, network: s.network ?? null, error: s.errorReason ?? null };
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
@@ -0,0 +1,8 @@
1
+ export type Price = number | string;
2
+ /**
3
+ * Parse a USD price (`0.01`, `"0.01"`, `"$0.01"`) into USDC atomic units. Digits past the
4
+ * 6th decimal are truncated (a cap never rounds up). Throws on anything else.
5
+ */
6
+ export declare function usdToAtomic(price: Price): bigint;
7
+ /** Atomic USDC units -> "$0.01" (trailing zeros trimmed, at least 2 decimals). */
8
+ export declare function atomicToUsd(atomic: bigint | string): string;
package/dist/price.js ADDED
@@ -0,0 +1,35 @@
1
+ // USD <-> USDC atomic units (6 decimals). Decimal string math only, no floats.
2
+ /**
3
+ * Parse a USD price (`0.01`, `"0.01"`, `"$0.01"`) into USDC atomic units. Digits past the
4
+ * 6th decimal are truncated (a cap never rounds up). Throws on anything else.
5
+ */
6
+ export function usdToAtomic(price) {
7
+ let s;
8
+ if (typeof price === "number") {
9
+ if (!Number.isFinite(price) || price < 0)
10
+ throw new Error(`invalid price ${price}`);
11
+ s = price.toFixed(10);
12
+ }
13
+ else if (typeof price === "string") {
14
+ s = price.trim().replace(/^\$/, "").trim();
15
+ }
16
+ else {
17
+ throw new Error("price must be a number or a string like \"$0.01\"");
18
+ }
19
+ const m = /^(\d+)(?:\.(\d*))?$/.exec(s);
20
+ if (!m)
21
+ throw new Error(`invalid price ${JSON.stringify(price)}`);
22
+ const frac = (m[2] ?? "").padEnd(6, "0").slice(0, 6);
23
+ return BigInt(m[1]) * 1000000n + BigInt(frac);
24
+ }
25
+ /** Atomic USDC units -> "$0.01" (trailing zeros trimmed, at least 2 decimals). */
26
+ export function atomicToUsd(atomic) {
27
+ const v = BigInt(atomic);
28
+ const neg = v < 0n;
29
+ const a = neg ? -v : v;
30
+ const whole = a / 1000000n;
31
+ let frac = (a % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
32
+ if (frac.length < 2)
33
+ frac = frac.padEnd(2, "0");
34
+ return `${neg ? "-" : ""}$${whole}.${frac}`;
35
+ }
@@ -0,0 +1,36 @@
1
+ /** Fields of the Probe's host document that safe-buy uses (others are ignored). */
2
+ export type ProbeDoc = {
3
+ host?: string;
4
+ score?: number | null;
5
+ coverage?: string | null;
6
+ buyable_without_input?: boolean | null;
7
+ requires_input_reason?: {
8
+ source?: string;
9
+ reason?: string;
10
+ } | null;
11
+ last_paid?: {
12
+ date?: string;
13
+ ts?: string;
14
+ outcome?: string | null;
15
+ price_usd?: number | null;
16
+ network?: string | null;
17
+ } | null;
18
+ catalog?: {
19
+ url?: string;
20
+ price_usd?: number | null;
21
+ } | null;
22
+ };
23
+ export type ProbeLookup = {
24
+ status: "found";
25
+ doc: ProbeDoc;
26
+ } | {
27
+ status: "no_data";
28
+ } | {
29
+ status: "unavailable";
30
+ error: string;
31
+ };
32
+ /** Lowercase hostname without a leading "www.". */
33
+ export declare const hostOf: (url: string) => string;
34
+ export declare function lookupProbe(probeUrl: string, host: string, timeoutMs?: number): Promise<ProbeLookup>;
35
+ /** Last-purchase outcomes that make safe-buy refuse to pay. */
36
+ export declare const BAD_LAST_PAID: string[];
package/dist/probe.js ADDED
@@ -0,0 +1,46 @@
1
+ // Stage 1: the seller's public record at the x402 Seller Probe (GET {probeUrl}/scores/{host}).
2
+ import { PROBE_TIMEOUT_MS, USER_AGENT } from "./constants.js";
3
+ /** Lowercase hostname without a leading "www.". */
4
+ export const hostOf = (url) => new URL(url).hostname.toLowerCase().replace(/^www\./, "");
5
+ export async function lookupProbe(probeUrl, host, timeoutMs = PROBE_TIMEOUT_MS) {
6
+ let res;
7
+ try {
8
+ res = await fetch(`${probeUrl.replace(/\/+$/, "")}/scores/${encodeURIComponent(host)}`, {
9
+ headers: { accept: "application/json", "user-agent": USER_AGENT },
10
+ signal: AbortSignal.timeout(timeoutMs),
11
+ });
12
+ }
13
+ catch (e) {
14
+ return { status: "unavailable", error: e?.name === "TimeoutError" ? "timeout" : String(e?.message ?? e) };
15
+ }
16
+ if (res.status === 404) {
17
+ await res.body?.cancel().catch(() => { });
18
+ return { status: "no_data" };
19
+ }
20
+ if (!res.ok) {
21
+ await res.body?.cancel().catch(() => { });
22
+ return { status: "unavailable", error: `HTTP ${res.status}` };
23
+ }
24
+ try {
25
+ const doc = await res.json();
26
+ if (!doc || typeof doc !== "object")
27
+ return { status: "unavailable", error: "not a JSON object" };
28
+ return { status: "found", doc: pickDoc(doc) };
29
+ }
30
+ catch {
31
+ return { status: "unavailable", error: "invalid JSON" };
32
+ }
33
+ }
34
+ function pickDoc(d) {
35
+ return {
36
+ host: d.host,
37
+ score: d.score ?? null,
38
+ coverage: d.coverage ?? null,
39
+ buyable_without_input: typeof d.buyable_without_input === "boolean" ? d.buyable_without_input : null,
40
+ requires_input_reason: d.requires_input_reason ?? null,
41
+ last_paid: d.last_paid ?? null,
42
+ catalog: d.catalog && typeof d.catalog === "object" ? { url: d.catalog.url, price_usd: d.catalog.price_usd ?? null } : null,
43
+ };
44
+ }
45
+ /** Last-purchase outcomes that make safe-buy refuse to pay. */
46
+ export const BAD_LAST_PAID = ["not_delivered", "overcharged"];
@@ -0,0 +1,30 @@
1
+ import { RECEIPT_HEADER, REFETCH_HEADER, WELL_KNOWN_PATH, type WellKnown } from "@x402jp/delivery-receipt-verify";
2
+ export { RECEIPT_HEADER, REFETCH_HEADER, WELL_KNOWN_PATH, type WellKnown };
3
+ export type WellKnownLookup = {
4
+ advertised: boolean;
5
+ wellKnown: WellKnown | null;
6
+ error: string | null;
7
+ };
8
+ /** Fetch `<origin>/.well-known/x402-receipt.json`. advertised = valid document with at least one key. */
9
+ export declare function fetchReceiptKeys(url: string, timeoutMs?: number): Promise<WellKnownLookup>;
10
+ export type ReceiptCheck = {
11
+ header: string;
12
+ ok: boolean;
13
+ reason: string | null;
14
+ };
15
+ /** Verify a receipt header against body bytes with a known key document. */
16
+ export declare function checkReceipt(header: string, body: Uint8Array, wellKnown: WellKnown | null, paymentId: string | null): ReceiptCheck;
17
+ export type VerifyReceiptArgs = {
18
+ /** The paid resource URL (its origin serves the key document). */
19
+ url: string;
20
+ /** Raw X-Delivery-Receipt header value. */
21
+ header: string;
22
+ /** Exact response body bytes. */
23
+ body: Uint8Array;
24
+ /** Expected payment_id: Solana tx signature, Base EIP-3009 nonce. */
25
+ paymentId?: string | null;
26
+ };
27
+ /** Fetch the seller's key document and verify a receipt. */
28
+ export declare function verifyReceipt(args: VerifyReceiptArgs): Promise<ReceiptCheck & {
29
+ key_document: string | null;
30
+ }>;
@@ -0,0 +1,44 @@
1
+ // Delivery Receipts (kato9292929/delivery-receipt): the seller's key document and
2
+ // verification of a receipt against the exact body bytes.
3
+ import { USER_AGENT } from "./constants.js";
4
+ import { RECEIPT_HEADER, REFETCH_HEADER, WELL_KNOWN_PATH, parseWellKnown, verifyReceipt as verifyReceiptCore, } from "@x402jp/delivery-receipt-verify";
5
+ export { RECEIPT_HEADER, REFETCH_HEADER, WELL_KNOWN_PATH };
6
+ /** Fetch `<origin>/.well-known/x402-receipt.json`. advertised = valid document with at least one key. */
7
+ export async function fetchReceiptKeys(url, timeoutMs = 5000) {
8
+ let res;
9
+ try {
10
+ res = await fetch(new URL(WELL_KNOWN_PATH, new URL(url).origin), {
11
+ headers: { accept: "application/json", "user-agent": USER_AGENT },
12
+ signal: AbortSignal.timeout(timeoutMs),
13
+ });
14
+ }
15
+ catch (e) {
16
+ return { advertised: false, wellKnown: null, error: e?.name === "TimeoutError" ? "timeout" : String(e?.message ?? e) };
17
+ }
18
+ if (!res.ok) {
19
+ await res.body?.cancel().catch(() => { });
20
+ return { advertised: false, wellKnown: null, error: `HTTP ${res.status}` };
21
+ }
22
+ try {
23
+ const wk = parseWellKnown(await res.json());
24
+ return { advertised: wk.keys.length > 0, wellKnown: wk, error: null };
25
+ }
26
+ catch (e) {
27
+ return { advertised: false, wellKnown: null, error: `invalid ${WELL_KNOWN_PATH}: ${e?.message ?? e}` };
28
+ }
29
+ }
30
+ /** Verify a receipt header against body bytes with a known key document. */
31
+ export function checkReceipt(header, body, wellKnown, paymentId) {
32
+ if (!wellKnown)
33
+ return { header, ok: false, reason: `receipt_key_unknown (no usable ${WELL_KNOWN_PATH})` };
34
+ const v = verifyReceiptCore({ header, body, wellKnown, expect: paymentId ? { payment_id: paymentId } : undefined });
35
+ return v.ok ? { header, ok: true, reason: null } : { header, ok: false, reason: v.detail ? `${v.reason}: ${v.detail}` : v.reason };
36
+ }
37
+ /** Fetch the seller's key document and verify a receipt. */
38
+ export async function verifyReceipt(args) {
39
+ const wk = await fetchReceiptKeys(args.url);
40
+ const r = wk.wellKnown
41
+ ? checkReceipt(args.header, args.body, wk.wellKnown, args.paymentId ?? null)
42
+ : { header: args.header, ok: false, reason: `receipt_key_unknown (${WELL_KNOWN_PATH}: ${wk.error})` };
43
+ return { ...r, key_document: wk.wellKnown ? new URL(WELL_KNOWN_PATH, new URL(args.url).origin).toString() : null };
44
+ }
@@ -0,0 +1,26 @@
1
+ export type Report = {
2
+ v: 1;
3
+ host: string;
4
+ /** origin + path only (no query, no fragment). */
5
+ url: string;
6
+ network: string | null;
7
+ amount: string | null;
8
+ outcome: string;
9
+ http_status: number | null;
10
+ latency_ms: number | null;
11
+ settled_header: boolean;
12
+ onchain_found: boolean;
13
+ receipt_ok: boolean | null;
14
+ refetched: boolean;
15
+ sdk: string;
16
+ ts: string;
17
+ };
18
+ export declare function buildReport(r: Omit<Report, "v" | "sdk" | "url" | "ts"> & {
19
+ url: string;
20
+ ts?: Date;
21
+ }): Report;
22
+ /** POST the report. Never throws: a failed report never fails the purchase. */
23
+ export declare function sendReport(probeUrl: string, report: Report, timeoutMs?: number): Promise<{
24
+ ok: boolean;
25
+ detail: string;
26
+ }>;
package/dist/report.js ADDED
@@ -0,0 +1,38 @@
1
+ // Stage 8 (opt-in): an anonymized purchase report to the Probe. It never contains the
2
+ // payer address, the tx, payer-side identifiers, the body or any query string.
3
+ import { SDK_ID, USER_AGENT } from "./constants.js";
4
+ export function buildReport(r) {
5
+ const u = new URL(r.url);
6
+ return {
7
+ v: 1,
8
+ host: r.host,
9
+ url: u.origin + u.pathname,
10
+ network: r.network,
11
+ amount: r.amount,
12
+ outcome: r.outcome,
13
+ http_status: r.http_status,
14
+ latency_ms: r.latency_ms,
15
+ settled_header: r.settled_header,
16
+ onchain_found: r.onchain_found,
17
+ receipt_ok: r.receipt_ok,
18
+ refetched: r.refetched,
19
+ sdk: SDK_ID,
20
+ ts: (r.ts ?? new Date()).toISOString(),
21
+ };
22
+ }
23
+ /** POST the report. Never throws: a failed report never fails the purchase. */
24
+ export async function sendReport(probeUrl, report, timeoutMs = 5000) {
25
+ try {
26
+ const res = await fetch(`${probeUrl.replace(/\/+$/, "")}/reports`, {
27
+ method: "POST",
28
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
29
+ body: JSON.stringify(report),
30
+ signal: AbortSignal.timeout(timeoutMs),
31
+ });
32
+ await res.body?.cancel().catch(() => { });
33
+ return { ok: res.ok, detail: `HTTP ${res.status}` };
34
+ }
35
+ catch (e) {
36
+ return { ok: false, detail: e?.name === "TimeoutError" ? "timeout" : String(e?.message ?? e) };
37
+ }
38
+ }
@@ -0,0 +1,122 @@
1
+ import type { x402Client } from "@x402/core/client";
2
+ import { type Chain, type ChainChoice } from "./constants.js";
3
+ import { type PayerKey, type Signer } from "./pay.js";
4
+ import { type Price } from "./price.js";
5
+ import { type ProbeDoc } from "./probe.js";
6
+ import { type ReceiptCheck } from "./receipt.js";
7
+ import { type InputRequirement } from "./x402.js";
8
+ export type StageName = "probe" | "402" | "price" | "pay" | "reconcile" | "refetch" | "receipt" | "report";
9
+ export type Stage = {
10
+ name: StageName;
11
+ ok: boolean;
12
+ detail: string;
13
+ };
14
+ /** Request input for endpoints that need more than a bare GET. */
15
+ export type BuyInput = {
16
+ /** Defaults to GET, or POST when `body` is given. */
17
+ method?: string;
18
+ query?: Record<string, string | number | boolean>;
19
+ /** Object/array → JSON; string/Uint8Array → sent as is. */
20
+ body?: unknown;
21
+ headers?: Record<string, string>;
22
+ };
23
+ /** The accept that would be (or was) paid. */
24
+ export type Offer = {
25
+ spec_version: "v1" | "v2" | null;
26
+ scheme: string;
27
+ network: string;
28
+ chain: Chain;
29
+ /** Atomic USDC (6 decimals). */
30
+ amount: string;
31
+ amount_usd: string;
32
+ asset: string;
33
+ pay_to: string;
34
+ resource: string | null;
35
+ max_timeout_seconds: number | null;
36
+ };
37
+ export type ProbeInfo = {
38
+ status: "found" | "no_data" | "unavailable";
39
+ doc: ProbeDoc | null;
40
+ };
41
+ export type CheckOptions = {
42
+ /** Default https://probe.x402jp.com. */
43
+ probeUrl?: string;
44
+ /** Default "any" (Base preferred, then Solana). */
45
+ chain?: ChainChoice;
46
+ /** USD cap, e.g. 0.01 or "$0.01". Optional for checkSeller, required for safeBuy. */
47
+ maxPrice?: Price;
48
+ input?: BuyInput;
49
+ /** Pay even when the 402 price differs from the Probe catalog price. */
50
+ allowCatalogMismatch?: boolean;
51
+ /** Timeout of the unpaid and paid requests to the seller (default 60 s). */
52
+ requestTimeoutMs?: number;
53
+ };
54
+ /** What onAttempt receives: everything needed to find the payment later. No key material. */
55
+ export type AttemptRecord = {
56
+ ts: string;
57
+ url: string;
58
+ host: string;
59
+ network: string;
60
+ chain: Chain;
61
+ amount: string;
62
+ pay_to: string;
63
+ payer_key: PayerKey;
64
+ };
65
+ export type SafeBuyOptions = CheckOptions & {
66
+ maxPrice: Price;
67
+ /** Keys used only to sign this payment; never stored or logged. */
68
+ signer?: Signer;
69
+ /** Alternatively a prebuilt x402Client with schemes registered. */
70
+ client?: x402Client;
71
+ /** Called after signing and BEFORE the payment is sent (e.g. to persist a ledger row). If it throws, nothing is sent. */
72
+ onAttempt?: (record: AttemptRecord) => void | Promise<void>;
73
+ /** RPC URLs for the chain search (default: env BASE_RPC_URL / SOLANA_RPC_URL, then public endpoints). */
74
+ rpc?: {
75
+ base?: string;
76
+ solana?: string;
77
+ };
78
+ /** How long to look on chain for our payment (default 90 s). */
79
+ findTimeoutMs?: number;
80
+ /** Poll interval of the chain search (default 5 s). */
81
+ pollIntervalMs?: number;
82
+ /** Send an anonymized report to the Probe (default false). */
83
+ report?: boolean;
84
+ };
85
+ export type Outcome = "delivered" | "delivered_unsettled" | "not_delivered" | "overcharged" | "amount_mismatch" | "rejected" | "failed" | "refused";
86
+ export type CheckResult = {
87
+ ok: boolean;
88
+ stopped_at: StageName | null;
89
+ reason: string | null;
90
+ stages: Stage[];
91
+ host: string;
92
+ probe: ProbeInfo;
93
+ offer: Offer | null;
94
+ input_requirement: InputRequirement | null;
95
+ };
96
+ export type SafeBuyResult = {
97
+ ok: boolean;
98
+ outcome: Outcome;
99
+ stopped_at: StageName | null;
100
+ reason: string | null;
101
+ status: number | null;
102
+ body: Uint8Array | null;
103
+ body_sha256: string | null;
104
+ content_type: string | null;
105
+ tx: string | null;
106
+ tx_source: "settle_header" | "onchain_search" | null;
107
+ network: string | null;
108
+ amount: string | null;
109
+ /** USDC that actually left the payer in the tx (atomic), when it could be read on chain. */
110
+ transferred: string | null;
111
+ payer_key: PayerKey | null;
112
+ receipt: ReceiptCheck | null;
113
+ refetched: boolean;
114
+ latency_ms: number | null;
115
+ offer: Offer | null;
116
+ probe: ProbeInfo;
117
+ stages: Stage[];
118
+ };
119
+ /** Stages 1–3 only: Probe record, the 402 offer and the price checks. Nothing is signed or paid. */
120
+ export declare function checkSeller(url: string, opts?: CheckOptions): Promise<CheckResult>;
121
+ /** Buy one call from an x402 seller. See README for the stages. */
122
+ export declare function safeBuy(url: string, opts: SafeBuyOptions): Promise<SafeBuyResult>;