I made an Ark wallet with a unilateral exit that a user can actually use.

  • https://jumble.social/notes/nevent1qqs28ejp5e7htr3v8gdsc3qm8e0737tt9mwhvr60ut37m3pxsghv3cspz3mhxue69uhhyetvv9ujumn0wd68ytngw5q32amnwvaz7tmwdaehgu3wxg6nv6e39ejx2aspy9mhxue69uhk6atvw35hqmr90pjhytngw4eh5mmwv4nhjtnhdaexceqzyz4vqlv4pzwwdt0s3wg4d4purf9tt9xxzv9hmjcjasveqzx9sxdz7n0adzf
  • https://stacker.news/items/1518557
  • https://stacker.news/items/1521494

This is just a vibe-coded proof-of-concept wallet. Don’t put any meaningful amount of money in it.

I saw the recent argument that in these “easy” Lightning-alternative wallets — the ones that advertise no channels, no liquidity to manage; Spark being the main one, and broadly Ark too — the unilateral exit is just talk. I think Ark has the better trust model among them, but even there nobody had actually built an exit a user can really use. I’d been working on an app out of the same frustration, so I finished a prototype and I’m sharing it.

https://github.com/tajava2006/arkade-nwc-bridge

I built the UX so a user can actually unilaterally exit by clicking, not by running some complicated CLI.

  1. A pre-signed exit transaction needs CPFP, so you need an address the client fully controls. Instead of some separate address, I derive a plain taproot address from the client’s own private key and let you top it up as exit fuel. Unlike a normal ARK address (whether an Ark address or an onchain boarding address), this one has no extra script — it’s unlocked by a single private key only — so the client can freely use it for CPFP.

  2. To minimize the exit right’s dependence on the server, I made this a long-running daemon rather than an ordinary client. To fully own the exit right you have to grab the proof from the server at the exact moment your balance changes, and back it up. If you don’t get the proof, you just treat the money as not received — so at that point the ownership question is demoted to a UX question. The long-running daemon persists the proof automatically on every balance change, so at the actual moment of exit the server isn’t needed. For what it’s worth: as far as I know, on a statechain like Spark even a proof (recovery bundle) you’ve already fetched can later become invalid depending on what the server does, so you have to keep refreshing it — whereas in Ark a proof you’ve captured once is immutable until that VTXO expires, so Ark is better on this point. That said, Ark also mints new proofs on every receive/change/refresh, so if you want a fully current sync you end up needing to keep it running anyway — so I wouldn’t call it an overwhelming advantage over Spark.

  3. Obviously, since change (a new VTXO) also comes back when you send, not just when you receive, the exit right is synced the moment any balance change happens.

  4. The exit right is a pre-signed transaction tree, and a lot of VTXOs end up sharing the same transaction as their proof. So for storage efficiency, when they overlap I only store one copy.

  5. By “syncing the proofs” I mean there’s GC logic that automatically drops proofs you no longer need after spending, so storage doesn’t grow without bound. And of course it doesn’t delete pre-signatures that other proofs still need — it only picks out and deletes the pre-signatures needed exclusively by the spent VTXO.

  6. The “spent, no longer needed” call is not just taking the server’s word for it — it demands proof that I spent it. That is: if I hold a VTXO with an exit right and the server claims I spent it, the client asks the server to produce the spend transaction signed by my key, verifies it, and only deletes on a match. So it correctly detects and syncs the case where you use the same private key in a separate Arkade wallet, without false alarms. If the server can’t produce it, the proof is immediately quarantined and preserved permanently.

  7. The opposite thing a server could pull — claiming it credited you a VTXO but withholding the proof — is also surfaced on the dashboard right away.

  8. You can exit each VTXO separately, so you can pick only the economically worthwhile ones. It shows how many onchain transactions it’ll take in total and the estimated exit cost, so you don’t pay unnecessary exit fees, and because there’s an address for CPFP, if fees spike mid-exit and your mining priority slips you can quickly bump it via RBF. You can watch the progress as a graph.

  9. The default exit destination is the taproot exit-fuel address from #1. It’s an address you already fully control, but if you want a different one you can register it, and to prevent copy-paste mistakes it only sends to an address that has passed a challenge.

  10. I hate making users set up env vars, so it runs with just git clone -> bun install -> bun run dev, no special .env needed.

  11. Everything used for the exit is identical across Arkade, so at fresh start you can enter which Ark to use — if you already have an Ark wallet, put in that server and private key and it instantly syncs the proofs of every VTXO in that wallet. So it’s compatible with the official arkade.money. (One catch: official arkade.money has delegation ON by default, and turning delegation on adds a tapleaf that changes the ARK address even for the same key — so only funds on a delegation-off address land on the same address and sync.)

  12. It boots fine even when the server is down. Everything else stops working, but the exit function still works. Of course, to actually exit you need the proofs to have already been synced beforehand — you can’t leave it off, have the server go down, and then use this to escape.

While I was at it I improved usability a bit too:

  1. I made it easy to empty out. With other wallets it’s annoying that you can’t cleanly deal with leftover dust change, so here you can make an invoice for the right amount to leave with zero change on the Lightning side, or hit the max button on an onchain send to take everything out cleanly.

  2. I implemented a sub-dust (under 330) atomic swap. sub-dust LN swaps themselves were supported first by the Second team’s bark, but that one is non-atomic (server-trusted) — there’s no pre-signed commitment underneath, so if a swap only half-completes the server can end up taking the sub-dust HTLC. Making it atomic via a pre-signed split is, I believe, a first in the Ark camp.

To unpack the background: a sub-dust amount can’t be a standalone onchain output in the first place (Bitcoin dust rule). Ark’s unilateral exit means pulling out onchain without the server’s permission, and for sub-dust that onchain claim is below dust so it vanishes. That’s why Arkade blocks a sub-dust VTXO from being spent offchain until a refresh — in a UTXO model where coins merge and split, if you just let it be spent it would contaminate the exit rights of other perfectly good VTXOs. (Note this is a per-implementation policy choice, not a protocol hard-lock. bark, by contrast, has a policy of never merging VTXOs, so it doesn’t have this contamination, and it lets you spend sub-dust relatively freely and supports sub-dust LN — but it pays for that with fragmentation, since change only ever keeps splitting.)

A normal atomic swap sends the target amount to an intermediate script address for the claimer to sweep later, and that sub-dust output can’t be swept before a refresh, so it gets stuck at exactly that step. That’s why the official implementation’s minimum swap amount is 330.

I implemented sub-dust atomic swaps by not sending the swap’s target sub-dust VTXO to the intermediate script address, but instead sending a larger, normal dust+ amount and having it split on claim. This obviously requires changing the server, and it doesn’t work on the official Arkade wallet — only on my own Ark server.

Here are actual screenshots of the wallet.

image

  • The exit readiness field is the number of proofs I actually hold locally / the VTXO countV the server says "you have this much". If the former is smaller (proven < claimed), that’s always a bad signal — the server credited you funds but didn’t give you the proof, so a strong warning shows. If the former is larger (proven > claimed) there are two branches, and the app distinguishes them intelligently. One is a fraud attempt where the server suddenly claims a VTXO I never spent is “gone” — that gets quarantined, the exit right is preserved permanently, and a warning goes up on the dashboard. The other is a temporary, benign mismatch like the swap-in-progress case I explain right below. So even at proven > claimed, if it’s fraud it warns, and if it’s normal it stays quiet. This screenshot is the latter, so there’s no alarm.

  • This is mid-swap. When you spend your VTXO into the script address during a sub-dust swap, naturally the VTXO count you hold drops by one. But that swap isn’t finished — the LN payment can fail for various reasons, and then you need a refund. So you have to keep the proof around, at least temporarily, even then. This isn’t server misbehavior, so no warning is attached. If the VTXO count is low and it isn’t this case, a strong warning shows and the proof is quarantined permanently.

  • Active connections means NWC connections. Since I was making it a long-running daemon anyway, I set it up so you can use it from various nostr clients over NWC.

  • There are three ways to receive. For LN, I used a CLINK noffer to make a fixed receive handle without buying a domain name or opening inbound traffic. If you put an onchain address in there, it automatically swaps into a VTXO.

image

  • This is the send tab. You put an Ark, LN, or onchain address into a single input without distinction, enter the amount (except LN — the invoice already contains it), and send. For the reason above, sub-dust VTXOs have their normal use restricted until a refresh, but an onchain send acts as a refresh itself, so you can freely use and send even a sub-dust VTXO. In other words, sub-dust or not, you can pull out all your funds immediately regardless of amount.

  • The balance-breakdown shows the list of VTXOs I own and their expiry. You need to refresh before expiry, and of course if you leave it running it does that automatically. You can still use them after expiry, but that depends on the server’s mercy — there’s no cryptographic guarantee.

  • It auto-refreshes if you leave it alone, but you can also do it manually if you want. That said, an unnecessarily frequent refresh can be a griefing attack on the server, so a refresh only goes through if there’s at least one urgent VTXO (less than 3 days to expiry). Refresh costs no fee, so it’s an essential measure.

image

  • I’m skipping the history tab; this is the tab that shows only swaps out of it. It only shows Lightning receives and payments of external invoices, and I split it out because a Lightning payment that doesn’t ultimately go through needs a refund.

  • The regular swaps officially supported by the Arkade team and the custom sub-dust swaps I made are shown separately. Since something I built personally is presumably a bit less stable, I put it on top so it’s easy to spot if something goes wrong.

  • The in-flight one is a payment failure I induced on purpose. I received an external invoice and paid it — that is, you (the user) sent a VTXO to a script address shared with the Ark server, and if for whatever reason the Ark server didn’t pay that invoice on your behalf, you can get that amount refunded after the waiting period.

  • The invoice was 100 sats, but because of the sub-dust issue above, what actually went to the script address was 500 sats. On a normal payment, Ark claims it, I get 400 back, and Ark takes 100. But in this case Ark didn’t pay the external invoice, so it couldn’t get the preimage, so it can’t claim. When the waiting period ends, the user will be able to get the whole 500 sats back via refund.

image

  • The most important one, the exit tab. As explained, even the 500-sat one stuck at the script address mid-swap has its proof kept, so I set it up so it can be unilaterally exited. It shows the waiting period and the estimated cost so you can tell at a glance whether it’s worth exiting.

image

  • The detail page for each VTXO. Since I’m a single user the transaction tree is a straight line, but in general it’d be a more complex tree.

image

  • This is what an actual exit attempt looks like. It broadcasts in tree order one by one, with no mid-way cancel. If you turn it off and back on, it keeps broadcasting from where it left off.

https://mempool.space/tx/b663386e24ee1419dba26306e1cf58ada2875d0d9c29bdc4c3801b272b543bd3

https://mempool.space/tx/eb5cfb1111c447187b7866b5981af991852e944d4ffb3545ec638ea604b9c33a

  • These are the transactions actually submitted as a CPFP package. You can see the real exit transaction with fee 0, the anyone-can-spend 0-amount output for the fee bump, and the exit fuel that pulled it via CPFP — 278 sats went in across the two transactions combined.

image

  • All the tree has been unrolled and the Ark-server-protection waiting period is ticking. After that you can sweep fully. If you don’t specify a separate address it sweeps to the exit-fuel address; if you submit an arbitrary address and complete the proof-of-address, the sweep goes to that address automatically.

A lot of people dislike Ark, but I think Lightning has too many limits for an individual to run, so I think an alternative is needed. If the problem were simply that it’s hard, I’d have become a Lightning purist too. But the problem isn’t difficulty — with Lightning, block space is consumed exclusively for a single person, in proportion to the number of people. Given that block space is hard-capped while the number of people only grows, complete self-custody becomes close to impossible. The day when a single onchain transaction fee is more than a person can afford over an entire lifetime’s income will inevitably come. Broadly, there are three proposed ways to ease this:

  1. Hard-fork to increase block space itself.
  2. Offload to sidechains (merged-mined, drivechains, etc.).
  3. Statechains. The Spark cited in this post belongs here.
  4. A shared-utxo model like Ark.

I don’t know Spark (statechains) precisely so I’m cautious, but here’s why I think Ark is better on the trust model. In Ark the operator can never move my funds with its own keys alone. The exception is a VTXO I just received (not yet refreshed) being double-spent by the operator colluding with that VTXO’s previous owner — and that disappears the moment I refresh once and cut the chain of previous owners, i.e. ownership is fully confirmed on every refresh. As far as I understand it, on statechains this “cut the history to confirm” concept is weak, so operator trust lingers longer (not certain). So despite Ark’s few downsides, I think #4 has the fewest side effects among the solutions proposed so far, and I’ve tried to contribute by implementing, as a PoC, a solution to those downsides.

Thanks to the economies of scale in the model’s structure, if Ark takes off, the Ark server can run a sustainable business while refreshing very often, the user gets ownership confirmed on every refresh, and because many people participate at once the onchain fee is suppressed to nothing or a negligible level. The ASP sustains its business by charging only for leaving its own Ark.

Take a look and I’d appreciate a review.


#1 #4
Write a comment