@primitivedotdev/sdk 1.3.0 → 1.4.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.
@@ -0,0 +1,263 @@
1
+ import { Address, Hex } from "viem";
2
+
3
+ //#region src/x402/sign.d.ts
4
+ interface NonceBinding {
5
+ /** The interaction id, including its `@domain`. Lowercased before hashing. */
6
+ interactionId: string;
7
+ /** The challenge step id (a UUID). Lowercased before hashing. */
8
+ challengeStepId: string;
9
+ /** The challenger's per-challenge random nonce: 64 lowercase hex chars. */
10
+ challengeNonce: string;
11
+ }
12
+ /**
13
+ * Derive the EIP-3009 nonce bound to a specific interaction step:
14
+ *
15
+ * keccak256( utf8(lower(interaction_id)) || 0x00
16
+ * || utf8(lower(challenge_step_id)) || 0x00
17
+ * || hexdecode(challenge_nonce) )
18
+ *
19
+ * The `0x00` separators pin the field boundaries (undelimited concatenation of
20
+ * variable-length strings is collision-ambiguous), and the challenge nonce is
21
+ * decoded to its 32 raw bytes before hashing. The platform recomputes this and
22
+ * rejects a mismatch.
23
+ */
24
+ declare function deriveEip3009Nonce(input: NonceBinding): Hex;
25
+ /**
26
+ * The EIP-3009 `TransferWithAuthorization` EIP-712 type. The field order and
27
+ * types are part of the on-chain contract and MUST NOT change.
28
+ */
29
+ declare const TRANSFER_WITH_AUTHORIZATION_TYPES: {
30
+ readonly TransferWithAuthorization: readonly [{
31
+ readonly name: "from";
32
+ readonly type: "address";
33
+ }, {
34
+ readonly name: "to";
35
+ readonly type: "address";
36
+ }, {
37
+ readonly name: "value";
38
+ readonly type: "uint256";
39
+ }, {
40
+ readonly name: "validAfter";
41
+ readonly type: "uint256";
42
+ }, {
43
+ readonly name: "validBefore";
44
+ readonly type: "uint256";
45
+ }, {
46
+ readonly name: "nonce";
47
+ readonly type: "bytes32";
48
+ }];
49
+ };
50
+ /**
51
+ * The token's EIP-712 domain. `name`/`version` MUST be the actual token's domain
52
+ * params (Base mainnet USDC reports `name: "USD Coin"`, Base Sepolia `"USDC"`;
53
+ * both `version: "2"`); they come from the challenge's payment requirements
54
+ * `extra`. A wrong name/version produces a signature the verifier rejects.
55
+ */
56
+ interface TokenDomain {
57
+ name: string;
58
+ version: string;
59
+ chainId: number;
60
+ verifyingContract: Address;
61
+ }
62
+ interface TransferAuthorization {
63
+ from: Address;
64
+ to: Address;
65
+ /** Token base units (USDC has 6 decimals), as a bigint. */
66
+ value: bigint;
67
+ validAfter: bigint;
68
+ validBefore: bigint;
69
+ nonce: Hex;
70
+ }
71
+ interface TransferWithAuthorizationTypedData {
72
+ domain: {
73
+ name: string;
74
+ version: string;
75
+ chainId: number;
76
+ verifyingContract: Address;
77
+ };
78
+ types: typeof TRANSFER_WITH_AUTHORIZATION_TYPES;
79
+ primaryType: "TransferWithAuthorization";
80
+ message: TransferAuthorization;
81
+ }
82
+ declare function transferWithAuthorizationTypedData(domain: TokenDomain, auth: TransferAuthorization): TransferWithAuthorizationTypedData;
83
+ /**
84
+ * A customer-held signer. A viem `LocalAccount` satisfies this directly; any
85
+ * key source (hardware wallet, injected provider) can be adapted. The key never
86
+ * leaves the caller.
87
+ */
88
+ interface X402Signer {
89
+ address: Address;
90
+ signTypedData(typedData: TransferWithAuthorizationTypedData): Promise<Hex>;
91
+ /**
92
+ * `personal_sign` over a UTF-8 string. Only needed for
93
+ * `registerPayoutAddress()` (the ownership proof); a viem `LocalAccount`
94
+ * provides it directly.
95
+ */
96
+ signMessage?(args: {
97
+ message: string;
98
+ }): Promise<Hex>;
99
+ }
100
+ interface PayoutRegistrationMessageInput {
101
+ /** The org id the address is being authorized for. Bound into the signature. */
102
+ org: string;
103
+ /** The payout address (the signer's own address). Lowercased in the message. */
104
+ address: string;
105
+ network: string;
106
+ /** ISO-8601 timestamp; the server enforces a freshness window against replay. */
107
+ issuedAt: string;
108
+ }
109
+ /**
110
+ * Build the payout-address ownership message. This MUST be byte-identical to the
111
+ * platform's `buildPayoutRegistrationMessage`, or registration fails the
112
+ * ownership proof. The org id is in the signed bytes, so a captured signature
113
+ * can never register the address under a different org.
114
+ */
115
+ declare function buildPayoutRegistrationMessage(input: PayoutRegistrationMessageInput): string;
116
+ /** The x402 wire payload (validated server-side against the x402 schema). */
117
+ interface X402PaymentPayload {
118
+ x402Version: 1;
119
+ scheme: "exact";
120
+ network: string;
121
+ payload: {
122
+ signature: Hex;
123
+ authorization: {
124
+ from: Address;
125
+ to: Address;
126
+ value: string;
127
+ validAfter: string;
128
+ validBefore: string;
129
+ nonce: Hex;
130
+ };
131
+ };
132
+ }
133
+ /** Assemble the wire payload from a signed authorization. */
134
+ declare function toPaymentPayload(network: string, auth: TransferAuthorization, signature: Hex): X402PaymentPayload;
135
+ //#endregion
136
+ //#region src/x402/client.d.ts
137
+ interface X402PaymentRequirements {
138
+ scheme: string;
139
+ network: string;
140
+ maxAmountRequired: string;
141
+ payTo: string;
142
+ asset: string;
143
+ extra: {
144
+ name: string;
145
+ version: string;
146
+ };
147
+ }
148
+ /** A request for payment, as returned by `charge()` / the platform. */
149
+ interface X402Challenge {
150
+ id: string;
151
+ network: string;
152
+ amount: string;
153
+ pay_to: string;
154
+ nonce_binding: {
155
+ interaction_id: string;
156
+ challenge_step_id: string;
157
+ challenge_nonce: string;
158
+ };
159
+ payment_requirements: X402PaymentRequirements;
160
+ expires_at: string;
161
+ }
162
+ interface X402Receipt {
163
+ id: string;
164
+ status: string;
165
+ settle_tx: string | null;
166
+ }
167
+ /** A registered payout address (read shape; mirrors the platform response). */
168
+ interface X402PayoutAddress {
169
+ id: string;
170
+ address: string;
171
+ network: string;
172
+ label: string | null;
173
+ is_default: boolean;
174
+ verified_at: string | null;
175
+ }
176
+ /** The org's spend policy (read shape; also accepted by `setSpendPolicy`). */
177
+ interface X402SpendPolicy {
178
+ /** Kill-switch: when true, all outbound payments are refused. */
179
+ paused: boolean;
180
+ /** Per-payment cap in token base units, or null for no cap. */
181
+ max_per_payment: string | null;
182
+ /** Daily cap in token base units, or null for no cap. */
183
+ max_per_day: string | null;
184
+ /** Allowed payee org ids; null = any on-net payee, [] = deny all. */
185
+ allowlist: string[] | null;
186
+ }
187
+ interface X402ChargeInput {
188
+ /** Amount in token base units (USDC has 6 decimals, so "10000" = 0.01). */
189
+ amount: string;
190
+ /** Defaults to "base-sepolia". */
191
+ network?: string;
192
+ /** The org id allowed to pay this challenge (on-net binding). */
193
+ payerOrg?: string;
194
+ description?: string;
195
+ /** A URL identifying the thing being paid for. */
196
+ resource?: string;
197
+ /** Seconds until the challenge expires (default 1h). */
198
+ expiresIn?: number;
199
+ }
200
+ declare class X402Error extends Error {
201
+ /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
202
+ readonly status: number;
203
+ readonly body: unknown;
204
+ /** The `Retry-After` response header, if the server sent one. */
205
+ readonly retryAfter: string | null;
206
+ constructor(message: string, status: number, body?: unknown, options?: {
207
+ cause?: unknown;
208
+ retryAfter?: string | null;
209
+ });
210
+ }
211
+ interface X402ClientOptions {
212
+ /** API key. Defaults to `process.env.PRIMITIVE_API_KEY`. */
213
+ apiKey?: string;
214
+ /** API base URL. Defaults to the production host. */
215
+ baseUrl?: string;
216
+ /** Override the fetch implementation (e.g. for testing). */
217
+ fetch?: typeof fetch;
218
+ /** Per-request timeout in milliseconds. Defaults to 30000. */
219
+ timeoutMs?: number;
220
+ }
221
+ declare class X402Client {
222
+ #private;
223
+ constructor(options?: X402ClientOptions);
224
+ /** Request a payment (payee side). Returns the challenge to hand to the payer. */
225
+ charge(input: X402ChargeInput): Promise<X402Challenge>;
226
+ /**
227
+ * Pay a challenge (payer side). Derives the interaction-bound authorization,
228
+ * signs it locally with the caller's key, and submits it for settlement.
229
+ */
230
+ pay(challenge: X402Challenge, options: {
231
+ signer: X402Signer;
232
+ }): Promise<X402Receipt>;
233
+ /** Fetch a challenge by id (scoped to the challenger org that created it). */
234
+ getChallenge(id: string): Promise<X402Challenge>;
235
+ /**
236
+ * Register a payout address for your org (payee side). The signer proves
237
+ * control of its own address with an org-bound `personal_sign`; the proven
238
+ * address becomes (or updates to) the default payout destination for the
239
+ * network. `charge()` resolves its `pay_to` from this directory, so a payee
240
+ * must register before requesting payments.
241
+ */
242
+ registerPayoutAddress(input: {
243
+ org: string;
244
+ network?: string;
245
+ issuedAt?: string;
246
+ }, options: {
247
+ signer: X402Signer;
248
+ }): Promise<X402PayoutAddress>;
249
+ /** List your org's registered payout addresses. */
250
+ listPayoutAddresses(): Promise<X402PayoutAddress[]>;
251
+ /** Read your org's spend policy (kill-switch + caps + allowlist). */
252
+ getSpendPolicy(): Promise<X402SpendPolicy>;
253
+ /**
254
+ * Update your org's spend policy. The endpoint is a PUT, but the server
255
+ * applies it as a merge: only the fields you include are changed and omitted
256
+ * fields keep their current value, so a partial update can't silently reset
257
+ * the kill-switch. Pass `null` to clear a cap.
258
+ */
259
+ setSpendPolicy(update: Partial<X402SpendPolicy>): Promise<X402SpendPolicy>;
260
+ }
261
+ declare function createX402Client(options?: X402ClientOptions): X402Client;
262
+ //#endregion
263
+ export { NonceBinding, PayoutRegistrationMessageInput, TRANSFER_WITH_AUTHORIZATION_TYPES, TokenDomain, TransferAuthorization, TransferWithAuthorizationTypedData, X402Challenge, X402ChargeInput, X402Client, X402ClientOptions, X402Error, X402PaymentPayload, X402PaymentRequirements, X402PayoutAddress, X402Receipt, X402Signer, X402SpendPolicy, buildPayoutRegistrationMessage, createX402Client, deriveEip3009Nonce, toPaymentPayload, transferWithAuthorizationTypedData };
@@ -0,0 +1,320 @@
1
+ import { concat, hexToBytes, keccak256, stringToBytes } from "viem";
2
+ //#region src/x402/sign.ts
3
+ /**
4
+ * x402 client-side signing.
5
+ *
6
+ * The payer signs an EIP-3009 `transferWithAuthorization` over the customer's
7
+ * own key; the key never leaves them. This module derives the interaction-bound
8
+ * nonce and assembles the EIP-712 typed data and the wire payload. The byte
9
+ * layout here MUST match the platform verifier exactly; a normative test vector
10
+ * (see sign.test.ts) locks the nonce derivation to the same value the server
11
+ * recomputes.
12
+ */
13
+ /** A challenge nonce is 32 bytes rendered as 64 lowercase hex chars, no 0x. */
14
+ const CHALLENGE_NONCE_RE = /^[0-9a-f]{64}$/;
15
+ /** Single-byte domain separator between the variable-length string fields. */
16
+ const FIELD_SEPARATOR = new Uint8Array([0]);
17
+ /**
18
+ * Derive the EIP-3009 nonce bound to a specific interaction step:
19
+ *
20
+ * keccak256( utf8(lower(interaction_id)) || 0x00
21
+ * || utf8(lower(challenge_step_id)) || 0x00
22
+ * || hexdecode(challenge_nonce) )
23
+ *
24
+ * The `0x00` separators pin the field boundaries (undelimited concatenation of
25
+ * variable-length strings is collision-ambiguous), and the challenge nonce is
26
+ * decoded to its 32 raw bytes before hashing. The platform recomputes this and
27
+ * rejects a mismatch.
28
+ */
29
+ function deriveEip3009Nonce(input) {
30
+ if (!CHALLENGE_NONCE_RE.test(input.challengeNonce)) throw new Error("challengeNonce must be exactly 64 lowercase hex chars (32 bytes), no 0x prefix");
31
+ return keccak256(concat([
32
+ stringToBytes(input.interactionId.toLowerCase()),
33
+ FIELD_SEPARATOR,
34
+ stringToBytes(input.challengeStepId.toLowerCase()),
35
+ FIELD_SEPARATOR,
36
+ hexToBytes(`0x${input.challengeNonce}`)
37
+ ]));
38
+ }
39
+ /**
40
+ * The EIP-3009 `TransferWithAuthorization` EIP-712 type. The field order and
41
+ * types are part of the on-chain contract and MUST NOT change.
42
+ */
43
+ const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [
44
+ {
45
+ name: "from",
46
+ type: "address"
47
+ },
48
+ {
49
+ name: "to",
50
+ type: "address"
51
+ },
52
+ {
53
+ name: "value",
54
+ type: "uint256"
55
+ },
56
+ {
57
+ name: "validAfter",
58
+ type: "uint256"
59
+ },
60
+ {
61
+ name: "validBefore",
62
+ type: "uint256"
63
+ },
64
+ {
65
+ name: "nonce",
66
+ type: "bytes32"
67
+ }
68
+ ] };
69
+ function transferWithAuthorizationTypedData(domain, auth) {
70
+ return {
71
+ domain: {
72
+ name: domain.name,
73
+ version: domain.version,
74
+ chainId: domain.chainId,
75
+ verifyingContract: domain.verifyingContract
76
+ },
77
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
78
+ primaryType: "TransferWithAuthorization",
79
+ message: auth
80
+ };
81
+ }
82
+ /**
83
+ * Build the payout-address ownership message. This MUST be byte-identical to the
84
+ * platform's `buildPayoutRegistrationMessage`, or registration fails the
85
+ * ownership proof. The org id is in the signed bytes, so a captured signature
86
+ * can never register the address under a different org.
87
+ */
88
+ function buildPayoutRegistrationMessage(input) {
89
+ return [
90
+ "Primitive x402 payout address authorization",
91
+ "",
92
+ "I authorize this address as a payout destination for my Primitive organization.",
93
+ "",
94
+ `org: ${input.org}`,
95
+ `address: ${input.address.toLowerCase()}`,
96
+ `network: ${input.network}`,
97
+ `issued: ${input.issuedAt}`
98
+ ].join("\n");
99
+ }
100
+ /** Assemble the wire payload from a signed authorization. */
101
+ function toPaymentPayload(network, auth, signature) {
102
+ return {
103
+ x402Version: 1,
104
+ scheme: "exact",
105
+ network,
106
+ payload: {
107
+ signature,
108
+ authorization: {
109
+ from: auth.from,
110
+ to: auth.to,
111
+ value: auth.value.toString(),
112
+ validAfter: auth.validAfter.toString(),
113
+ validBefore: auth.validBefore.toString(),
114
+ nonce: auth.nonce
115
+ }
116
+ }
117
+ };
118
+ }
119
+ //#endregion
120
+ //#region src/x402/client.ts
121
+ const CHAIN_IDS = {
122
+ "base-sepolia": 84532,
123
+ base: 8453
124
+ };
125
+ const CLOCK_SKEW_SEC = 300;
126
+ const SETTLEMENT_MARGIN_SEC = 300;
127
+ const DEFAULT_BASE_URL = "https://api.primitive.dev";
128
+ const CHARGE_INPUT_KEYS = {
129
+ amount: true,
130
+ network: true,
131
+ payerOrg: true,
132
+ description: true,
133
+ resource: true,
134
+ expiresIn: true
135
+ };
136
+ var X402Error = class extends Error {
137
+ /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
138
+ status;
139
+ body;
140
+ /** The `Retry-After` response header, if the server sent one. */
141
+ retryAfter;
142
+ constructor(message, status, body, options) {
143
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
144
+ this.name = "X402Error";
145
+ this.status = status;
146
+ this.body = body;
147
+ this.retryAfter = options?.retryAfter ?? null;
148
+ }
149
+ };
150
+ /**
151
+ * Assert a challenge is fully hydrated before signing, so a missing field fails
152
+ * with a named X402Error instead of an opaque viem/BigInt error mid-sign.
153
+ */
154
+ function validateChallenge(c) {
155
+ const bad = (field) => {
156
+ throw new X402Error(`challenge is missing or malformed: ${field}`, 0);
157
+ };
158
+ if (!c || typeof c !== "object") bad("challenge");
159
+ if (!c.id) bad("id");
160
+ if (!c.network) bad("network");
161
+ if (!c.expires_at) bad("expires_at");
162
+ const nb = c.nonce_binding;
163
+ if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("nonce_binding");
164
+ const pr = c.payment_requirements;
165
+ if (!pr) bad("payment_requirements");
166
+ if (!pr.maxAmountRequired) bad("payment_requirements.maxAmountRequired");
167
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.payTo ?? "")) bad("payment_requirements.payTo (expected a 0x address)");
168
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.asset ?? "")) bad("payment_requirements.asset (expected a 0x address)");
169
+ if (!pr.extra?.name || !pr.extra.version) bad("payment_requirements.extra (name/version)");
170
+ }
171
+ var X402Client = class {
172
+ #apiKey;
173
+ #baseUrl;
174
+ #fetch;
175
+ #timeoutMs;
176
+ constructor(options = {}) {
177
+ this.#apiKey = options.apiKey ?? process.env.PRIMITIVE_API_KEY ?? "";
178
+ this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
179
+ this.#fetch = options.fetch ?? fetch;
180
+ this.#timeoutMs = options.timeoutMs ?? 3e4;
181
+ }
182
+ async #request(method, path, body, init) {
183
+ if (!this.#apiKey) throw new X402Error("no API key configured; set PRIMITIVE_API_KEY or pass { apiKey } to the client", 0);
184
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
185
+ const signal = init?.signal ? AbortSignal.any([timeout, init.signal]) : timeout;
186
+ let res;
187
+ try {
188
+ res = await this.#fetch(`${this.#baseUrl}${path}`, {
189
+ method,
190
+ headers: {
191
+ authorization: `Bearer ${this.#apiKey}`,
192
+ "content-type": "application/json"
193
+ },
194
+ body: body === void 0 ? void 0 : JSON.stringify(body),
195
+ signal
196
+ });
197
+ } catch (cause) {
198
+ throw new X402Error(cause instanceof Error && cause.name === "TimeoutError" ? `request to ${path} timed out after ${this.#timeoutMs}ms` : `request to ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`, 0, void 0, { cause });
199
+ }
200
+ const retryAfter = res.headers.get("retry-after");
201
+ const text = await res.text().catch(() => "");
202
+ let json;
203
+ if (text) try {
204
+ json = JSON.parse(text);
205
+ } catch {
206
+ throw new X402Error(`non-JSON response (${res.status}) from ${path}: ${text.slice(0, 200)}`, res.status, text.slice(0, 500), { retryAfter });
207
+ }
208
+ if (!res.ok || json?.success === false) throw new X402Error(json?.error?.message ?? `request failed with ${res.status}`, res.status, json ?? text.slice(0, 500), { retryAfter });
209
+ if (!json || json.success !== true || json.data === void 0) throw new X402Error(`unexpected response shape (${res.status}) from ${path}: missing success/data envelope`, res.status, json ?? text.slice(0, 500), { retryAfter });
210
+ return json.data;
211
+ }
212
+ /** Request a payment (payee side). Returns the challenge to hand to the payer. */
213
+ async charge(input) {
214
+ for (const key of Object.keys(input)) if (!(key in CHARGE_INPUT_KEYS)) throw new X402Error(`unknown charge() option "${key}"; expected one of: ${Object.keys(CHARGE_INPUT_KEYS).join(", ")}`, 0);
215
+ if (!input.amount || !/^[1-9][0-9]{0,38}$/.test(input.amount)) throw new X402Error("charge() requires `amount` as a positive integer string in token base units, e.g. \"10000\"", 0);
216
+ const body = {
217
+ amount: input.amount,
218
+ network: input.network ?? "base-sepolia"
219
+ };
220
+ if (input.payerOrg) body.payer_org = input.payerOrg;
221
+ if (input.description) body.description = input.description;
222
+ if (input.resource) body.resource = input.resource;
223
+ if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
224
+ return this.#request("POST", "/v1/x402/challenges", body);
225
+ }
226
+ /**
227
+ * Pay a challenge (payer side). Derives the interaction-bound authorization,
228
+ * signs it locally with the caller's key, and submits it for settlement.
229
+ */
230
+ async pay(challenge, options) {
231
+ if (!options?.signer?.address || typeof options.signer.signTypedData !== "function") throw new X402Error("pay() requires options.signer with { address, signTypedData } (e.g. a viem LocalAccount)", 0);
232
+ validateChallenge(challenge);
233
+ const chainId = CHAIN_IDS[challenge.network];
234
+ if (chainId === void 0) throw new X402Error(`unsupported network: ${challenge.network}`, 0);
235
+ const pr = challenge.payment_requirements;
236
+ if (pr.network !== challenge.network) throw new X402Error(`challenge network mismatch: ${challenge.network} vs payment_requirements ${pr.network}`, 0);
237
+ if (pr.scheme !== "exact") throw new X402Error(`unsupported payment scheme: ${pr.scheme}`, 0);
238
+ const nonce = deriveEip3009Nonce({
239
+ interactionId: challenge.nonce_binding.interaction_id,
240
+ challengeStepId: challenge.nonce_binding.challenge_step_id,
241
+ challengeNonce: challenge.nonce_binding.challenge_nonce
242
+ });
243
+ const nowSec = Math.floor(Date.now() / 1e3);
244
+ const expiresAtMs = Date.parse(challenge.expires_at);
245
+ if (Number.isNaN(expiresAtMs)) throw new X402Error(`challenge has an invalid expires_at: ${challenge.expires_at}`, 0);
246
+ const expiresAtSec = Math.floor(expiresAtMs / 1e3);
247
+ if (expiresAtSec <= nowSec) throw new X402Error(`challenge has already expired (expires_at ${challenge.expires_at}); not signing`, 0);
248
+ const validAfter = BigInt(nowSec - CLOCK_SKEW_SEC);
249
+ const validBefore = BigInt(expiresAtSec + SETTLEMENT_MARGIN_SEC);
250
+ const auth = {
251
+ from: options.signer.address,
252
+ to: pr.payTo,
253
+ value: BigInt(pr.maxAmountRequired),
254
+ validAfter,
255
+ validBefore,
256
+ nonce
257
+ };
258
+ const signature = await options.signer.signTypedData(transferWithAuthorizationTypedData({
259
+ name: pr.extra.name,
260
+ version: pr.extra.version,
261
+ chainId,
262
+ verifyingContract: pr.asset
263
+ }, auth));
264
+ return this.#request("POST", `/v1/x402/challenges/${challenge.id}/pay`, { payment: toPaymentPayload(challenge.network, auth, signature) });
265
+ }
266
+ /** Fetch a challenge by id (scoped to the challenger org that created it). */
267
+ async getChallenge(id) {
268
+ if (!id) throw new X402Error("getChallenge() requires a challenge id", 0);
269
+ return this.#request("GET", `/v1/x402/challenges/${encodeURIComponent(id)}`);
270
+ }
271
+ /**
272
+ * Register a payout address for your org (payee side). The signer proves
273
+ * control of its own address with an org-bound `personal_sign`; the proven
274
+ * address becomes (or updates to) the default payout destination for the
275
+ * network. `charge()` resolves its `pay_to` from this directory, so a payee
276
+ * must register before requesting payments.
277
+ */
278
+ async registerPayoutAddress(input, options) {
279
+ if (!input?.org) throw new X402Error("registerPayoutAddress() requires an org id", 0);
280
+ if (typeof options?.signer?.signMessage !== "function") throw new X402Error("registerPayoutAddress() requires a signer with signMessage (e.g. a viem LocalAccount)", 0);
281
+ const network = input.network ?? "base-sepolia";
282
+ const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
283
+ const address = options.signer.address;
284
+ const message = buildPayoutRegistrationMessage({
285
+ org: input.org,
286
+ address,
287
+ network,
288
+ issuedAt
289
+ });
290
+ const signature = await options.signer.signMessage({ message });
291
+ return this.#request("POST", "/v1/x402/payout-addresses", {
292
+ address,
293
+ network,
294
+ signature,
295
+ issued_at: issuedAt
296
+ });
297
+ }
298
+ /** List your org's registered payout addresses. */
299
+ async listPayoutAddresses() {
300
+ return this.#request("GET", "/v1/x402/payout-addresses");
301
+ }
302
+ /** Read your org's spend policy (kill-switch + caps + allowlist). */
303
+ async getSpendPolicy() {
304
+ return this.#request("GET", "/v1/x402/spend-policy");
305
+ }
306
+ /**
307
+ * Update your org's spend policy. The endpoint is a PUT, but the server
308
+ * applies it as a merge: only the fields you include are changed and omitted
309
+ * fields keep their current value, so a partial update can't silently reset
310
+ * the kill-switch. Pass `null` to clear a cap.
311
+ */
312
+ async setSpendPolicy(update) {
313
+ return this.#request("PUT", "/v1/x402/spend-policy", update);
314
+ }
315
+ };
316
+ function createX402Client(options = {}) {
317
+ return new X402Client(options);
318
+ }
319
+ //#endregion
320
+ export { TRANSFER_WITH_AUTHORIZATION_TYPES, X402Client, X402Error, buildPayoutRegistrationMessage, createX402Client, deriveEip3009Nonce, toPaymentPayload, transferWithAuthorizationTypedData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -40,6 +40,11 @@
40
40
  "types": "./dist/parser/address.d.ts",
41
41
  "import": "./dist/parser/address.js",
42
42
  "default": "./dist/parser/address.js"
43
+ },
44
+ "./x402": {
45
+ "types": "./dist/x402/index.d.ts",
46
+ "import": "./dist/x402/index.js",
47
+ "default": "./dist/x402/index.js"
43
48
  }
44
49
  },
45
50
  "sideEffects": false,