<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinos-server.git/lib, branch staging</title>
<subtitle>coinos server — legacy coinos.io backend (Lightning, on-chain, Liquid)</subtitle>
<id>https://git.coinos.io/coinos-server.git/atom?h=staging</id>
<link rel='self' href='https://git.coinos.io/coinos-server.git/atom?h=staging'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/'/>
<updated>2026-06-18T14:43:48Z</updated>
<entry>
<title>invoices: don't re-list an invoice id on update (dedupe :invoices)</title>
<updated>2026-06-18T14:43:48Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-18T14:43:48Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=64691693e7ff40aae59b05a234f88025757959e3'/>
<id>urn:sha1:64691693e7ff40aae59b05a234f88025757959e3</id>
<content type='text'>
Port of the master fix (2d2f9856) + cleanup script to staging.

generate() unconditionally lPush'd the invoice id to `${aid}:invoices`,
but it's also called from update() (tip/webhook changes, repeated bolt12
offer creation) re-using the existing id — so each update re-appended the
same id and the user's :invoices accumulated duplicate entries, rendering
the same offer/invoice multiple times. Guard the push with LPOS so it only
adds the id the first time.

Also add scripts/dedup-invoices.ts to scrub already-accumulated dupes
(dry-run by default, --apply to write, idempotent).

Co-Authored-By: Claude Opus 4.8 (1M context) &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>lightning: watchdog only recycles on real backlog, not normal idle</title>
<updated>2026-06-17T14:48:18Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:45:44Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=0a1450ada9c20499fa86a619ee36176f717d5414'/>
<id>urn:sha1:0a1450ada9c20499fa86a619ee36176f717d5414</id>
<content type='text'>
Port of master 7b71c96b, refining the zombie-listener watchdog staging
adopted in 7c34b431. Treating "waitanyinvoice blocked &gt; 5min while cl is
alive" as a stall false-fires during normal quiet periods (~26x in 6h on
master 2026-06-07) and needlessly recycles the cl listen socket. Instead,
when blocked past the threshold, probe cl on the MAIN socket with
waitanyinvoice(pay_index, 2s server-side timeout): an invoice returned
means a real backlog the wedged listener missed -&gt; recycle; a timeout
(CLN 904) means the listener is just idle -&gt; leave it alone.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>ln: time out short-lived RPCs so a hung cl reconnects without restart</title>
<updated>2026-06-17T14:47:50Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:44:19Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=3d2a980971b59cdd2a4ea41b2da51f370eee6b75'/>
<id>urn:sha1:3d2a980971b59cdd2a4ea41b2da51f370eee6b75</id>
<content type='text'>
Port of master a664af99. A unix-domain RPC socket has no TCP keepalive, so
if cl goes unresponsive while the socket stays open (zombie/half-open) an
awaited call hangs forever — nothing throws, the reconnect-on-error path
never fires, and only an app restart recovers. Wrap short-lived calls in a
30s Promise.race timeout (matching lib/health.ts); a timeout rejects with
ETIMEDOUT, now part of isSocketDied, so the client is dropped and the next
call reconnects. Long-polls, in-flight payments (pay/xpay/sendpay/keysend)
and bolt12 round-trips are exempt to avoid leaked debits.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>nwc: fix pay_invoice reply reliability, rate-limit drops, always reply</title>
<updated>2026-06-17T14:47:49Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:42:59Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=b83d5dfc6dc27b7af1fbc723b61e017fd9caf7d7'/>
<id>urn:sha1:b83d5dfc6dc27b7af1fbc723b61e017fd9caf7d7</id>
<content type='text'>
Port of master 8cb18821 (refs #80), adapted to staging's nwc event loop.

(a) pay_invoice: sendLightning runs xpay to completion and finalize() sets
p.ref = preimage, so return that directly instead of polling listpays and
racing a 20s timeout that returned a misleading "Payment timed out" even
when the payment settled. Keep a longer listpays loop as a fallback; on
still-in-flight return an explicit "check with lookup_invoice".

(b) rate limiting: raised per-pubkey cap 5 -&gt; 60/min, and over-limit
requests now get a NIP-47 RATE_LIMITED reply instead of a silent drop that
looked like a lost reply. Every request path now always sends a 23195
reply (success, error, or handler-returned-nothing) so a client never
hangs. Pulled the reply/encrypt logic into a reply() helper.

The notify default-on part of 8cb18821 is intentionally NOT ported: master
10284aa1 reverted it to opt-in, which is already staging's behavior
(notify = String(notify) === "true").

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>nwc: publish 13194 info event only when missing, recheck hourly</title>
<updated>2026-06-17T14:47:49Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:40:33Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=ceb003afb154560bb68b4252759fedc1fee75a0f'/>
<id>urn:sha1:ceb003afb154560bb68b4252759fedc1fee75a0f</id>
<content type='text'>
Port of master 6372d918, adapted to staging's nwc relay loop (no
heartbeat here). NWC clients fetch the kind 13194 info event before they
talk to the wallet; without it they show "no info event" and an unknown
balance. 13194 is replaceable and meant to persist, so instead of blindly
republishing on every connect, query the relay (ensureInfo) and publish
only the server pubkeys whose event is actually missing. Re-check hourly
so a relay restart that leaves our socket intact self-heals; interval is
cleared on close/reconnect. Verified nostr@0.2.8 Relay exposes ws/
readyState, on("eose") and unsubscribe.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>nwc: pay_invoice honors request amount for amountless bolt11</title>
<updated>2026-06-17T14:47:49Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:38:29Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=0987582546363935c5a388a9accb9b245c11a7c7'/>
<id>urn:sha1:0987582546363935c5a388a9accb9b245c11a7c7</id>
<content type='text'>
Port of master ce9616f6. pay_invoice derived the amount solely from the
decoded invoice, so an amountless bolt11 yielded amount = round(undefined/
1000) = NaN and sendLightning rejected it with "Invalid amount". NIP-47
lets the client specify the amount (msat) in the request; fall back to it
when the invoice carries none, with a clear error if still missing.
Fixes #83.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>nwc: fix make_invoice timestamps (ms -&gt; seconds) breaking NWC clients</title>
<updated>2026-06-17T14:47:48Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:38:29Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=af8e395dd9ed010fce0e3b92593d5ea6d39dc65a'/>
<id>urn:sha1:af8e395dd9ed010fce0e3b92593d5ea6d39dc65a</id>
<content type='text'>
Port of master 0154bb06. generate() stores `created` in milliseconds, but
NWC make_invoice returned it directly as created_at and computed
expires_at = created_at + expiry. NWC timestamps are Unix SECONDS
(NIP-47), so clients parsed a ~1.78e12 expires_at that overflowed their
date types (ptcpay's .NET DateTimeOffset, issue #84). Convert created to
seconds. Fixes #84.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>analytics: monthly insight reports + signup instrumentation</title>
<updated>2026-06-17T14:47:48Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:36:43Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=33de17fa10e4fc7f44456cfbb3152fd16c86a567'/>
<id>urn:sha1:33de17fa10e4fc7f44456cfbb3152fd16c86a567</id>
<content type='text'>
Port of master 183a3cb0 + 3db24424. scripts/analytics.ts streams live db
(SCAN, never KEYS) and reports a trailing window: MAU/WAU, new users,
custody total + top holders, network volume by type, most-active/highest-
volume users, bolt12 recipients; --csv flag for month-over-month trends.
scripts/analytics-geo.ts reports top countries from the request log.
lib/register.ts stamps `created` and stores `country` on new users so
reports get signup timing/geography from the db directly.

Note: these scripts were written against the master stack; on staging
balances live in TigerBeetle, so custody/holder figures may need the
balance source adjusted from the Redis balance: keys.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>finalize: don't crash on non-integer fee delta (tbRefund NaN)</title>
<updated>2026-06-17T14:47:47Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:34:12Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=51847c5e05ad70e936fd374e01e62e0e4b5e3c72'/>
<id>urn:sha1:51847c5e05ad70e936fd374e01e62e0e4b5e3c72</id>
<content type='text'>
Port of master 8aba0bc2, adapted to this stack's tbRefund. When xpay's
response shape is unexpected (or a zero-amount invoice's decoded
amount_msat is undefined) the computed p.fee becomes NaN, and the refund
delta crashes the balance write, leaving finalize partial (user loses up
to maxfee). Validate the computed fee is a finite integer; if not, keep
the originally-reserved maxfee (zero refund, no balance corruption) and
warn. Same guard on the refund delta before tbRefund.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>sendLightning/sendKeysend: instrument exit paths, tighten LEAKED DEBIT guard</title>
<updated>2026-06-17T14:47:47Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-06-14T19:34:04Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=e55165307096093d769893475a7481c781457f94'/>
<id>urn:sha1:e55165307096093d769893475a7481c781457f94</id>
<content type='text'>
Port of master 51107ed4 + fe767002 + cb0f1962 (combined) to the staging
stack. Track an outcome string through every exit path, handle the
xpay failed_parts branch explicitly (verify with listpays then finalize/
reverse/leave-pending), and dump abnormal xpay/keysend response shapes.
Leak guard re-reads the db record before firing: if ref is persisted the
payment is settled regardless of which path threw (finalize may set ref
then throw on the later tbRefund), so only a genuinely missing ref logs
"LEAKED DEBIT". Uses warn (no warnThrottled on this stack) and keeps the
existing retry_for: 20.

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