@bitpal/checkout 0.8.0 → 0.10.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 CHANGED
@@ -180,6 +180,7 @@ app.post(
180
180
  | `checkout.session.expired` | Session expired unpaid |
181
181
  | `checkout.session.unresolved` | Underpaid / overpaid / wrong token — needs review |
182
182
  | `checkout.refund.recorded` | An on-chain auto-refund (over / under / late payment) was recorded |
183
+ | `x402.payment.resolved` | An x402 payment that `/settle` left unresolved was later settled on-chain (see below) |
183
184
 
184
185
  ### ⚠️ Replay defense + at-least-once delivery
185
186
 
@@ -208,7 +209,7 @@ app.get('/report', x402({
208
209
  apiKey: process.env.BITPAL_API_KEY, // bp_test_… or bp_live_…
209
210
  payTo: '0xYourPayoutWallet',
210
211
  network: 'base', // test key → 'base-sepolia'
211
- amount: '1000000', // atomic (USDC, 6dp) — your gross; the fee comes out of it
212
+ amount: '1.00', // human-readable — no decimals math
212
213
  }), (req, res) => {
213
214
  res.json({ report: '…' }); // req.x402Payment has the txHash
214
215
  });
@@ -227,6 +228,12 @@ handler with `X-PAYMENT-RESPONSE` set. The middleware also does what the API can
227
228
  timeout — answers `503` and asks for a retry with the *same* header. Handing back a `402` there
228
229
  would make the caller re-sign and pay twice.
229
230
 
231
+ When `/settle` answers `503`, nobody knows yet whether the money moved — so BitPal watches the chain
232
+ and tells you how it ended with a **`x402.payment.resolved`** webhook (`status`: `confirmed` |
233
+ `reverted` | `expired`, plus `payment_id` and `tx_hash`). It fires only on that path: the normal case
234
+ already returned the result to you, and you should not have to handle the same outcome twice. Verify
235
+ it exactly like a checkout webhook.
236
+
230
237
  Not on Express? `X402Gate` is the framework-neutral core — give it the header and the resource URL,
231
238
  it tells you whether to bill or serve. On a `settled` result, call `commit()` once you have delivered
232
239
  the response, or `release()` if you could not.
@@ -267,6 +274,9 @@ if (res.status === 402) {
267
274
  | Base | USDC | `base` |
268
275
  | Base Sepolia | USDC | `base-sepolia` |
269
276
 
277
+ Amounts are human-readable (`'1.00'`), the same model as `line_items[].amount` — the server converts.
278
+ Pass `amountAtomic` instead if you already have raw units.
279
+
270
280
  EOA signers only — smart-contract wallets cannot sign EIP-3009 authorizations. Amounts are exact:
271
281
  over- and underpayment are both rejected, and this lane has no refund path. Authorizations are valid
272
282
  for 90–3600 seconds (the server's floor is 60s; the extra 30s absorbs clock skew and round trips).
package/dist/index.cjs CHANGED
@@ -315,12 +315,23 @@ var BitPalCheckoutClient = class {
315
315
  );
316
316
  }
317
317
  /* ─── Fees ─── */
318
- /** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
318
+ /**
319
+ * 수수료 미리보기 — `/v1/checkout/fee-preview`.
320
+ *
321
+ * `amount` 는 **사람 표기**다("100", "0.5") — line item / Price / x402 와 같은 모델이라
322
+ * decimals 를 계산할 필요가 없다. raw μUSD 를 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
323
+ * 응답은 계속 atomic 이다(계산 결과라 정밀도가 곧 계약이다).
324
+ *
325
+ * @example
326
+ * const { data } = await bp.checkout.previewFee('100'); // $100
327
+ * const { data } = await bp.checkout.previewFee({ amountAtomic: '100000000' });
328
+ */
319
329
  async previewFee(amount) {
320
- return this.request(
321
- "GET",
322
- `/v1/checkout/fee-preview?amount=${encodeURIComponent(amount)}`
323
- );
330
+ const qs = typeof amount === "string" ? `amount=${encodeURIComponent(amount)}` : `amount_atomic=${encodeURIComponent(amount.amountAtomic)}`;
331
+ if (typeof amount === "string" && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(amount)) {
332
+ throw new Error('[BitPal] previewFee: amount must be a decimal string like "100" or "0.5" (max 6 decimals). For raw \u03BCUSD use { amountAtomic }.');
333
+ }
334
+ return this.request("GET", `/v1/checkout/fee-preview?${qs}`);
324
335
  }
325
336
  };
326
337
  var BitPalError = class extends Error {
@@ -340,7 +351,12 @@ var CHECKOUT_WEBHOOK_EVENTS = [
340
351
  "checkout.session.settled",
341
352
  "checkout.session.expired",
342
353
  "checkout.session.unresolved",
343
- "checkout.refund.recorded"
354
+ "checkout.refund.recorded",
355
+ // x402 결제의 **사후** 확정. 정상 경로(/settle 이 그 자리에서 결과를 반환)는 발행되지 않는다 —
356
+ // 이미 아는 결과를 중복으로 받지 않게 하기 위함이다. /settle 이 503 을 반환해 결과를 모른 채
357
+ // 남았을 때, reconciler 가 체인을 보고 종결시키면 이 이벤트가 온다.
358
+ // payload: { payment_id, status: 'confirmed'|'reverted'|'expired', tx_hash, resource, amount, … }
359
+ "x402.payment.resolved"
344
360
  ];
345
361
  var WEBHOOK_HEADERS = {
346
362
  signature: "X-Webhook-Signature-256",
@@ -476,8 +492,16 @@ var X402Gate = class {
476
492
  if (!/^0x[a-fA-F0-9]{40}$/.test(opts.payTo)) {
477
493
  throw new Error("[BitPal] x402: payTo must be a 0x EVM address (your registered payout wallet).");
478
494
  }
479
- if (!/^\d+$/.test(opts.amount)) {
480
- throw new Error('[BitPal] x402: amount must be an atomic integer string (USDC 6dp, $1 = "1000000").');
495
+ const hasHuman = opts.amount != null;
496
+ const hasAtomic = opts.amountAtomic != null;
497
+ if (hasHuman === hasAtomic) {
498
+ throw new Error('[BitPal] x402: provide exactly one of `amount` ("0.1") or `amountAtomic` ("100000").');
499
+ }
500
+ if (hasHuman && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(opts.amount)) {
501
+ throw new Error('[BitPal] x402: amount must be a decimal string like "0.1" or "29" (max 6 decimals).');
502
+ }
503
+ if (hasAtomic && !/^\d+$/.test(opts.amountAtomic)) {
504
+ throw new Error('[BitPal] x402: amountAtomic must be an integer string (USDC 6dp, $1 = "1000000").');
481
505
  }
482
506
  const timeoutSeconds = opts.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
483
507
  if (timeoutSeconds < MIN_VALIDITY_SECONDS || timeoutSeconds > MAX_VALIDITY_SECONDS) {
@@ -495,7 +519,8 @@ var X402Gate = class {
495
519
  async requirements(resource) {
496
520
  return this.request("POST", "/v1/x402/requirements", {
497
521
  payTo: this.opts.payTo,
498
- amount: this.opts.amount,
522
+ // 사람 표기 → atomic 변환은 **서버가** 한다. SDK 가 decimals 를 알 필요가 없다.
523
+ ...this.opts.amount != null ? { amount: this.opts.amount } : { amountAtomic: this.opts.amountAtomic },
499
524
  resource,
500
525
  ...this.opts.network ? { network: this.opts.network } : {},
501
526
  ...this.opts.asset ? { asset: this.opts.asset } : {},
package/dist/index.d.cts CHANGED
@@ -556,8 +556,20 @@ declare class BitPalCheckoutClient {
556
556
  * 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
557
557
  */
558
558
  getStatus(sessionId: string): Promise<ApiResponse<SessionStatus>>;
559
- /** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
560
- previewFee(amount: string): Promise<ApiResponse<FeePreview>>;
559
+ /**
560
+ * 수수료 미리보기 — `/v1/checkout/fee-preview`.
561
+ *
562
+ * `amount` 는 **사람 표기**다("100", "0.5") — line item / Price / x402 와 같은 모델이라
563
+ * decimals 를 계산할 필요가 없다. raw μUSD 를 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
564
+ * 응답은 계속 atomic 이다(계산 결과라 정밀도가 곧 계약이다).
565
+ *
566
+ * @example
567
+ * const { data } = await bp.checkout.previewFee('100'); // $100
568
+ * const { data } = await bp.checkout.previewFee({ amountAtomic: '100000000' });
569
+ */
570
+ previewFee(amount: string | {
571
+ amountAtomic: string;
572
+ }): Promise<ApiResponse<FeePreview>>;
561
573
  }
562
574
  declare class BitPalError extends Error {
563
575
  readonly statusCode: number;
@@ -572,7 +584,7 @@ declare class BitPalError extends Error {
572
584
  /**
573
585
  * BitPal 이 발송하는 webhook 이벤트 타입 전체 목록.
574
586
  */
575
- declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded"];
587
+ declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded", "x402.payment.resolved"];
576
588
  type CheckoutWebhookEvent = (typeof CHECKOUT_WEBHOOK_EVENTS)[number];
577
589
  /**
578
590
  * Canonical envelope (HTTP body).
@@ -785,8 +797,14 @@ interface X402GateOptions {
785
797
  apiKey: string;
786
798
  /** 정산 지갑. 콘솔에 등록한 payout 주소와 같은 값이어야 한다. */
787
799
  payTo: string;
788
- /** 호출 1건당 받을 총액(atomic). USDC 6dp 기준 $1 = `'1000000'`. 수수료는 여기서 차감된다. */
789
- amount: string;
800
+ /**
801
+ * 호출 1건당 받을 총액. **사람 표기 문자열**이다 — `'0.1'`, `'1'`, `'29.00'`.
802
+ * 체인·토큰 decimals 를 계산할 필요가 없다(체크아웃 `line_items[].amount` 와 같은 모델).
803
+ * 수수료는 이 금액에서 차감된다. raw atomic 을 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
804
+ */
805
+ amount?: string;
806
+ /** raw atomic 정수 문자열(고급). USDC 6dp 기준 $1 = `'1000000'`. `amount` 와 상호 배타. */
807
+ amountAtomic?: string;
790
808
  /** 생략 시 API 키 모드의 기본 자산(현재 Base / Base Sepolia). */
791
809
  network?: string;
792
810
  /** 기본 `'USDC'`. */
package/dist/index.d.ts CHANGED
@@ -556,8 +556,20 @@ declare class BitPalCheckoutClient {
556
556
  * 통지되므로, 서버 사이드 확정은 webhook 을 신뢰(폴링은 buyer UX 용).
557
557
  */
558
558
  getStatus(sessionId: string): Promise<ApiResponse<SessionStatus>>;
559
- /** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
560
- previewFee(amount: string): Promise<ApiResponse<FeePreview>>;
559
+ /**
560
+ * 수수료 미리보기 — `/v1/checkout/fee-preview`.
561
+ *
562
+ * `amount` 는 **사람 표기**다("100", "0.5") — line item / Price / x402 와 같은 모델이라
563
+ * decimals 를 계산할 필요가 없다. raw μUSD 를 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
564
+ * 응답은 계속 atomic 이다(계산 결과라 정밀도가 곧 계약이다).
565
+ *
566
+ * @example
567
+ * const { data } = await bp.checkout.previewFee('100'); // $100
568
+ * const { data } = await bp.checkout.previewFee({ amountAtomic: '100000000' });
569
+ */
570
+ previewFee(amount: string | {
571
+ amountAtomic: string;
572
+ }): Promise<ApiResponse<FeePreview>>;
561
573
  }
562
574
  declare class BitPalError extends Error {
563
575
  readonly statusCode: number;
@@ -572,7 +584,7 @@ declare class BitPalError extends Error {
572
584
  /**
573
585
  * BitPal 이 발송하는 webhook 이벤트 타입 전체 목록.
574
586
  */
575
- declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded"];
587
+ declare const CHECKOUT_WEBHOOK_EVENTS: readonly ["checkout.session.created", "checkout.session.deposit_detected", "checkout.session.paid", "checkout.session.settled", "checkout.session.expired", "checkout.session.unresolved", "checkout.refund.recorded", "x402.payment.resolved"];
576
588
  type CheckoutWebhookEvent = (typeof CHECKOUT_WEBHOOK_EVENTS)[number];
577
589
  /**
578
590
  * Canonical envelope (HTTP body).
@@ -785,8 +797,14 @@ interface X402GateOptions {
785
797
  apiKey: string;
786
798
  /** 정산 지갑. 콘솔에 등록한 payout 주소와 같은 값이어야 한다. */
787
799
  payTo: string;
788
- /** 호출 1건당 받을 총액(atomic). USDC 6dp 기준 $1 = `'1000000'`. 수수료는 여기서 차감된다. */
789
- amount: string;
800
+ /**
801
+ * 호출 1건당 받을 총액. **사람 표기 문자열**이다 — `'0.1'`, `'1'`, `'29.00'`.
802
+ * 체인·토큰 decimals 를 계산할 필요가 없다(체크아웃 `line_items[].amount` 와 같은 모델).
803
+ * 수수료는 이 금액에서 차감된다. raw atomic 을 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
804
+ */
805
+ amount?: string;
806
+ /** raw atomic 정수 문자열(고급). USDC 6dp 기준 $1 = `'1000000'`. `amount` 와 상호 배타. */
807
+ amountAtomic?: string;
790
808
  /** 생략 시 API 키 모드의 기본 자산(현재 Base / Base Sepolia). */
791
809
  network?: string;
792
810
  /** 기본 `'USDC'`. */
package/dist/index.js CHANGED
@@ -256,12 +256,23 @@ var BitPalCheckoutClient = class {
256
256
  );
257
257
  }
258
258
  /* ─── Fees ─── */
259
- /** 수수료 미리보기 — `/v1/checkout/fee-preview` (Phase 1 credit system 폐기 후 canonical path) */
259
+ /**
260
+ * 수수료 미리보기 — `/v1/checkout/fee-preview`.
261
+ *
262
+ * `amount` 는 **사람 표기**다("100", "0.5") — line item / Price / x402 와 같은 모델이라
263
+ * decimals 를 계산할 필요가 없다. raw μUSD 를 직접 넣으려면 `amountAtomic` 을 쓴다(상호 배타).
264
+ * 응답은 계속 atomic 이다(계산 결과라 정밀도가 곧 계약이다).
265
+ *
266
+ * @example
267
+ * const { data } = await bp.checkout.previewFee('100'); // $100
268
+ * const { data } = await bp.checkout.previewFee({ amountAtomic: '100000000' });
269
+ */
260
270
  async previewFee(amount) {
261
- return this.request(
262
- "GET",
263
- `/v1/checkout/fee-preview?amount=${encodeURIComponent(amount)}`
264
- );
271
+ const qs = typeof amount === "string" ? `amount=${encodeURIComponent(amount)}` : `amount_atomic=${encodeURIComponent(amount.amountAtomic)}`;
272
+ if (typeof amount === "string" && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(amount)) {
273
+ throw new Error('[BitPal] previewFee: amount must be a decimal string like "100" or "0.5" (max 6 decimals). For raw \u03BCUSD use { amountAtomic }.');
274
+ }
275
+ return this.request("GET", `/v1/checkout/fee-preview?${qs}`);
265
276
  }
266
277
  };
267
278
  var BitPalError = class extends Error {
@@ -281,7 +292,12 @@ var CHECKOUT_WEBHOOK_EVENTS = [
281
292
  "checkout.session.settled",
282
293
  "checkout.session.expired",
283
294
  "checkout.session.unresolved",
284
- "checkout.refund.recorded"
295
+ "checkout.refund.recorded",
296
+ // x402 결제의 **사후** 확정. 정상 경로(/settle 이 그 자리에서 결과를 반환)는 발행되지 않는다 —
297
+ // 이미 아는 결과를 중복으로 받지 않게 하기 위함이다. /settle 이 503 을 반환해 결과를 모른 채
298
+ // 남았을 때, reconciler 가 체인을 보고 종결시키면 이 이벤트가 온다.
299
+ // payload: { payment_id, status: 'confirmed'|'reverted'|'expired', tx_hash, resource, amount, … }
300
+ "x402.payment.resolved"
285
301
  ];
286
302
  var WEBHOOK_HEADERS = {
287
303
  signature: "X-Webhook-Signature-256",
@@ -417,8 +433,16 @@ var X402Gate = class {
417
433
  if (!/^0x[a-fA-F0-9]{40}$/.test(opts.payTo)) {
418
434
  throw new Error("[BitPal] x402: payTo must be a 0x EVM address (your registered payout wallet).");
419
435
  }
420
- if (!/^\d+$/.test(opts.amount)) {
421
- throw new Error('[BitPal] x402: amount must be an atomic integer string (USDC 6dp, $1 = "1000000").');
436
+ const hasHuman = opts.amount != null;
437
+ const hasAtomic = opts.amountAtomic != null;
438
+ if (hasHuman === hasAtomic) {
439
+ throw new Error('[BitPal] x402: provide exactly one of `amount` ("0.1") or `amountAtomic` ("100000").');
440
+ }
441
+ if (hasHuman && !/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(opts.amount)) {
442
+ throw new Error('[BitPal] x402: amount must be a decimal string like "0.1" or "29" (max 6 decimals).');
443
+ }
444
+ if (hasAtomic && !/^\d+$/.test(opts.amountAtomic)) {
445
+ throw new Error('[BitPal] x402: amountAtomic must be an integer string (USDC 6dp, $1 = "1000000").');
422
446
  }
423
447
  const timeoutSeconds = opts.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
424
448
  if (timeoutSeconds < MIN_VALIDITY_SECONDS || timeoutSeconds > MAX_VALIDITY_SECONDS) {
@@ -436,7 +460,8 @@ var X402Gate = class {
436
460
  async requirements(resource) {
437
461
  return this.request("POST", "/v1/x402/requirements", {
438
462
  payTo: this.opts.payTo,
439
- amount: this.opts.amount,
463
+ // 사람 표기 → atomic 변환은 **서버가** 한다. SDK 가 decimals 를 알 필요가 없다.
464
+ ...this.opts.amount != null ? { amount: this.opts.amount } : { amountAtomic: this.opts.amountAtomic },
440
465
  resource,
441
466
  ...this.opts.network ? { network: this.opts.network } : {},
442
467
  ...this.opts.asset ? { asset: this.opts.asset } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpal/checkout",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "BitPal Checkout SDK — non-custodial crypto checkout: create sessions, issue per-order deposit addresses, poll status, verify signed webhooks, and charge per API call with x402.",
5
5
  "license": "MIT",
6
6
  "repository": {