@siglume/direct-request-payment 0.4.5 → 0.4.7

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.
@@ -16,11 +16,15 @@ order challenge; the buyer-facing Siglume payment flow pays it.
16
16
 
17
17
  This quickstart uses Standard-band example amounts. Micro Payment and Nano
18
18
  Payment are applied automatically by amount through the same Hosted Checkout or
19
- agent/API flow; you do not create a separate Micro/Nano object or pass a
20
- "force Standard" flag. Micro/Nano are settled on account-assigned weekly /
19
+ agent/API flow; you do not create a separate Micro/Nano object or manually
20
+ select the amount band. Micro/Nano are settled on account-assigned weekly /
21
21
  monthly slots after the final notice and close-plus-3-day window (see
22
22
  [Pricing](./pricing.md#settlement-schedule)), and provider revenue remains
23
- unsettled until the later on-chain settlement succeeds.
23
+ unsettled until the later on-chain settlement succeeds. Completing merchant
24
+ setup and the billing mandate means accepting this Micro/Nano delayed aggregated
25
+ settlement model for low-price items. If your product requires immediate
26
+ on-chain settlement, keep its price above the Micro/Nano thresholds instead of
27
+ offering JPY 500-and-under or USD 3-and-under amounts.
24
28
 
25
29
  ## Two Buyer Systems
26
30
 
@@ -396,16 +400,63 @@ const { event } = await verifyDirectRequestPaymentWebhook(
396
400
  siglumeSignatureHeader,
397
401
  );
398
402
 
399
- if (event.type === "direct_payment.confirmed") {
400
- const data = event.data;
403
+ if (event.type !== "direct_payment.confirmed") {
404
+ return new Response(null, { status: 204 });
405
+ }
406
+
407
+ const data = event.data;
408
+
409
+ if (data.mode === "metered_settlement_batch") {
410
+ // Aggregated Micro/Nano settlement events do not carry an order challenge.
411
+ if (data.settlement_status === "settled") {
412
+ await orders.reconcileMeteredSettlementOnce({
413
+ settlement_batch_id: String(data.settlement_batch_id ?? ""),
414
+ chain_receipt_id: String(data.chain_receipt_id ?? ""),
415
+ usage_event_digest: String(data.usage_event_digest ?? ""),
416
+ settled_at: String(data.settled_at ?? ""),
417
+ });
418
+ }
419
+ return new Response(null, { status: 204 });
420
+ }
421
+
422
+ if (
423
+ data.pricing_band === "standard" &&
424
+ data.finality === "per_payment_onchain" &&
425
+ data.settlement_status === "settled"
426
+ ) {
401
427
  const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
402
428
  if (!order) {
403
- throw new Error("Unknown Siglume challenge hash");
429
+ await orders.flagForPaymentStateReview({
430
+ reason: "unknown_challenge_hash",
431
+ requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
432
+ });
433
+ return new Response(null, { status: 204 });
404
434
  }
405
435
  await orders.markPaidOnce(order.id, {
406
436
  siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
437
+ chain_receipt_id: String(data.chain_receipt_id ?? ""),
407
438
  });
439
+ return new Response(null, { status: 204 });
440
+ }
441
+
442
+ if (data.pricing_band === "micro" || data.pricing_band === "nano") {
443
+ const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
444
+ if (order) {
445
+ await orders.markFulfilledButUnsettledOnce(order.id, {
446
+ siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
447
+ pricing_band: String(data.pricing_band),
448
+ });
449
+ }
450
+ return new Response(null, { status: 204 });
408
451
  }
452
+
453
+ // Missing or unknown machine fields: do not mark the order paid from the event
454
+ // name alone. Fetch the requirement or route it to manual review.
455
+ await orders.flagForPaymentStateReview({
456
+ reason: "missing_settlement_machine_fields",
457
+ requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
458
+ });
459
+ return new Response(null, { status: 204 });
409
460
  ```
410
461
 
411
462
  Python:
@@ -421,23 +472,68 @@ verified = verify_direct_request_payment_webhook(
421
472
  siglume_signature_header,
422
473
  )
423
474
 
424
- if verified["event"]["type"] == "direct_payment.confirmed":
425
- data = verified["event"]["data"]
475
+ if verified["event"]["type"] != "direct_payment.confirmed":
476
+ return "", 204
477
+
478
+ data = verified["event"]["data"]
479
+
480
+ if data.get("mode") == "metered_settlement_batch":
481
+ # Aggregated Micro/Nano settlement events do not carry an order challenge.
482
+ if data.get("settlement_status") == "settled":
483
+ orders.reconcile_metered_settlement_once(
484
+ settlement_batch_id=str(data.get("settlement_batch_id") or ""),
485
+ chain_receipt_id=str(data.get("chain_receipt_id") or ""),
486
+ usage_event_digest=str(data.get("usage_event_digest") or ""),
487
+ settled_at=str(data.get("settled_at") or ""),
488
+ )
489
+ return "", 204
490
+
491
+ if (
492
+ data.get("pricing_band") == "standard"
493
+ and data.get("finality") == "per_payment_onchain"
494
+ and data.get("settlement_status") == "settled"
495
+ ):
426
496
  order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
427
497
  if not order:
428
- raise RuntimeError("Unknown Siglume challenge hash")
498
+ orders.flag_for_payment_state_review(
499
+ reason="unknown_challenge_hash",
500
+ requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
501
+ )
502
+ return "", 204
429
503
  orders.mark_paid_once(
430
504
  order["id"],
431
505
  siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
506
+ chain_receipt_id=str(data.get("chain_receipt_id") or ""),
432
507
  )
508
+ return "", 204
509
+
510
+ if data.get("pricing_band") in ("micro", "nano"):
511
+ order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
512
+ if order:
513
+ orders.mark_fulfilled_but_unsettled_once(
514
+ order["id"],
515
+ siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
516
+ pricing_band=str(data.get("pricing_band") or ""),
517
+ )
518
+ return "", 204
519
+
520
+ # Missing or unknown machine fields: do not mark the order paid from the event
521
+ # name alone. Fetch the requirement or route it to manual review.
522
+ orders.flag_for_payment_state_review(
523
+ reason="missing_settlement_machine_fields",
524
+ requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
525
+ )
526
+ return "", 204
433
527
  ```
434
528
 
435
529
  ## Reconcile Micro / Nano Statements
436
530
 
437
- Standard Payment can be fulfilled from the verified
438
- `direct_payment.confirmed` webhook. Micro Payment and Nano Payment are different:
439
- they are automatic amount bands and are settled later in aggregated on-chain
440
- batches. Use the statement APIs to answer:
531
+ Standard Payment can be marked paid from the verified `direct_payment.confirmed`
532
+ webhook only when `pricing_band === "standard"`,
533
+ `finality === "per_payment_onchain"`, and `settlement_status === "settled"`.
534
+ Micro Payment and Nano Payment are different: they are automatic amount bands
535
+ and are settled later in aggregated on-chain batches. Use the statement APIs to
536
+ answer:
441
537
 
442
538
  - how much Micro / Nano usage is open this week or month,
443
539
  - when the buyer's assigned period closes,
@@ -109,7 +109,10 @@ rounding_delta_minor = buyer_debit_minor - gross_buyer_debit_minor
109
109
  For low-count Nano batches, the ceiling can make the effective buyer burden per
110
110
  usage higher than the headline "USD 0.001 / usage" protocol fee. The protocol
111
111
  fee remains the decimal statement amount; the extra integer-minor-unit
112
- adjustment is recorded as `rounding_delta_minor` on the settlement batch.
112
+ adjustment is recorded as `rounding_delta_minor` on the settlement batch. Each
113
+ settlement batch can add a positive rounding adjustment of less than 1 token
114
+ minor unit; if a buyer uses many providers / payees in one period, that
115
+ adjustment can occur once per settlement batch.
113
116
 
114
117
  `rounding_delta_minor` belongs to the buyer debit and Siglume's rounding
115
118
  adjustment accounting for that batch. It is not provider revenue. Provider
@@ -377,9 +380,20 @@ or support staff. Do not depend on raw platform failure messages.
377
380
 
378
381
  ## Usage Accounting by Result
379
382
 
380
- Use idempotency keys for every paid operation. Siglume records one chargeable
381
- usage event per idempotency key within the same buyer / listing / operation
382
- scope.
383
+ Use idempotency keys for every paid operation. For Siglume Marketplace paid
384
+ capability execution and MCP tools, pass `idempotency_key` as a top-level JSON
385
+ field on the execution payload / tool arguments. Do not rely on an HTTP
386
+ `Idempotency-Key` header for this public paid-operation contract; Siglume may
387
+ also use that header internally when it calls providers.
388
+
389
+ The key should be a stable retry key for one logical paid operation, such as
390
+ `order:<order_id>:attempt:<n>` or `<provider_event_id>`, and should not be reused
391
+ for a different payload. The current public contract stores up to 128
392
+ characters. Siglume records one chargeable usage event per idempotency key within
393
+ the same buyer / listing / capability scope; a retry with the same key returns or
394
+ reconciles the first recorded outcome rather than creating another chargeable
395
+ event. If a provider times out after doing work, retry or reconcile with the
396
+ same key before repeating side effects.
383
397
 
384
398
  | Case | Provider API executed? | Usage counted? | Integration rule |
385
399
  | --- | --- | --- | --- |
@@ -446,7 +460,9 @@ separate from settled revenue.
446
460
  ## Go-Live Checklist
447
461
 
448
462
  - Your order fulfillment is idempotent by order id and requirement id.
449
- - Standard Payment fulfillment still uses verified `direct_payment.confirmed`.
463
+ - Standard Payment fulfillment still uses verified `direct_payment.confirmed`
464
+ only when `pricing_band`, `finality`, and `settlement_status` show settled
465
+ per-payment finality.
450
466
  - Micro / Nano accounting uses statement APIs or CSV, not only webhooks.
451
467
  - Your dashboard separates settled, unsettled, and past-due provider amounts.
452
468
  - Your support UI shows sanitized failure fields and `support_reference`.
package/docs/pricing.md CHANGED
@@ -7,7 +7,9 @@ fee data returned at runtime.
7
7
 
8
8
  Pricing has one structure: a merchant selects the Standard Payment plan during
9
9
  setup, then Siglume applies the fee for each payment by amount. Micro Payment
10
- and Nano Payment are automatic amount bands, not separate choices.
10
+ and Nano Payment are automatic amount bands, not separate choices. Merchant
11
+ setup and the billing mandate terms assume the merchant accepts Micro / Nano
12
+ delayed aggregated settlement whenever they offer amounts in those bands.
11
13
 
12
14
  ## Settlement Currencies
13
15
 
@@ -36,10 +38,17 @@ aggregated and settled in account-assigned weekly / monthly slots - see
36
38
  pre-debit notice window elapses, when revenue becomes settled, and how rejected
37
39
  requests behave.
38
40
 
39
- The current public API chooses the band from `amount_minor`; it does not expose
40
- `settlement_mode: "immediate"` or `require_immediate_finality: true`. If a
41
- merchant needs immediate on-chain finality, the payment amount must be in the
42
- Standard band or the merchant must have a separately agreed platform contract.
41
+ The current public API chooses the band from `amount_minor`; JPY 500-and-under /
42
+ USD 3-and-under payments are routed to Micro / Nano delayed aggregated
43
+ settlement. If a merchant needs immediate on-chain finality, the payment amount
44
+ must be in the Standard band. In practice, do not offer JPY 500-and-under or
45
+ USD 3-and-under items for a product that cannot accept Micro / Nano delayed
46
+ aggregated settlement. For public one-time Direct Payment / Hosted Checkout,
47
+ `amount_minor` is a positive integer in minor currency units. That means the
48
+ smallest public one-time checkout amount is JPY 1 or USD 0.01. Nano Payment on
49
+ this public path therefore means JPY 1-49 or USD 0.01-0.30. Sub-minor Nano
50
+ protocol fees are settlement-accounting amounts, not externally submitted
51
+ one-time item prices.
43
52
 
44
53
  For the operational statement APIs, CSV export, buyer past-due blocks, and the
45
54
  field-by-field meaning of `scheduled_debit_at`, `not_before_attempt_at`,
@@ -183,8 +192,11 @@ For low-count Nano batches, the integer ceiling can make the effective buyer
183
192
  burden per usage higher than the headline USD 0.001 / usage protocol fee. The
184
193
  decimal protocol fee remains visible as `protocol_fee_minor`; the difference
185
194
  created by integer-token settlement is visible as `rounding_delta_minor` on the
186
- batch. JavaScript integrations should not sum Micro / Nano minor amounts with
187
- `number`; use a decimal library. Python integrations should use `Decimal`.
195
+ batch. Each settlement batch can add a positive rounding adjustment of less than
196
+ 1 token minor unit. If a buyer uses many providers / payees in one period, that
197
+ adjustment can occur once per settlement batch. JavaScript integrations should
198
+ not sum Micro / Nano minor amounts with `number`; use a decimal library. Python
199
+ integrations should use `Decimal`.
188
200
 
189
201
  ## Statement APIs and Notices
190
202
 
package/docs/security.md CHANGED
@@ -125,6 +125,13 @@ merchant-authored challenge nonce plus the returned `challenge_hash` /
125
125
  `request_hash_v2`. Reuse the same order-attempt nonce when reconciling a retry;
126
126
  mint a new nonce only for a new payment attempt.
127
127
 
128
+ For Micro / Nano paid capability execution through Siglume Marketplace or MCP
129
+ tools, use the top-level JSON field `idempotency_key` on the execution payload /
130
+ tool arguments. Treat it as a stable retry key for one logical paid operation
131
+ and do not reuse it for a different payload. The HTTP `Idempotency-Key` header is
132
+ not the public contract for this requirement-create API; Siglume may use headers
133
+ internally when calling providers.
134
+
128
135
  ## Micro / Nano Statement Privacy
129
136
 
130
137
  Micro Payment and Nano Payment introduce operational statement APIs and CSV
@@ -22,6 +22,48 @@ app.use((req, res, next) => {
22
22
 
23
23
  const orders = new Map<string, any>();
24
24
 
25
+ async function handleDirectPaymentConfirmed(data: Record<string, any>): Promise<void> {
26
+ if (data.mode === "metered_settlement_batch") {
27
+ // Aggregated Micro/Nano settlement events do not carry an order challenge.
28
+ // Reconcile them against statement / settlement batch data instead.
29
+ if (data.settlement_status === "settled") {
30
+ console.log("settled metered batch", data.settlement_batch_id || data.usage_event_digest);
31
+ }
32
+ return;
33
+ }
34
+
35
+ if (
36
+ data.pricing_band === "standard" &&
37
+ data.finality === "per_payment_onchain" &&
38
+ data.settlement_status === "settled"
39
+ ) {
40
+ const challengeHash = String(data.challenge_hash || "");
41
+ const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
42
+ if (order) {
43
+ order.siglume_payment_status = "paid";
44
+ order.siglume_requirement_id = data.requirement_id || data.direct_payment_requirement_id;
45
+ order.siglume_chain_receipt_id = data.chain_receipt_id || null;
46
+ }
47
+ return;
48
+ }
49
+
50
+ if (data.pricing_band === "micro" || data.pricing_band === "nano") {
51
+ const challengeHash = String(data.challenge_hash || "");
52
+ const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
53
+ if (order) {
54
+ order.siglume_payment_status = "fulfilled_unsettled";
55
+ order.siglume_requirement_id = data.requirement_id || data.direct_payment_requirement_id;
56
+ }
57
+ return;
58
+ }
59
+
60
+ // Unknown or older payload shape: do not mark paid from the event name alone.
61
+ console.warn("direct_payment.confirmed missing settlement machine fields", {
62
+ id: data.id,
63
+ requirement_id: data.requirement_id || data.direct_payment_requirement_id,
64
+ });
65
+ }
66
+
25
67
  const asyncRoute =
26
68
  (handler: express.RequestHandler): express.RequestHandler =>
27
69
  (req, res, next) => {
@@ -69,12 +111,7 @@ app.post("/siglume/webhook", express.raw({ type: "application/json" }), asyncRou
69
111
  );
70
112
 
71
113
  if (event.type === "direct_payment.confirmed") {
72
- const challengeHash = String(event.data.challenge_hash || "");
73
- const order = [...orders.values()].find((item) => item.siglume_challenge_hash === challengeHash);
74
- if (order) {
75
- order.siglume_payment_status = "paid";
76
- order.siglume_requirement_id = event.data.requirement_id || event.data.direct_payment_requirement_id;
77
- }
114
+ await handleDirectPaymentConfirmed(event.data);
78
115
  }
79
116
 
80
117
  res.status(204).send();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siglume/direct-request-payment",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",