From 923825abc997ba0ba2fecb866f8fa8a7f8282049 Mon Sep 17 00:00:00 2001 From: shum Date: Thu, 22 Oct 2026 10:13:13 +0000 Subject: [PATCH] plans: names late credit in atomic units --- plans/names-codes/2026-10-21-store-api.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plans/names-codes/2026-10-21-store-api.md b/plans/names-codes/2026-10-21-store-api.md index 1020f3d150..df7f8d6fd0 100644 --- a/plans/names-codes/2026-10-21-store-api.md +++ b/plans/names-codes/2026-10-21-store-api.md @@ -51,7 +51,7 @@ The service serializes the requests that create or change an order: The database backs this with unique constraints: one live registration per commitment, one live registration per owner and label hash, one registration per reserved code, one holder per balance. A registration is live while it can still lead to a registration of its name: `unpaid` with an open invoice, `queued`, `committed`, revealing or registering, and `failed` (retry is allowed). A `registered` order is not live, so the same wallet can order the name again after it lapses; a `taken` order is not live. `live` is a stored flag, and every transition that changes it does so in the same transaction. -In one transaction, rows are locked in this order: the invoice, then the registration, then a prepaid code or a balance; when one transaction releases one balance and credits another (a difference order's invoice closing with money in it), the released balance is locked first. Rows a transaction inserts are new and wait on nobody, so `/spend` may insert its invoice and order after closing a holder (which releases that holder's balance row) and before writing the balance again. +In one transaction, rows are locked in this order: the invoice, then the registration, then a prepaid code or a balance. A late credit is its own transaction: the invoice, then the order's balance. Rows a transaction inserts are new and wait on nobody, so `/spend` may insert its invoice and order after closing a holder (which releases that holder's balance row) and before writing the balance again. **An overdue order is closed by the next order.** When a new order from the same wallet and name meets a live order whose open invoice has passed its expiry time with no money received (the poller may not have marked it yet; a funded invoice is never closed this way, as the sweep never expires one), the new order closes it inside its own transaction, under the label hash's turn: the old invoice becomes `expired`, its registration loses `live`, and only then is the new order inserted. The provider's cancel of the old invoice is sent afterwards, best effort, as the poller would. Settlement queues a registration only while it is still live; a payment that arrives for a closed order is a late payment (it stays `unpaid`, and its confirmed money is credited to that order's balance, section 8; owner, 2026-10-22), never a second live order. @@ -406,7 +406,7 @@ The badge invoice's fields, with `invoiceId`, `kind: "name"` and the registratio The long poll for a paid order. - Without `wait`, it answers at once. - With `wait`, it holds while the status is `wait`, the payment marks are `seenPaid` and `seenFull` (each compared only when sent), and the registration's `version` is `seenVersion` (when sent). It answers on any change, or unchanged after at most 30 seconds (the mock: 20). -- An invoice that reads `paid` while its registration is `unpaid` (paid after it closed) is answered at once and never held: there is nothing more to wait for. Its confirmed money is credited to the order's balance (section 8), which its view shows. +- An invoice that reads `paid` while its registration is `unpaid` (paid after it closed) is answered at once and never held: there is nothing more to wait for. Its confirmed money is credited to the order's balance (section 8) by the poller, which can come a pass later than the paid status, and leaves `version` unchanged: the page reads the view again until the balance shows it. - An open invoice past its expiry with no money received reads `expired`; a funded one stays `open` until it settles. ### `GET /api/registration/:id?seenVersion=` @@ -492,11 +492,12 @@ A paid order whose name someone registered first keeps what it is worth as a bal - **What it is:** US cents, held per order and owned by that order's owner code. Balances are never merged, across orders or wallets: one order's balance pays for one new order at a time. A balance never expires. - **Credited:** with the order's `price`, in the write that makes a paid order `taken` (paid by invoice, from a balance, or both), so a `taken` order with a payment always has a balance. A move to a cheaper name adds the difference (`/name`, above). A code order gets none: codes carry no money, and its code is released (section 4). A late payment (section 1) adds its money to the closed order's own balance (owner, 2026-10-22). It covers money that arrives on any name invoice after it closed (a difference invoice included), and money received while the invoice was open that it still holds when the provider closes it or a cancel meets it. - - *Only confirmed money is credited.* Stripe: the succeeded amount. BTCPay: when the invoice reads `Settled`, or every payment of its payment method reads `Settled`, all of `paymentMethodPaid`; otherwise the sum of the `Settled` payments' `value`, capped at `paymentMethodPaid`, so the credited crypto never exceeds the received and reaches it exactly once everything settles. BTCPay settles a payment at the store's speed policy (`[btcpay] speed_policy`); a late credit needs at least one confirmation whatever the policy, so with `HighSpeed` (which settles at zero confirmations) late crypto is never credited automatically and goes to the alert below. - - *At the lower rate* (owner, 2026-10-22): new confirmed crypto is credited at the lower of the invoice's rate and the provider's current rate, read when crediting (BTCPay's store rates, through the configured provider), and rounded to cents once. The rates and the cents are stored with each credit. A failed rate read credits nothing yet: the credit stays pending and is tried again, never at a guessed rate. - - *Once:* each credit adds only the confirmed crypto (or, for Stripe, cents) not yet credited from that invoice, under the invoice's lock, so a repeated, growing or reorganised report never credits twice, and an unconfirmed payment that is replaced or dropped never credits. - - *When:* right after the settlement or cancel that recorded the payment commits, and again by the poller for any invoice whose confirmed money is not all credited, so a crash or a failed rate read between the two loses nothing. The rate read never runs under the invoice's lock. - - *Pending and the alert:* money received but not confirmed (a mempool payment, Monero confirming with a zero figure) is pending and logged at info. The operator is alerted once per invoice, when money on a closed invoice is still not all credited and its payment was first seen longer ago than the poller's read window (`settleWindow`, about 72 hours): support then refunds by hand only a transaction confirmed on chain that BTCPay no longer monitors; one BTCPay still shows `Processing` waits. + - *Only confirmed money is credited.* Stripe: the succeeded amount. BTCPay: when the invoice reads `Settled`, or every payment of its payment method reads `Settled`, all of `paymentMethodPaid`; otherwise the sum of the `Settled` payments' `value`, capped at `paymentMethodPaid`, so the credited crypto never exceeds the received and reaches it exactly once everything settles. Crypto is counted in integer atomic units (satoshi for BTC, piconero for XMR); an amount with more decimals than its unit fails the read. BTCPay settles a payment at the invoice's own speed policy (`checkout.speedPolicy`, fixed when the invoice was made); a late credit needs at least one confirmation, so money on an invoice made under `HighSpeed` (which settles at zero confirmations) is never credited automatically, whatever the configuration says later, and goes to the alert below. + - *At the lower rate* (owner, 2026-10-22): new confirmed crypto is credited at the lower of the invoice's rate and the provider's current rate (BTCPay's store rates, through the configured provider), and rounded down to the cent, so a credit never exceeds its value at either rate. The rates and the cents are stored with each credit. A failed rate read credits nothing yet, and the credit is tried again, never at a guessed rate: a failed read is an error, a missing pair, a rate with errors, a rate at or below 0, or a rate below half the invoice's (`minCurrentRateFraction`, an owner-visible constant), which is taken as a glitch and logged as an error. + - *Once:* each credit adds only the confirmed crypto (or, for Stripe, cents) not yet credited from that invoice, under the invoice's lock, with each credit numbered per invoice and unique, so a repeated, growing or reorganised report, or a second instance, never credits twice, and an unconfirmed payment that is replaced or dropped never credits. + - *Which invoices:* a name invoice whose order is `unpaid` and not live, whatever the invoice's own status (a late `Settled` invoice reads `paid` while its order stays `unpaid`). + - *When:* after the settlement that recorded the payment commits, by the poller, and again by the poller's pass (at its idle cadence) for any such invoice whose confirmed money is not all credited, so a crash or a failed rate read loses nothing. A cancel by the buyer reads no rate: its late money is credited by the poller. The rate read never runs under the invoice's lock, happens only when crypto is due, and is made once per currency pair per pass. + - *Pending and the alert:* money received but not confirmed (a mempool payment, Monero confirming with a zero figure) is pending and logged at info. The operator is alerted once per invoice, and again only when more money arrives, when money on a closed invoice is still not all credited and its payment was first seen longer ago than the poller's read window (`settleWindow`, about 72 hours). The alert names the crypto received, confirmed and credited (cents for Stripe), and its reason: unconfirmed money ("refund by hand only a transaction confirmed on chain that BTCPay no longer monitors; one still `Processing` there waits"), or confirmed money waiting on the rate read ("do not refund; fix the rate read"). Only one instance logs it, by a conditional write of the alert's mark. - **Spent:** only by `/spend` (below), signed by the owner, for a new order of the same owner. When the balance covers the new name's price, the price is spent at once and the rest stays as balance. When it does not, all of it pays, and an invoice is made for the difference. - **Held:** while that invoice is open, the new order holds the balance: no other spend and no refund takes it. Settlement queues the new order and spends what it held, in one transaction. Whatever closes the invoice unpaid (a cancel, its expiry, the poller's sweep, a next order closing it as overdue, section 1) releases the hold in the transaction that closes the order. A payment that arrives after that is a late payment (section 1): it is credited to the closed order's own balance, and the released balance is not touched. A hold by an order whose invoice is past its expiry time with no money received reads as free (`held` false), and the next `/spend` of that balance closes that order first, in its own transaction, as section 1 closes an overdue order (below). A refund does not: it waits for the poller's sweep to close the holder. - **Refunded:** only by support, when the buyer writes with the order's reference (its invoice id, or its registration id for an order without an invoice). The operator command `//refund ` (task plan, T26) sets the balance to 0 and adds the cents to `refunded`, which counts every refund of the balance (one can be credited again later, by a late payment or a move); support then pays them back by hand, by the methods that paid them, which the command lists back through every balance the money came from. A held balance is never refunded, overdue holder or not: the command names the holder and its invoice's expiry, and support runs it again once the poller has closed the holder. There is no refund endpoint, no automatic refund, and no refund address given to the service.