<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinos-server.git/routes/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-07T04:12:19Z</updated>
<entry>
<title>payments: lowercase payreq before looking up our own invoices</title>
<updated>2026-10-07T04:12:19Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-07T04:12:19Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=f53e70ed98eb723479b4276c5b621b599fe570a0'/>
<id>urn:sha1:f53e70ed98eb723479b4276c5b621b599fe570a0</id>
<content type='text'>
An uppercase bolt11 for a coinos invoice missed the getInvoice lookup, so
it was paid over lightning to our own node instead of internally. The
listener then saw the sender's payment pointer for that bolt11 and skipped
crediting the recipient. Normalize in POST /payments and nwc pay_invoice.

Co-Authored-By: Claude Opus 5.5 &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>parse: cache node aliases as a small map with a working timestamp</title>
<updated>2026-09-22T02:44:24Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-22T02:44:24Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=6186159954646b92f1efdbb5c8f0ffa26e29e0ab'/>
<id>urn:sha1:6186159954646b92f1efdbb5c8f0ffa26e29e0ab</id>
<content type='text'>
The alias cache stored the full listnodes output as an array and hung its
`last` stamp on that array, which JSON.stringify drops, and the refresh
condition was inverted besides. Net effect: every lightning send page view
refetched all ~16k nodes and rewrote ~10MB to the db. Keep only the
id→alias map with a real timestamp and refresh it hourly.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>lightning: log quote outcomes</title>
<updated>2026-09-22T02:41:33Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-22T02:41:31Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=23c8b11bd3c1dee56781cc83e65e077409c0858e'/>
<id>urn:sha1:23c8b11bd3c1dee56781cc83e65e077409c0858e</id>
<content type='text'>
The quote endpoint only logged failures, so a "send everything" that went
wrong somewhere after the quote left nothing to correlate against. Log the
solved amount and fees for max quotes and the fee for plain ones.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>funds: close deposits, withdrawals and authorizations</title>
<updated>2026-09-22T02:32:54Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-22T02:32:54Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=d128ee51c292c331ea1f26d745a5fc280fb3f439'/>
<id>urn:sha1:d128ee51c292c331ea1f26d745a5fc280fb3f439</id>
<content type='text'>
The fund mechanism is retired. Deposits (POST /payments with `fund`),
withdrawals (POST /take) and pre-authorizations (POST /authorize) now
refuse unconditionally instead of consulting the `fund:disabled` flag,
so the door cannot be reopened by clearing a key.

The deposit path never had a kill-switch of its own, so with the flag
set users could still pay into funds they were unable to withdraw from.

Read paths are left intact: GET /fund/:id and the manager endpoints
still serve existing funds so balances stay visible and can be settled
out of band.

Co-Authored-By: Claude Opus 5 (1M context) &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>ecash: redeem tokens by melting to the user's own invoice</title>
<updated>2026-09-16T11:53:22Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-16T11:53:22Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=c72fa2a5ccf1cc04073f9ff12606491971dadd36'/>
<id>urn:sha1:c72fa2a5ccf1cc04073f9ff12606491971dadd36</id>
<content type='text'>
Redeeming ecash is now a plain NUT-05 melt into a Lightning invoice owned
by the redeeming user, credited by the regular lightning listener. Drop
the house proof pool and its lock, the /mint and /melt routes, and the
special-casing of a mint account in NWC and payment routing. Tokens are
decoded against the mint's keyset list so short v1 ids resolve, and an
empty keyset list is never cached.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>printer: publish receipts at QoS 1 and clean LNURL metadata memos</title>
<updated>2026-09-14T20:19:52Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-14T20:19:52Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=19f73ae9cf2afffbbb2abc71eb80c6d5bdccca52'/>
<id>urn:sha1:19f73ae9cf2afffbbb2abc71eb80c6d5bdccca52</id>
<content type='text'>
Receipts were published at QoS 0, so a printer that was briefly offline
(WiFi blip, keepalive timeout) never received them. QoS 1 lets the broker
queue them for printers that hold a persistent session.

LNURL-pay invoices carry the raw metadata array as their memo when the
payer leaves no comment; unwrap the text/plain entry before printing.

Co-Authored-By: Claude Fable 5.1 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011cCouc5ASRXHDp5esLciEK
</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>fund: authenticated recovery of a rotated fund's old link</title>
<updated>2026-08-28T18:11:32Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-28T18:11:32Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=a7fc7959eeb58fa7230c7e0869012f03ad2b23b7'/>
<id>urn:sha1:a7fc7959eeb58fa7230c7e0869012f03ad2b23b7</id>
<content type='text'>
When a fund's secret id has been rotated (invalidating an exfiltrated id list),
its old id 404s. Let a legit owner who bookmarked the old link find the fund
again — but reveal the new id ONLY to an authenticated manager of the new fund,
via a `fund:rotated:&lt;oldid&gt;` -&gt; newid map. An anonymous caller or a non-owner
gets the same "fund not found" as before, so an attacker replaying a leaked old
id learns nothing (an open old-&gt;new redirect would defeat the rotation). The
/fund/:id route now runs `optional` auth so req.user is available without
requiring it; bearer funds with no managers are intentionally not recoverable
this way. rotate-funds.ts records the mapping on any future rotation.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
</content>
</entry>
</feed>
