@siglume/direct-request-payment 0.4.4 → 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
 
@@ -36,7 +38,7 @@ and no new webhook.
36
38
  | --- | --- | --- |
37
39
  | `SIGLUME_DIRECT_PAYMENT_CHALLENGE_SECRET` | merchant server | HMAC secret for order challenges |
38
40
  | `SIGLUME_MERCHANT_AUTH_TOKEN` | merchant setup helper | merchant Siglume bearer token for self-service setup |
39
- | `SIGLUME_AUTH_TOKEN` | buyer payment helper | buyer Siglume bearer token for API calls |
41
+ | `SIGLUME_AUTH_TOKEN` | user-authenticated helper | buyer Siglume bearer token for payment / buyer statements, or provider Siglume bearer token for provider statements |
40
42
  | `SIGLUME_API_BASE` | optional | API base URL override; defaults to `https://siglume.com/v1` |
41
43
  | `SIGLUME_WEBHOOK_SECRET` | merchant server | webhook signing secret returned as `whsec_...` |
42
44
 
@@ -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`
@@ -505,8 +521,10 @@ or rotation.
505
521
  ## `DirectRequestPaymentClient`
506
522
 
507
523
  Thin wrapper around the current Siglume Direct Request Payment HTTP contract.
508
- Use it with the authenticated buyer's Siglume bearer token. Developer Portal
509
- `cli_` API keys are not accepted by these buyer-authenticated routes.
524
+ Use it with the authenticated Siglume user bearer token for the route you call:
525
+ buyer tokens for buyer payment and buyer statement routes, provider / merchant
526
+ tokens for provider statement routes. Developer Portal `cli_` API keys are not
527
+ accepted by these user-authenticated routes.
510
528
 
511
529
  This client creates SDRP Standard Payment requirements for external merchant
512
530
  checkout flows. Micro Payment and Nano Payment are applied automatically by
@@ -522,14 +540,14 @@ statement API amount fields (`protocol_fee_minor`, `gross_buyer_debit_minor`,
522
540
 
523
541
  ```ts
524
542
  const siglume = new DirectRequestPaymentClient({
525
- auth_token: buyerSiglumeBearerToken,
543
+ auth_token: buyerOrProviderSiglumeBearerToken,
526
544
  base_url: "https://siglume.com/v1",
527
545
  });
528
546
  ```
529
547
 
530
548
  ```py
531
549
  siglume = DirectRequestPaymentClient(
532
- auth_token=buyer_siglume_bearer_token,
550
+ auth_token=buyer_or_provider_siglume_bearer_token,
533
551
  base_url="https://siglume.com/v1",
534
552
  )
535
553
  ```
@@ -556,6 +574,20 @@ Input:
556
574
  - `allowance_amount_minor`: optional positive integer
557
575
  - `metadata`: optional JSON object
558
576
 
577
+ There is no public `idempotency_key` field on this requirement-create request.
578
+ For one-time external checkout, idempotency is the merchant-signed challenge
579
+ nonce: derive `nonce` from a durable order payment attempt, store the returned
580
+ `challenge_hash`, and use `request_hash_v2` when you need a canonical machine
581
+ hash for the same payment request. Do not retry the same order by minting a new
582
+ nonce unless you intentionally want a new payment attempt.
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
+
559
591
  The returned requirement includes both compatibility and machine-readable
560
592
  settlement fields:
561
593
 
@@ -723,8 +755,9 @@ SDK methods:
723
755
  - TypeScript: `listBuyerUsageEvents(...)`, `listBuyerSettlementBatches(...)`
724
756
  - Python: `list_buyer_usage_events(...)`, `list_buyer_settlement_batches(...)`
725
757
 
726
- Each accepts `plan_type`, `token_symbol`, `status`, and `limit`, and returns
727
- `{items, next_cursor}`.
758
+ Each accepts `plan_type`, `token_symbol`, `status`, `limit`, and `cursor`, and
759
+ returns `{items, next_cursor}`. When `next_cursor` is non-null, pass it back as
760
+ `cursor` to fetch the next page.
728
761
 
729
762
  Buyer-facing amount names are centered on the debit:
730
763
 
@@ -783,9 +816,10 @@ SDK methods:
783
816
  `list_provider_settlement_batches(...)`, `get_provider_settlement_batch(...)`
784
817
 
785
818
  Provider list methods accept `plan_type`, `token_symbol`, `status`,
786
- `listing_id`, `capability_key`, and `limit`. Detail accepts
819
+ `listing_id`, `capability_key`, `limit`, and `cursor`. Detail accepts
787
820
  `settlement_batch_id`, plus optional `listing_id` and `capability_key`. List
788
- methods return `{items, next_cursor}`.
821
+ methods return `{items, next_cursor}`. When `next_cursor` is non-null, pass it
822
+ back as `cursor` to fetch the next page.
789
823
 
790
824
  Provider-facing amount names:
791
825
 
@@ -843,6 +877,13 @@ metered_usage_id,created_at,plan_type,settlement_cadence,period_start,period_end
843
877
  ```
844
878
 
845
879
  The CSV uses `buyer_period_ref`, not raw buyer account identifiers.
880
+ The CSV keeps the `rounding_delta_minor` column for schema stability, but usage
881
+ rows report `0`; the authoritative rounding adjustment is the settlement batch
882
+ field `rounding_delta_minor`.
883
+
884
+ Micro / Nano amount fields are decimal minor-unit strings. In JavaScript, do
885
+ not aggregate them with `number`; parse them with a decimal library. In Python,
886
+ use `Decimal` for accounting and reconciliation.
846
887
 
847
888
  ## Payload Builders
848
889
 
@@ -919,11 +960,26 @@ argument is positional in both languages.
919
960
 
920
961
  For `direct_payment.confirmed`, inspect `event.data.pricing_band`,
921
962
  `event.data.settlement_cadence`, `event.data.finality`,
922
- `event.data.protocol_fee_minor`, and `event.data.settlement_status` instead of
923
- inferring whether the event means per-payment on-chain confirmation or an
924
- aggregated Micro / Nano settlement confirmation. `event.data.request_hash_v2`
925
- is present on new challenge-backed requirements; keep accepting
926
- `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.
927
983
 
928
984
  ### `verifyDirectRequestPaymentWebhook(secret, body, signature_header, options)` / `verify_direct_request_payment_webhook(secret, body, signature_header, *, tolerance_seconds=300, now=None)`
929
985
 
@@ -941,7 +997,8 @@ const { event, verification } = await verifyDirectRequestPaymentWebhook(
941
997
  rawRequestBody, // the RAW body bytes/string, not re-stringified JSON
942
998
  request.headers["siglume-signature"],
943
999
  );
944
- // 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.
945
1002
  ```
946
1003
 
947
1004
  ```py
@@ -953,7 +1010,8 @@ verified = verify_direct_request_payment_webhook(
953
1010
  siglume_signature_header,
954
1011
  )
955
1012
  event = verified["event"]
956
- # 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.
957
1015
  ```
958
1016
 
959
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,
@@ -106,12 +106,24 @@ buyer_debit_minor = ceil(gross_buyer_debit_minor)
106
106
  rounding_delta_minor = buyer_debit_minor - gross_buyer_debit_minor
107
107
  ```
108
108
 
109
+ For low-count Nano batches, the ceiling can make the effective buyer burden per
110
+ usage higher than the headline "USD 0.001 / usage" protocol fee. The protocol
111
+ fee remains the decimal statement amount; the extra integer-minor-unit
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.
116
+
109
117
  `rounding_delta_minor` belongs to the buyer debit and Siglume's rounding
110
118
  adjustment accounting for that batch. It is not provider revenue. Provider
111
119
  reports should use `provider_receivable_minor`,
112
120
  `settled_provider_receivable_minor`, `unsettled_provider_receivable_minor`, and
113
121
  `past_due_provider_receivable_minor`.
114
122
 
123
+ Micro / Nano amount fields are decimal minor-unit strings. JavaScript
124
+ integrations should aggregate them with a decimal library, not `number`. Python
125
+ integrations should use `Decimal` for accounting.
126
+
115
127
  ### Usage Events
116
128
 
117
129
  ```text
@@ -123,6 +135,7 @@ Query parameters:
123
135
  - `plan_type`: `micro` or `nano`
124
136
  - `token_symbol`: `JPYC` or `USDC`
125
137
  - `status`: for example `pending_settlement`, `settled`, `failed_chargeable`
138
+ - `cursor`: pass the previous response's `next_cursor` to fetch the next page
126
139
  - `limit`: 1 to 500
127
140
 
128
141
  SDK methods: `listBuyerUsageEvents(...)` / `list_buyer_usage_events(...)`.
@@ -148,6 +161,7 @@ Query parameters:
148
161
  - `token_symbol`: `JPYC` or `USDC`
149
162
  - `status`: `notice_pending`, `ready`, `submitted`, `settled`, `failed`,
150
163
  `past_due`, or `notice_delivery_failed`
164
+ - `cursor`: pass the previous response's `next_cursor` to fetch the next page
151
165
  - `limit`: 1 to 200
152
166
 
153
167
  SDK methods: `listBuyerSettlementBatches(...)` /
@@ -233,6 +247,7 @@ Query parameters:
233
247
  - `status`
234
248
  - `listing_id`
235
249
  - `capability_key`
250
+ - `cursor`: pass the previous response's `next_cursor` to fetch the next page
236
251
  - `limit`: 1 to 500
237
252
 
238
253
  SDK methods: `listProviderUsageEvents(...)` /
@@ -266,6 +281,7 @@ Query parameters for the list endpoint:
266
281
  - `status`
267
282
  - `listing_id`
268
283
  - `capability_key`
284
+ - `cursor`: pass the previous response's `next_cursor` to fetch the next page
269
285
  - `limit`: 1 to 200
270
286
 
271
287
  The detail endpoint also accepts `listing_id` and `capability_key`.
@@ -317,6 +333,10 @@ metered_usage_id,created_at,plan_type,settlement_cadence,period_start,period_end
317
333
  ```
318
334
 
319
335
  The CSV uses `buyer_period_ref`, not `buyer_user_id`.
336
+ `rounding_delta_minor` is present for a stable usage-event schema, but per-row
337
+ values are `0`. The authoritative rounding adjustment is the settlement batch
338
+ `rounding_delta_minor`; do not allocate that adjustment to provider revenue
339
+ from individual CSV rows.
320
340
 
321
341
  ## Notifications
322
342
 
@@ -360,9 +380,20 @@ or support staff. Do not depend on raw platform failure messages.
360
380
 
361
381
  ## Usage Accounting by Result
362
382
 
363
- Use idempotency keys for every paid operation. Siglume records one chargeable
364
- usage event per idempotency key within the same buyer / listing / operation
365
- 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.
366
397
 
367
398
  | Case | Provider API executed? | Usage counted? | Integration rule |
368
399
  | --- | --- | --- | --- |
@@ -392,6 +423,11 @@ using stable idempotency keys and provider-side completion records.
392
423
  | `past_due` | Buyer has an unresolved settlement block; provider sees past-due revenue | New Micro / Nano usage for the same buyer / plan / token is paused | Operator requeue or manual resolution only | Do not promise collection or provider payment. Ask the buyer to repair balance / allowance / BudgetVault / caps and reference `support_reference`. |
393
424
  | `failed_chargeable` | Usage is still chargeable because provider work was accepted or completed | Included in later settlement attempts | Review if the provider disputes completion | Keep fulfillment idempotent and preserve evidence keyed by idempotency key. |
394
425
 
426
+ Future platform versions may add explicit terminal states such as
427
+ `closed_unpaid`, `uncollectible`, or `written_off`. Treat unknown terminal
428
+ settlement states as not settled unless `status === "settled"` and
429
+ `chain_receipt_id` is present.
430
+
395
431
  ## Operational Recipes
396
432
 
397
433
  ### "How much did we use this week or month?"
@@ -424,7 +460,9 @@ separate from settled revenue.
424
460
  ## Go-Live Checklist
425
461
 
426
462
  - Your order fulfillment is idempotent by order id and requirement id.
427
- - 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.
428
466
  - Micro / Nano accounting uses statement APIs or CSV, not only webhooks.
429
467
  - Your dashboard separates settled, unsettled, and past-due provider amounts.
430
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`,
@@ -179,6 +184,16 @@ revenue. Providers should reconcile their revenue with
179
184
  `unsettled_provider_receivable_minor`, and
180
185
  `past_due_provider_receivable_minor`, not with `buyer_debit_minor`.
181
186
 
187
+ For low-count Nano batches, the integer ceiling can make the effective buyer
188
+ burden per usage higher than the headline USD 0.001 / usage protocol fee. The
189
+ decimal protocol fee remains visible as `protocol_fee_minor`; the difference
190
+ created by integer-token settlement is visible as `rounding_delta_minor` on the
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`.
196
+
182
197
  ## Statement APIs and Notices
183
198
 
184
199
  Micro and Nano require operational reconciliation after usage is accepted. The
package/docs/security.md CHANGED
@@ -119,6 +119,19 @@ Fulfill exactly once per order. Store at least:
119
119
  Duplicate webhook deliveries and manual redelivery can occur. A duplicate
120
120
  webhook with the same requirement id must not ship the order twice.
121
121
 
122
+ The public requirement-create API does not accept an `idempotency_key` field.
123
+ For one-time external checkout, the durable idempotency material is the
124
+ merchant-authored challenge nonce plus the returned `challenge_hash` /
125
+ `request_hash_v2`. Reuse the same order-attempt nonce when reconciling a retry;
126
+ mint a new nonce only for a new payment attempt.
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
+
122
135
  ## Micro / Nano Statement Privacy
123
136
 
124
137
  Micro Payment and Nano Payment introduce operational statement APIs and CSV
@@ -154,16 +167,15 @@ Direct Request Payment is not:
154
167
  - a platform balance
155
168
  - a card payment fallback
156
169
 
157
- Each payment is an individual wallet payment backed by an on-chain receipt. Small
158
- payments in the Micro and Nano amount bands are aggregated and settled on
159
- account-assigned weekly / monthly slots instead of one transaction at a time
160
- (see the [pricing guide](./pricing.md#settlement-schedule)), but they are still
161
- wallet payments, not a stored balance. Before a small payment is fulfilled,
162
- Siglume checks the buyer's wallet budget and fails closed when it is invalid, so
163
- a rejected request is never charged. Provider revenue for Micro and Nano remains
164
- unsettled until the aggregated on-chain settlement succeeds; Siglume does not
165
- advance or guarantee revenue when a buyer's balance, allowance, BudgetVault
166
- authorization, cap, or on-chain transaction fails.
170
+ Standard Payment is settled individually with its own on-chain receipt. Micro
171
+ and Nano usage events are included in an aggregated settlement batch, and the
172
+ batch is backed by an on-chain receipt. They are still wallet payments, not a
173
+ stored balance. Before a small payment is fulfilled, Siglume checks the buyer's
174
+ wallet budget and fails closed when it is invalid, so a rejected request is
175
+ never charged. Provider revenue for Micro and Nano remains unsettled until the
176
+ aggregated on-chain settlement succeeds; Siglume does not advance or guarantee
177
+ revenue when a buyer's balance, allowance, BudgetVault authorization, cap, or
178
+ on-chain transaction fails.
167
179
 
168
180
  A Micro / Nano budget reservation is not a token lock, escrow, or payment
169
181
  guarantee. It reserves room against Siglume spending limits only. A later
@@ -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.4",
3
+ "version": "0.4.6",
4
4
  "description": "SDK for the Siglume Direct Request Payment SDRP payment protocol",
5
5
  "keywords": [
6
6
  "siglume",