@primitivedotdev/sdk 1.4.0 → 1.6.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.
@@ -184,9 +184,27 @@ interface X402SpendPolicy {
184
184
  /** Allowed payee org ids; null = any on-net payee, [] = deny all. */
185
185
  allowlist: string[] | null;
186
186
  }
187
- interface X402ChargeInput {
188
- /** Amount in token base units (USDC has 6 decimals, so "10000" = 0.01). */
187
+ /** A payment the org's spend policy refused (read shape). */
188
+ interface X402DeclinedPayment {
189
+ id: string;
190
+ challenge_id: string | null;
191
+ counterparty_org: string | null;
192
+ network: string;
189
193
  amount: string;
194
+ reason: string;
195
+ declined_at: string;
196
+ }
197
+ interface X402ChargeInput {
198
+ /**
199
+ * Amount in token base units (USDC has 6 decimals, so "10000" = 0.01).
200
+ * Provide exactly one of `amount` or `amountUsdc`.
201
+ */
202
+ amount?: string;
203
+ /**
204
+ * Amount as human USDC (e.g. "0.01"), converted to base units for you.
205
+ * Provide exactly one of `amount` or `amountUsdc`.
206
+ */
207
+ amountUsdc?: string;
190
208
  /** Defaults to "base-sepolia". */
191
209
  network?: string;
192
210
  /** The org id allowed to pay this challenge (on-net binding). */
@@ -196,6 +214,11 @@ interface X402ChargeInput {
196
214
  resource?: string;
197
215
  /** Seconds until the challenge expires (default 1h). */
198
216
  expiresIn?: number;
217
+ /**
218
+ * Optional idempotency key. Retrying `charge()` with the same key returns the
219
+ * original challenge instead of creating a duplicate.
220
+ */
221
+ idempotencyKey?: string;
199
222
  }
200
223
  declare class X402Error extends Error {
201
224
  /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
@@ -238,16 +261,25 @@ declare class X402Client {
238
261
  * address becomes (or updates to) the default payout destination for the
239
262
  * network. `charge()` resolves its `pay_to` from this directory, so a payee
240
263
  * must register before requesting payments.
264
+ *
265
+ * `org` is optional: when omitted it is resolved from your authenticated
266
+ * account, so most callers never need to supply it.
241
267
  */
242
268
  registerPayoutAddress(input: {
243
- org: string;
269
+ org?: string;
244
270
  network?: string;
245
271
  issuedAt?: string;
272
+ label?: string;
246
273
  }, options: {
247
274
  signer: X402Signer;
248
275
  }): Promise<X402PayoutAddress>;
249
276
  /** List your org's registered payout addresses. */
250
277
  listPayoutAddresses(): Promise<X402PayoutAddress[]>;
278
+ /**
279
+ * List the most recent payments your org's spend policy declined (newest
280
+ * first). Use this to see why an outbound payment was refused.
281
+ */
282
+ listDeclinedPayments(): Promise<X402DeclinedPayment[]>;
251
283
  /** Read your org's spend policy (kill-switch + caps + allowlist). */
252
284
  getSpendPolicy(): Promise<X402SpendPolicy>;
253
285
  /**
@@ -260,4 +292,4 @@ declare class X402Client {
260
292
  }
261
293
  declare function createX402Client(options?: X402ClientOptions): X402Client;
262
294
  //#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 };
295
+ export { NonceBinding, PayoutRegistrationMessageInput, TRANSFER_WITH_AUTHORIZATION_TYPES, TokenDomain, TransferAuthorization, TransferWithAuthorizationTypedData, X402Challenge, X402ChargeInput, X402Client, X402ClientOptions, X402DeclinedPayment, X402Error, X402PaymentPayload, X402PaymentRequirements, X402PayoutAddress, X402Receipt, X402Signer, X402SpendPolicy, buildPayoutRegistrationMessage, createX402Client, deriveEip3009Nonce, toPaymentPayload, transferWithAuthorizationTypedData };
@@ -127,12 +127,22 @@ const SETTLEMENT_MARGIN_SEC = 300;
127
127
  const DEFAULT_BASE_URL = "https://api.primitive.dev";
128
128
  const CHARGE_INPUT_KEYS = {
129
129
  amount: true,
130
+ amountUsdc: true,
130
131
  network: true,
131
132
  payerOrg: true,
132
133
  description: true,
133
134
  resource: true,
134
- expiresIn: true
135
+ expiresIn: true,
136
+ idempotencyKey: true
135
137
  };
138
+ function usdcToBaseUnits(human) {
139
+ const trimmed = human.trim();
140
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
141
+ const [whole, frac = ""] = trimmed.split(".");
142
+ if (frac.length > 6) return null;
143
+ const base = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
144
+ return base > 0n ? base.toString() : null;
145
+ }
136
146
  var X402Error = class extends Error {
137
147
  /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
138
148
  status;
@@ -189,7 +199,8 @@ var X402Client = class {
189
199
  method,
190
200
  headers: {
191
201
  authorization: `Bearer ${this.#apiKey}`,
192
- "content-type": "application/json"
202
+ "content-type": "application/json",
203
+ ...init?.headers
193
204
  },
194
205
  body: body === void 0 ? void 0 : JSON.stringify(body),
195
206
  signal
@@ -212,16 +223,18 @@ var X402Client = class {
212
223
  /** Request a payment (payee side). Returns the challenge to hand to the payer. */
213
224
  async charge(input) {
214
225
  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);
226
+ if (input.amount !== void 0 && input.amountUsdc !== void 0) throw new X402Error("charge() takes exactly one of `amount` (base units) or `amountUsdc` (human USDC), not both", 0);
227
+ const amount = input.amountUsdc !== void 0 ? usdcToBaseUnits(input.amountUsdc) : input.amount ?? null;
228
+ if (!amount || !/^[1-9][0-9]{0,38}$/.test(amount)) throw new X402Error("charge() requires `amount` as a positive integer string in token base units (e.g. \"10000\"), or `amountUsdc` as a positive USDC amount with at most 6 decimals (e.g. \"0.01\")", 0);
216
229
  const body = {
217
- amount: input.amount,
230
+ amount,
218
231
  network: input.network ?? "base-sepolia"
219
232
  };
220
233
  if (input.payerOrg) body.payer_org = input.payerOrg;
221
234
  if (input.description) body.description = input.description;
222
235
  if (input.resource) body.resource = input.resource;
223
236
  if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
224
- return this.#request("POST", "/v1/x402/challenges", body);
237
+ return this.#request("POST", "/v1/x402/challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
225
238
  }
226
239
  /**
227
240
  * Pay a challenge (payer side). Derives the interaction-bound authorization,
@@ -268,21 +281,30 @@ var X402Client = class {
268
281
  if (!id) throw new X402Error("getChallenge() requires a challenge id", 0);
269
282
  return this.#request("GET", `/v1/x402/challenges/${encodeURIComponent(id)}`);
270
283
  }
284
+ /** Resolve the caller's own organization id from the account endpoint. */
285
+ async #resolveOrgId() {
286
+ const account = await this.#request("GET", "/v1/account");
287
+ if (!account?.id) throw new X402Error("could not resolve your organization id from /v1/account; pass { org } explicitly", 0);
288
+ return account.id;
289
+ }
271
290
  /**
272
291
  * Register a payout address for your org (payee side). The signer proves
273
292
  * control of its own address with an org-bound `personal_sign`; the proven
274
293
  * address becomes (or updates to) the default payout destination for the
275
294
  * network. `charge()` resolves its `pay_to` from this directory, so a payee
276
295
  * must register before requesting payments.
296
+ *
297
+ * `org` is optional: when omitted it is resolved from your authenticated
298
+ * account, so most callers never need to supply it.
277
299
  */
278
300
  async registerPayoutAddress(input, options) {
279
- if (!input?.org) throw new X402Error("registerPayoutAddress() requires an org id", 0);
280
301
  if (typeof options?.signer?.signMessage !== "function") throw new X402Error("registerPayoutAddress() requires a signer with signMessage (e.g. a viem LocalAccount)", 0);
302
+ const org = input.org ?? await this.#resolveOrgId();
281
303
  const network = input.network ?? "base-sepolia";
282
304
  const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
283
305
  const address = options.signer.address;
284
306
  const message = buildPayoutRegistrationMessage({
285
- org: input.org,
307
+ org,
286
308
  address,
287
309
  network,
288
310
  issuedAt
@@ -292,13 +314,21 @@ var X402Client = class {
292
314
  address,
293
315
  network,
294
316
  signature,
295
- issued_at: issuedAt
317
+ issued_at: issuedAt,
318
+ ...input.label !== void 0 ? { label: input.label } : {}
296
319
  });
297
320
  }
298
321
  /** List your org's registered payout addresses. */
299
322
  async listPayoutAddresses() {
300
323
  return this.#request("GET", "/v1/x402/payout-addresses");
301
324
  }
325
+ /**
326
+ * List the most recent payments your org's spend policy declined (newest
327
+ * first). Use this to see why an outbound payment was refused.
328
+ */
329
+ async listDeclinedPayments() {
330
+ return this.#request("GET", "/v1/x402/declined-payments");
331
+ }
302
332
  /** Read your org's spend policy (kill-switch + caps + allowlist). */
303
333
  async getSpendPolicy() {
304
334
  return this.#request("GET", "/v1/x402/spend-policy");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",