diff --git a/examples/go/facilitator/advanced/README.md b/examples/go/facilitator/advanced/README.md index 76ada96923..332b3d74bd 100644 --- a/examples/go/facilitator/advanced/README.md +++ b/examples/go/facilitator/advanced/README.md @@ -296,6 +296,12 @@ if existing, found := store.Get(paymentID); found && existing.Status == "settled } ``` +For production use, bind the payment ID to a normalized fingerprint of the +settlement request, such as network, scheme, asset, amount, payer, payee, and +the application operation ID when available. If the same payment ID is replayed +with a different fingerprint, reject it with a conflict instead of returning a +cached transaction or settling a different payment. + **Use case:** Prevent duplicate settlements, provide exactly-once semantics, track payment lifecycle. ### Lifecycle Hooks diff --git a/examples/go/servers/payment-identifier/README.md b/examples/go/servers/payment-identifier/README.md index 490ac8d7d2..a62db7ea49 100644 --- a/examples/go/servers/payment-identifier/README.md +++ b/examples/go/servers/payment-identifier/README.md @@ -83,6 +83,13 @@ if existingOrder, found := processedPayments[paymentID]; found { } ``` +In production, store the payment ID together with a normalized fingerprint of +the paid operation, for example the HTTP method, route, selected payment +requirements, and application order ID. A retry with the same payment ID and the +same fingerprint can return the cached response. A request with the same payment +ID but a different fingerprint should return `409 Conflict` instead of creating +or charging for a different order. + ## API ### POST /order @@ -114,6 +121,9 @@ Creates an order with payment. Requires a payment identifier. - Replace the in-memory `processedPayments` map with Redis or a database - Set appropriate TTL for payment ID records - Consider distributed locking for high-concurrency scenarios +- Scope idempotency records by tenant, merchant, or route if the same storage + layer is shared across paid resources +- Bind each payment ID to a request fingerprint and reject conflicting replays ## Related Examples diff --git a/specs/extensions/payment_identifier.md b/specs/extensions/payment_identifier.md index 592ea0465a..e85f0d5310 100644 --- a/specs/extensions/payment_identifier.md +++ b/specs/extensions/payment_identifier.md @@ -86,6 +86,30 @@ Client echoes the extension and appends an `id`: | Same `id`, different payload | Return 409 Conflict | | `required: true`, no `id` provided | Return 400 Bad Request | +### Request Binding + +Resource servers and facilitators should bind each `id` to a normalized request +fingerprint before returning a cached result. The fingerprint should cover the +parts of the request that make the paid operation unique, such as: + +- `scheme` +- `network` +- `asset` +- `amount` +- `payTo` +- resource path and method +- application-level operation or order identifier + +Implementations should store the first observed fingerprint with the `id`. +Later requests with the same `id` and the same fingerprint can return the +cached response. Later requests with the same `id` and a different fingerprint +should fail with `409 Conflict` instead of reusing the cached response or +executing a second operation. + +Servers should avoid using `id` alone as the storage key for authorization +decisions when the same backend handles multiple paid resources. Scope the key +by tenant, merchant, route, or facilitator account when those boundaries exist. + --- ## Responsibilities