<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinos-server.git/lib/payments.ts, branch master</title>
<subtitle>coinos server — legacy coinos.io backend (Lightning, on-chain, Liquid)</subtitle>
<id>https://git.coinos.io/coinos-server.git/atom?h=master</id>
<link rel='self' href='https://git.coinos.io/coinos-server.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/'/>
<updated>2026-10-04T20:38:24Z</updated>
<entry>
<title>Evicted accounts can't receive or log in; limit errors say what's happening</title>
<updated>2026-10-04T20:38:24Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-04T20:38:24Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=41018c30a2e03f5ab05be64d419542be0cff7943'/>
<id>urn:sha1:41018c30a2e03f5ab05be64d419542be0cff7943</id>
<content type='text'>
An evicted account no longer gets lightning-address invoices (stops a
scam page's address from minting new ones) or a login token. A send
over the server limit now says withdrawals are temporarily limited
instead of a generic "Problem sending payment" that read as a frozen
account. (Already live since the 2026-10-01 restart.)

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
</content>
</entry>
<entry>
<title>v3 bridge: a migrated name's legacy receives are paid on to its v3 wallet</title>
<updated>2026-10-04T20:05:53Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-04T20:05:53Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=1ff2623dbb43148267eac5cbe68c76d2c0639805'/>
<id>urn:sha1:1ff2623dbb43148267eac5cbe68c76d2c0639805</id>
<content type='text'>
A user who moved their name to coinos v3 keeps their legacy account, and
old BOLT12 offers, bolt11 invoices and bitcoin/liquid deposit addresses
keep paying into it. Each confirmed receive of those kinds is now
forwarded as it lands to the name's v3 lightning address (registrar
invoice on the ASP node; the v3 forwarder delivers to the Ark mailbox
even with the wallet closed). Only new receives move; existing balances
stay. Routing cost only: the platform fee is waived (debit exempt), and
the direct channels to the ASP node quote 0. Proof required: the
registrar holds the name for the legacy account's own key, or for the
wallet its session named in a migration grant (/name granted flag).
Autowithdraw stands down for forwarded receives; failures retry with
backoff (v3fwd:retry, one process at a time) and otherwise stay put.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
</content>
</entry>
<entry>
<title>payments: refuse on-chain sends to blocked addresses before debiting</title>
<updated>2026-09-29T15:37:47Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-29T15:37:39Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=41a5819f5d7c662a5a2884952e49a14464e83c41'/>
<id>urn:sha1:41a5819f5d7c662a5a2884952e49a14464e83c41</id>
<content type='text'>
debit() checks blocked_addresses against the payment hash, which for an
on-chain send is the txid, so a blocked destination address was never
matched. Check each output address of the built transaction in sendOnchain
and fail before any debit.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>logging: throttle log lines that clients can trigger at will</title>
<updated>2026-09-29T15:37:47Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-29T15:37:39Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=4900ecb19ab9c6d279c8771b834fcb29e526a1e0'/>
<id>urn:sha1:4900ecb19ab9c6d279c8771b834fcb29e526a1e0</id>
<content type='text'>
Evicted-JWT replays, blocked sends from frozen or limited accounts and
failed payment retries each logged one line per request, so a looping client
could flood the log, and an evicted token replayed across a proxy pool made
atk-autoban.sh block every IP it saw. Add quiet()/suppressed() in
lib/logging.ts: the first occurrence per key logs as before, repeats within
the window are counted and reported on the next line that gets through. The
evicted-auth summary line is worded so the auto-banner ignores it.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>lightning: quote the routing fee up front and add a max-send quote endpoint</title>
<updated>2026-09-22T02:22:03Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-22T02:21:29Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=5929817e0436979821980abf2f5f4b8a187e8654'/>
<id>urn:sha1:5929817e0436979821980abf2f5f4b8a187e8654</id>
<content type='text'>
Users trying to empty their balance over lightning had to guess a maxfee
that covered both an unknown routing fee and the platform fee, and the
0.5% floor we imposed on that guess made an exact "send everything"
impossible. xpay routes with askrene's getroutes, so ask askrene the same
question before we debit and the fee is known exactly.

lib/lnquote.ts mirrors what xpay does in plugins/xpay/xpay.c (v26.06):
a throwaway private layer carrying the invoice's route hints (bolt11) or
blinded paths via xpay's fake destination node (bolt12), then getroutes
over the same layer stack in the same order. The fee is the sum of what
leaves our node minus what the destination receives, rounded up to a
whole sat since that is what xpay's maxfee reserves.

POST /lightning/quote returns the routing fee, the platform fee exactly
as debit() charges it (free-tier credit, ln:nofree rate, none on
sub-accounts) and the total. With `max` it iterates quote / subtract
fees / requote so amount + both fees lands exactly on the balance; an
offer gets a probe invoice then the real one for the solved amount, and
an LNURL caller can pass `ceiling` and a probe invoice the same way.

sendLightning now quotes for every send. With no explicit fee the quote
becomes the maxfee; an explicit fee below the quote is rejected before
any debit instead of failing inside xpay after one; the old 0.5% floor
on explicit fees is gone. If the quote itself fails we fall back to the
heuristic and let xpay try.

finalize rounds the settled fee up to the whole sat that was quoted and
reserved; rounding a 301 msat fee to 0 refunded a sat that the quote had
already accounted for and stranded it in the account.

The prefer-kappa layer is only passed to xpay when it exists on the node;
getroutes rejects unknown layers outright, which broke every regtest send.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>security: drop the admin endpoint surface and land pending hardening</title>
<updated>2026-09-14T03:44:48Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-14T03:44:48Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=d8a361bdcfaa8cc5b39d9675e74b0e1d2e42be2c'/>
<id>urn:sha1:d8a361bdcfaa8cc5b39d9675e74b0e1d2e42be2c</id>
<content type='text'>
Removes the admin-gated endpoints and the account flags they set, plus a
batch of hardening that had accumulated uncommitted in the tree. These were
deployed and verified together, so they land as one change.

admin surface:
  - drop POST /hidepay, POST /unlimit and GET /users, their handlers, and
    the now-unused admin preValidation export
  - `unlimited` was only ever written, never read; `hidepay` only appeared
    in output projections. Both flags go, along with their whitelist,
    public-fields and type entries.

request log:
  - redactBody now recurses. It only masked top-level keys, so a body that
    nests credentials one level down wrote them to the log in plaintext,
    and pin fields were not on the list at all.

payments:
  - sendOnchain/build gate `aid` through requireAccount, so a caller can
    only spend from an account it owns
  - sendOnchain signs only a tx we composed, matched against a short-lived
    per-account build receipt
  - raise xpay retry_for to 60s; route-finding preferred a drained zero-fee
    hop and burned the whole budget on it. check()'s reversal window keeps
    its 2x margin.

ecash / invoices / webhooks:
  - resolve a token's mint through the IP-pinning fetcher instead of plain
    fetch, closing an unauthenticated SSRF path into the internal network
  - reject non-positive amounts before the balance write
  - /sign requires the caller to own the address, rather than signing under
    any address the hot wallet controls
  - webhook delivery no longer disables TLS verification; the body carries
    the merchant's shared secret

registration:
  - refuse a username the v3 registrar holds unless the caller's own nostr
    key is the holder (lib/names.ts)

liquid:
  - honour the deposit kill switch at invoice generation and at credit

elements image:
  - pin 23.3.4 and verify the release tarball checksum

Co-Authored-By: Claude Opus 5 (1M context) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_0112TGi9GGNWqji1XQ5jZRzc
</content>
</entry>
<entry>
<title>credit: skip settlements for a deleted account instead of throwing</title>
<updated>2026-09-05T07:35:17Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-05T07:35:17Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=917633aeeab8518af9e52a16532d8485dbd62e2a'/>
<id>urn:sha1:917633aeeab8518af9e52a16532d8485dbd62e2a</id>
<content type='text'>
getUser(inv.uid) returns nothing when the account was deleted but its
invoice or bolt12 offer still receives (OCEAN payouts to a deleted
user's offer). The destructure on the next line threw inside the
lightning listener, which never advanced pay_index and crash-looped the
app every 50s, blocking all incoming lightning for two hours. Record the
preimage in `missing` and return, as an unknown invoice already does.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
</content>
</entry>
<entry>
<title>payments: finalize settled keysends in sendKeysend</title>
<updated>2026-09-05T02:04:11Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-05T02:04:08Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=abf0226dd0b2e7d50c378d266ae608e651c486de'/>
<id>urn:sha1:abf0226dd0b2e7d50c378d266ae608e651c486de</id>
<content type='text'>
keysend blocks until it settles, so a successful return already carries
the preimage. Previously only the caller was expected to confirm the
record, and the NWC pay_keysend path never did, leaving successful
keysends stuck pending forever (check() can't rescue them since the
label isn't a bolt11). Add finalizeKeysend(): flip pending-&gt;confirmed,
derive the real fee from amount_sent_msat, refund the unused fee
reserve, and refuse to re-create a record a concurrent reverse() already
removed.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014uxZLvYS4gH9UqUX2Ey8TV
</content>
</entry>
<entry>
<title>nwc: enforce per-connection spend budgets + flag-gated incident containment</title>
<updated>2026-08-22T15:08:04Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-22T15:08:04Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f'/>
<id>urn:sha1:0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f</id>
<content type='text'>
Thread the remaining allowance through every spend path (pay_invoice, pay,
pay_keysend, internal transfer) as maxTotal, and have debit() assert the spend
stays within it after it reloads the invoice and captures the tip — so a
delegated NWC credential can no longer exceed its configured budget, and a
mutated tip can't slip a small-budget check and drain via the internal path.
checkBudget now returns the remaining allowance alongside any error; a blank
budget is treated as deliberately unlimited and a positive budget requires a
known renewal period.

Add operator-controlled containment for a suspected credential disclosure, all
gated behind Redis flags with no hardcoded scope:
  nwc:disabled              fail every method closed
  nwc:credential-cutoff     retire connections created before an epoch
  nwc:quarantined           block an individual disclosed connection
  nwc:guard:dormant-keysend  quarantine the first keysend on a long-idle app

Fix an ownership check that read the bare pubkey instead of app:&lt;pubkey&gt;, which
let an authenticated user overwrite another connection's config, and validate
budget/fee/renewal inputs.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
</content>
</entry>
<entry>
<title>payments: prevent same-invoice retry from orphaning an unreconciled debit</title>
<updated>2026-08-09T05:36:52Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-09T05:36:52Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=14948460559788db024ff99406dd18adaed206ba'/>
<id>urn:sha1:14948460559788db024ff99406dd18adaed206ba</id>
<content type='text'>
The pending reconciliation set is keyed by bolt11, shared across attempts
at the same invoice. A retry made after a prior attempt's CLN parts
resolved (but before check() reconciled its debit) would pass the
listpays-only guard, and the retry's own reverse()/finalize() then
removed the shared entry — permanently hiding the first attempt's debit
from check(), so a failed send was never auto-refunded.

- sendLightning: refuse to pay while the invoice's bolt11 is still in
  the pending set; the caller is told to retry shortly.
- check(): an entry whose payment record no longer exists in db or
  archive is dropped from the set instead of skipped, so a dead entry
  can't block its invoice forever under the new guard.

Co-Authored-By: Claude Fable 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
