@siglume/direct-request-payment 0.4.5 → 0.4.6

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.
@@ -26,9 +26,11 @@ card — and the merchant SDK never authenticates the buyer.
26
26
  runs are bounded by Siglume's approval gates / spending budgets (per-run /
27
27
  daily / monthly auto-pay budgets, or Works approval).
28
28
 
29
- In both systems the merchant fulfills on the same signed
30
- `direct_payment.confirmed` webhook. Hosted Checkout adds no new money movement
31
- and no new webhook.
29
+ In both systems the merchant handles the same signed `direct_payment.confirmed`
30
+ webhook. Hosted Checkout adds no new money movement and no new webhook. Inspect
31
+ `pricing_band`, `finality`, and `settlement_status`: Standard can be marked paid
32
+ only after settled per-payment finality, while Micro / Nano usage is accepted
33
+ before the later aggregated settlement.
32
34
 
33
35
  ## Environment Variables
34
36
 
@@ -364,8 +366,9 @@ webhook-origin auto-allow apply.
364
366
  Beta / server rollout: Hosted Checkout is rolling out account by account. If the
365
367
  server endpoint is not enabled for the merchant yet, the SDK raises
366
368
  `HostedCheckoutNotAvailableError` (TS + Py) rather than leaking a raw rollout
367
- 404/409. Fulfillment must still key off the signed `direct_payment.confirmed`
368
- webhook.
369
+ 404/409. Payment handling must still key off the signed
370
+ `direct_payment.confirmed` webhook and its settlement machine fields, not the
371
+ event name alone.
369
372
 
370
373
  Creates a single-use, expiring Hosted Checkout session for a human web shopper
371
374
  and returns the URL to redirect them to. Requires the merchant's Siglume bearer
@@ -447,11 +450,24 @@ Returns a `HostedCheckoutSession` status object with:
447
450
  `failed`
448
451
  - `challenge_hash`
449
452
  - `requirement_id` (nullable until a requirement is created)
453
+ - `pricing_band` (nullable until a requirement is created): `standard`,
454
+ `micro`, or `nano`
455
+ - `settlement_cadence` (nullable until a requirement is created):
456
+ `per_payment`, `weekly`, or `monthly`
457
+ - `finality` (nullable until a requirement is created), for example
458
+ `per_payment_onchain` or `aggregated_onchain_settlement`
459
+ - `protocol_fee_minor` (nullable; decimal string for Micro / Nano)
460
+ - `settlement_status` (nullable until a requirement is created), for example
461
+ `pending_payment`, `provisional`, `settled`, or `pending_settlement`
462
+ - `chain_receipt_id` (nullable)
450
463
  - `success_url`
451
464
  - `cancel_url`
452
465
  - `expires_at` (nullable)
453
466
  - `authenticated_at` (nullable; set when the shopper signs into Siglume)
454
- - `paid_at` (nullable; set when the payment confirms)
467
+ - `paid_at` (nullable; set when Hosted Checkout has accepted the wallet
468
+ payment flow. For Micro / Nano, this is not the same as final provider
469
+ settlement; use `pricing_band`, `finality`, `settlement_status`, and the
470
+ statement APIs.)
455
471
  - `cancelled_at` (nullable; set when the shopper cancels)
456
472
  - `created_at` (nullable)
457
473
  - `metadata_jsonb`
@@ -565,6 +581,13 @@ nonce: derive `nonce` from a durable order payment attempt, store the returned
565
581
  hash for the same payment request. Do not retry the same order by minting a new
566
582
  nonce unless you intentionally want a new payment attempt.
567
583
 
584
+ For Siglume Marketplace paid capability execution / MCP tools, `idempotency_key`
585
+ is a separate top-level JSON field on the execution payload or tool arguments.
586
+ Use one stable key per logical paid operation, up to 128 characters, and do not
587
+ reuse it for a different payload. A retry with the same key returns or reconciles
588
+ the first recorded outcome instead of creating another chargeable usage event.
589
+ The HTTP `Idempotency-Key` header is not the public requirement-create contract.
590
+
568
591
  The returned requirement includes both compatibility and machine-readable
569
592
  settlement fields:
570
593
 
@@ -937,11 +960,26 @@ argument is positional in both languages.
937
960
 
938
961
  For `direct_payment.confirmed`, inspect `event.data.pricing_band`,
939
962
  `event.data.settlement_cadence`, `event.data.finality`,
940
- `event.data.protocol_fee_minor`, and `event.data.settlement_status` instead of
941
- inferring whether the event means per-payment on-chain confirmation or an
942
- aggregated Micro / Nano settlement confirmation. `event.data.request_hash_v2`
943
- is present on new challenge-backed requirements; keep accepting
944
- `event.data.request_hash` for historical payloads.
963
+ `event.data.protocol_fee_minor`, `event.data.settlement_status`,
964
+ `event.data.settlement_batch_id`, `event.data.chain_receipt_id`,
965
+ `event.data.usage_event_digest`, and `event.data.settled_at` instead of
966
+ inferring whether the event means per-payment on-chain confirmation, Micro /
967
+ Nano accepted-but-unsettled usage, or an aggregated Micro / Nano settlement
968
+ confirmation. `event.data.request_hash_v2` is present on new challenge-backed
969
+ requirements; keep accepting `event.data.request_hash` for historical payloads.
970
+
971
+ Recommended branch:
972
+
973
+ - `mode === "metered_settlement_batch"`: no order `challenge_hash` is expected.
974
+ Reconcile the batch only when `settlement_status === "settled"`.
975
+ - `pricing_band === "standard"`, `finality === "per_payment_onchain"`, and
976
+ `settlement_status === "settled"`: mark the mapped order paid once.
977
+ - `pricing_band === "micro" || pricing_band === "nano"`: treat the usage as
978
+ accepted but unsettled. Fulfill only if your business accepts delayed
979
+ settlement risk, and reconcile final revenue from statement APIs / settlement
980
+ batches.
981
+ - Missing machine fields: do not mark paid from the event type alone; fetch the
982
+ requirement or route the event to manual review.
945
983
 
946
984
  ### `verifyDirectRequestPaymentWebhook(secret, body, signature_header, options)` / `verify_direct_request_payment_webhook(secret, body, signature_header, *, tolerance_seconds=300, now=None)`
947
985
 
@@ -959,7 +997,8 @@ const { event, verification } = await verifyDirectRequestPaymentWebhook(
959
997
  rawRequestBody, // the RAW body bytes/string, not re-stringified JSON
960
998
  request.headers["siglume-signature"],
961
999
  );
962
- // event.type === "direct_payment.confirmed" -> fulfill once; verification.timestamp is the signed time
1000
+ // event.type === "direct_payment.confirmed"; inspect pricing_band/finality/
1001
+ // settlement_status before marking an order paid.
963
1002
  ```
964
1003
 
965
1004
  ```py
@@ -971,7 +1010,8 @@ verified = verify_direct_request_payment_webhook(
971
1010
  siglume_signature_header,
972
1011
  )
973
1012
  event = verified["event"]
974
- # event["type"] == "direct_payment.confirmed" -> fulfill once
1013
+ # event["type"] == "direct_payment.confirmed"; inspect pricing_band/finality/
1014
+ # settlement_status before marking an order paid.
975
1015
  ```
976
1016
 
977
1017
  ## Exported Constants
@@ -396,16 +396,63 @@ const { event } = await verifyDirectRequestPaymentWebhook(
396
396
  siglumeSignatureHeader,
397
397
  );
398
398
 
399
- if (event.type === "direct_payment.confirmed") {
400
- const data = event.data;
399
+ if (event.type !== "direct_payment.confirmed") {
400
+ return new Response(null, { status: 204 });
401
+ }
402
+
403
+ const data = event.data;
404
+
405
+ if (data.mode === "metered_settlement_batch") {
406
+ // Aggregated Micro/Nano settlement events do not carry an order challenge.
407
+ if (data.settlement_status === "settled") {
408
+ await orders.reconcileMeteredSettlementOnce({
409
+ settlement_batch_id: String(data.settlement_batch_id ?? ""),
410
+ chain_receipt_id: String(data.chain_receipt_id ?? ""),
411
+ usage_event_digest: String(data.usage_event_digest ?? ""),
412
+ settled_at: String(data.settled_at ?? ""),
413
+ });
414
+ }
415
+ return new Response(null, { status: 204 });
416
+ }
417
+
418
+ if (
419
+ data.pricing_band === "standard" &&
420
+ data.finality === "per_payment_onchain" &&
421
+ data.settlement_status === "settled"
422
+ ) {
401
423
  const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
402
424
  if (!order) {
403
- throw new Error("Unknown Siglume challenge hash");
425
+ await orders.flagForPaymentStateReview({
426
+ reason: "unknown_challenge_hash",
427
+ requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
428
+ });
429
+ return new Response(null, { status: 204 });
404
430
  }
405
431
  await orders.markPaidOnce(order.id, {
406
432
  siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
433
+ chain_receipt_id: String(data.chain_receipt_id ?? ""),
407
434
  });
435
+ return new Response(null, { status: 204 });
436
+ }
437
+
438
+ if (data.pricing_band === "micro" || data.pricing_band === "nano") {
439
+ const order = await orders.findByChallengeHash(String(data.challenge_hash ?? ""));
440
+ if (order) {
441
+ await orders.markFulfilledButUnsettledOnce(order.id, {
442
+ siglume_requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
443
+ pricing_band: String(data.pricing_band),
444
+ });
445
+ }
446
+ return new Response(null, { status: 204 });
408
447
  }
448
+
449
+ // Missing or unknown machine fields: do not mark the order paid from the event
450
+ // name alone. Fetch the requirement or route it to manual review.
451
+ await orders.flagForPaymentStateReview({
452
+ reason: "missing_settlement_machine_fields",
453
+ requirement_id: String(data.requirement_id ?? data.direct_payment_requirement_id ?? ""),
454
+ });
455
+ return new Response(null, { status: 204 });
409
456
  ```
410
457
 
411
458
  Python:
@@ -421,23 +468,68 @@ verified = verify_direct_request_payment_webhook(
421
468
  siglume_signature_header,
422
469
  )
423
470
 
424
- if verified["event"]["type"] == "direct_payment.confirmed":
425
- data = verified["event"]["data"]
471
+ if verified["event"]["type"] != "direct_payment.confirmed":
472
+ return "", 204
473
+
474
+ data = verified["event"]["data"]
475
+
476
+ if data.get("mode") == "metered_settlement_batch":
477
+ # Aggregated Micro/Nano settlement events do not carry an order challenge.
478
+ if data.get("settlement_status") == "settled":
479
+ orders.reconcile_metered_settlement_once(
480
+ settlement_batch_id=str(data.get("settlement_batch_id") or ""),
481
+ chain_receipt_id=str(data.get("chain_receipt_id") or ""),
482
+ usage_event_digest=str(data.get("usage_event_digest") or ""),
483
+ settled_at=str(data.get("settled_at") or ""),
484
+ )
485
+ return "", 204
486
+
487
+ if (
488
+ data.get("pricing_band") == "standard"
489
+ and data.get("finality") == "per_payment_onchain"
490
+ and data.get("settlement_status") == "settled"
491
+ ):
426
492
  order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
427
493
  if not order:
428
- raise RuntimeError("Unknown Siglume challenge hash")
494
+ orders.flag_for_payment_state_review(
495
+ reason="unknown_challenge_hash",
496
+ requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
497
+ )
498
+ return "", 204
429
499
  orders.mark_paid_once(
430
500
  order["id"],
431
501
  siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
502
+ chain_receipt_id=str(data.get("chain_receipt_id") or ""),
432
503
  )
504
+ return "", 204
505
+
506
+ if data.get("pricing_band") in ("micro", "nano"):
507
+ order = orders.find_by_challenge_hash(str(data.get("challenge_hash") or ""))
508
+ if order:
509
+ orders.mark_fulfilled_but_unsettled_once(
510
+ order["id"],
511
+ siglume_requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
512
+ pricing_band=str(data.get("pricing_band") or ""),
513
+ )
514
+ return "", 204
515
+
516
+ # Missing or unknown machine fields: do not mark the order paid from the event
517
+ # name alone. Fetch the requirement or route it to manual review.
518
+ orders.flag_for_payment_state_review(
519
+ reason="missing_settlement_machine_fields",
520
+ requirement_id=str(data.get("requirement_id") or data.get("direct_payment_requirement_id") or ""),
521
+ )
522
+ return "", 204
433
523
  ```
434
524
 
435
525
  ## Reconcile Micro / Nano Statements
436
526
 
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:
527
+ Standard Payment can be marked paid from the verified `direct_payment.confirmed`
528
+ webhook only when `pricing_band === "standard"`,
529
+ `finality === "per_payment_onchain"`, and `settlement_status === "settled"`.
530
+ Micro Payment and Nano Payment are different: they are automatic amount bands
531
+ and are settled later in aggregated on-chain batches. Use the statement APIs to
532
+ answer:
441
533
 
442
534
  - how much Micro / Nano usage is open this week or month,
443
535
  - 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
@@ -40,6 +40,11 @@ The current public API chooses the band from `amount_minor`; it does not expose
40
40
  `settlement_mode: "immediate"` or `require_immediate_finality: true`. If a
41
41
  merchant needs immediate on-chain finality, the payment amount must be in the
42
42
  Standard band or the merchant must have a separately agreed platform contract.
43
+ For public one-time Direct Payment / Hosted Checkout, `amount_minor` is a
44
+ positive integer in minor currency units. That means the smallest public
45
+ one-time checkout amount is JPY 1 or USD 0.01. Nano Payment on this public path
46
+ therefore means JPY 1-49 or USD 0.01-0.30. Sub-minor Nano protocol fees are
47
+ settlement-accounting amounts, not externally submitted one-time item prices.
43
48
 
44
49
  For the operational statement APIs, CSV export, buyer past-due blocks, and the
45
50
  field-by-field meaning of `scheduled_debit_at`, `not_before_attempt_at`,
@@ -183,8 +188,11 @@ For low-count Nano batches, the integer ceiling can make the effective buyer
183
188
  burden per usage higher than the headline USD 0.001 / usage protocol fee. The
184
189
  decimal protocol fee remains visible as `protocol_fee_minor`; the difference
185
190
  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`.
191
+ batch. Each settlement batch can add a positive rounding adjustment of less than
192
+ 1 token minor unit. If a buyer uses many providers / payees in one period, that
193
+ adjustment can occur once per settlement batch. JavaScript integrations should
194
+ not sum Micro / Nano minor amounts with `number`; use a decimal library. Python
195
+ integrations should use `Decimal`.
188
196
 
189
197
  ## Statement APIs and Notices
190
198
 
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.6",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",