Context
Today ArkPayoutHandler.ParseClaimDestination (BTCPayServer.Plugins.ArkPayServer/Payouts/Ark/ArkPayoutHandler.cs:96-117) accepts exactly two destination shapes:
- A bare
ArkAddress (tark1...)
- A
bitcoin: URI (wrapped as ArkUriClaimDestination)
Anything else — a bare on-chain address (bc1…, bcrt1…, etc.), a BOLT11 invoice (lnbc…), or an LNURL — is rejected with "A valid address was not provided". Operators that want to pay those destinations have to fall back to the BTC-CHAIN or BTC-LN payout methods explicitly, which fragments the "Arkade balance pays things" mental model.
The plugin already has the underlying primitives wired up for the Send wizard:
- VTXO → on-chain: batch-settle to an on-chain output, or chain swap via Boltz
- VTXO → Lightning: submarine swap via Boltz
So the gap is at the payout-handler layer, not the protocol layer.
Proposal
Let the Arkade payout method auto-detect the destination type:
| Destination |
Route |
tark1… (Ark address) |
VTXO → VTXO (current behaviour) |
bitcoin: URI with ark= |
VTXO → VTXO (current behaviour) |
bitcoin: URI without ark= |
VTXO → on-chain (chain swap or batch on-chain output) |
| Bare on-chain address |
VTXO → on-chain (same as above) |
lnbc… (BOLT11) |
VTXO → LN (Boltz submarine swap) |
| LNURL-pay |
resolve to BOLT11, then route as LN |
Single payout method, dispatch happens inside InitiatePayment based on the parsed claim destination.
Tradeoffs / open questions
-
Failure-mode opacity. Each route has very different latency and failure characteristics:
- VTXO → VTXO: instant, near-bulletproof
- VTXO → on-chain: ~10s batch wait, or minutes via Boltz chain swap (depends on Boltz limits + arkd batch session)
- VTXO → LN: depends on Boltz availability, swap limits, and the LN route
A payout that "just hangs" because we silently fell back across tiers will be hard to debug. We probably need explicit status messages per route (e.g. "Awaiting batch settle", "Boltz swap pending", "LN settling").
-
Min/max amount constraints differ per route. Boltz swaps have configured min/max; arkd batch settle has its own dust threshold; LN has its own. GetMinimumPayoutAmount currently returns the arkd dust value — would need to be route-aware (or just return the loosest minimum and validate later).
-
Fees differ per route. Today the payout proof is ArkPayoutProof (TransactionId). A chain-swap or LN payout would have a different "proof" — swap id, preimage, or final on-chain txid. The proof model probably needs to be a discriminated union, or a base type with route-specific subclasses.
-
Refund semantics for failed swaps. If a Boltz submarine swap fails after our VTXO is locked into the HTLC, the cooperative-refund path needs to be triggered automatically (or surfaced to the operator). The plugin already has SwapsManagementService.RequestRefundCooperatively — needs to be tied into payout cancellation/reject.
-
Alternative scope: keep payout strict (Ark address only) and instead document that operators should pick BTC-LN / BTC-CHAIN explicitly for non-Ark destinations. Less elegant UX but the dispatch + failure-mapping problem disappears.
Acceptance criteria (sketch)
Out of scope
- Cross-asset payouts (Arkade asset balance → BTC destination). Same hatch but different problem.
- Boarding-input-only payouts (paying into a boarding address from an external wallet — that's the inverse of what we're doing here).
Context
Today
ArkPayoutHandler.ParseClaimDestination(BTCPayServer.Plugins.ArkPayServer/Payouts/Ark/ArkPayoutHandler.cs:96-117) accepts exactly two destination shapes:ArkAddress(tark1...)bitcoin:URI (wrapped asArkUriClaimDestination)Anything else — a bare on-chain address (
bc1…,bcrt1…, etc.), a BOLT11 invoice (lnbc…), or an LNURL — is rejected with"A valid address was not provided". Operators that want to pay those destinations have to fall back to theBTC-CHAINorBTC-LNpayout methods explicitly, which fragments the "Arkade balance pays things" mental model.The plugin already has the underlying primitives wired up for the Send wizard:
So the gap is at the payout-handler layer, not the protocol layer.
Proposal
Let the Arkade payout method auto-detect the destination type:
tark1…(Ark address)bitcoin:URI withark=bitcoin:URI withoutark=lnbc…(BOLT11)Single payout method, dispatch happens inside
InitiatePaymentbased on the parsed claim destination.Tradeoffs / open questions
Failure-mode opacity. Each route has very different latency and failure characteristics:
A payout that "just hangs" because we silently fell back across tiers will be hard to debug. We probably need explicit status messages per route (e.g. "Awaiting batch settle", "Boltz swap pending", "LN settling").
Min/max amount constraints differ per route. Boltz swaps have configured min/max; arkd batch settle has its own dust threshold; LN has its own.
GetMinimumPayoutAmountcurrently returns the arkd dust value — would need to be route-aware (or just return the loosest minimum and validate later).Fees differ per route. Today the payout proof is
ArkPayoutProof(TransactionId). A chain-swap or LN payout would have a different "proof" — swap id, preimage, or final on-chain txid. The proof model probably needs to be a discriminated union, or a base type with route-specific subclasses.Refund semantics for failed swaps. If a Boltz submarine swap fails after our VTXO is locked into the HTLC, the cooperative-refund path needs to be triggered automatically (or surfaced to the operator). The plugin already has
SwapsManagementService.RequestRefundCooperatively— needs to be tied into payout cancellation/reject.Alternative scope: keep payout strict (Ark address only) and instead document that operators should pick
BTC-LN/BTC-CHAINexplicitly for non-Ark destinations. Less elegant UX but the dispatch + failure-mapping problem disappears.Acceptance criteria (sketch)
ArkPayoutHandler.ParseClaimDestinationrecognises bare on-chain addresses + BOLT11 invoices (and optionally LNURL).InitiatePaymentdispatches each destination type to the appropriate Send/Swap flow rather than always redirecting to the offchain Send wizard.ArkPayoutProofis extended (or split) so the proof reflects the route actually taken (offchain txid / on-chain txid / swap id + preimage).GetMinimumPayoutAmountis route-aware.Out of scope