|
| 1 | +# Bond payout invoice |
| 2 | + |
| 3 | +The `add-bond-invoice` action is the counterparty-direction dual of [`pay-bond-invoice`](./pay_bond_invoice.md): where `pay-bond-invoice` asks a taker to lock a bond at the start of a trade, `add-bond-invoice` asks the non-slashed counterparty to provide a Lightning invoice for their share of a bond that has just been slashed. The flow is bi-directional — Mostro first sends a request message, the counterparty replies with a bolt11 — and it only fires after a solver-directed slash via [Admin Settle order](./admin_settle_order.md) / [Admin Cancel order](./admin_cancel_order.md) or a timeout-driven slash. |
| 4 | + |
| 5 | +## Direction and trigger |
| 6 | + |
| 7 | +- **Mostro → counterparty (request).** Mostro emits this message after a [`bond_resolution`](./admin_settle_order.md#bond-resolution-payload) settles the *other* party's bond. The request is re-emitted periodically until the counterparty replies or the claim window expires; clients must treat the repeats as idempotent reminders, not as new requests for a fresh invoice. |
| 8 | + |
| 9 | +- **Counterparty → Mostro (reply).** The counterparty replies with a bolt11 sized at the counterparty share (`order.amount` in the request payload). The reply must arrive before the forfeit deadline (see [Forfeit deadline](#forfeit-deadline) below). |
| 10 | + |
| 11 | +## Mostro → counterparty (request) |
| 12 | + |
| 13 | +Mostro sends a single `add-bond-invoice` message to the non-slashed counterparty. The payload is a `bond_payout_request` variant that bundles the order context with the slash anchor: |
| 14 | + |
| 15 | +```json |
| 16 | +[ |
| 17 | + { |
| 18 | + "order": { |
| 19 | + "version": 1, |
| 20 | + "id": "<Order Id>", |
| 21 | + "action": "add-bond-invoice", |
| 22 | + "payload": { |
| 23 | + "bond_payout_request": { |
| 24 | + "order": { |
| 25 | + "id": "<Order Id>", |
| 26 | + "kind": "sell", |
| 27 | + "amount": 500, |
| 28 | + "fiat_code": "VES", |
| 29 | + "fiat_amount": 100, |
| 30 | + "payment_method": "face to face", |
| 31 | + "premium": 1 |
| 32 | + }, |
| 33 | + "slashed_at": 1734000000 |
| 34 | + } |
| 35 | + } |
| 36 | + } |
| 37 | + }, |
| 38 | + null |
| 39 | +] |
| 40 | +``` |
| 41 | + |
| 42 | +- `bond_payout_request.order.amount` is the counterparty's share of the slashed bond in sats. The reply's bolt11 principal must match this value exactly. |
| 43 | +- `bond_payout_request.slashed_at` is the Unix timestamp (seconds, UTC) at which the slash was recorded — see [Forfeit deadline](#forfeit-deadline). |
| 44 | + |
| 45 | +The message carries no hardcoded human-readable deadline text; the client renders that warning locally in the user's own locale from `slashed_at` and the info-event window tag. |
| 46 | + |
| 47 | +## Forfeit deadline |
| 48 | + |
| 49 | +The client computes the forfeit deadline as: |
| 50 | + |
| 51 | +```text |
| 52 | +deadline = slashed_at + bond_payout_claim_window_days * 86_400 |
| 53 | +``` |
| 54 | + |
| 55 | +- `slashed_at` is read from the `bond_payout_request` payload above. |
| 56 | +- `bond_payout_claim_window_days` is a tag published on the Mostro instance's kind-38385 info event — see [Other events published by Mostro](./other_events.md). |
| 57 | + |
| 58 | +`slashed_at` is shipped on the wire (rather than derived from the time the message lands) so that the deadline is stable across periodic re-deliveries and accurate even if the recipient — or their relay — was offline for several days. A deadline computed from "now + window" at receipt time would silently drift into the future on every retry; reading `slashed_at` from the payload pins it to the moment Mostro actually slashed. Clients **must** read `slashed_at` from the payload and **must not** substitute their local clock. |
| 59 | + |
| 60 | +## Counterparty → Mostro (reply) |
| 61 | + |
| 62 | +The counterparty replies with a Gift wrap Nostr event whose rumor content carries the bolt11 inside the standard `payment_request` 3-tuple. The third element is `null` because the invoice carries its own amount: |
| 63 | + |
| 64 | +```json |
| 65 | +[ |
| 66 | + { |
| 67 | + "order": { |
| 68 | + "version": 1, |
| 69 | + "id": "<Order Id>", |
| 70 | + "action": "add-bond-invoice", |
| 71 | + "payload": { |
| 72 | + "payment_request": [ |
| 73 | + null, |
| 74 | + "lnbcrt5u1pj59wmepp5...", |
| 75 | + null |
| 76 | + ] |
| 77 | + } |
| 78 | + } |
| 79 | + }, |
| 80 | + "<index N signature of the sha256 hash of the serialized first element of content>" |
| 81 | +] |
| 82 | +``` |
| 83 | + |
| 84 | +The reply is signed with the **trade key** of the counterparty side — the side that was *not* slashed — exactly as in any other order-scoped action; see [Keys management](./key_management.md). |
| 85 | + |
| 86 | +## Recipient resolution |
| 87 | + |
| 88 | +The recipient of the request DM is the non-slashed counterparty of the trade, derived from the order's `buyer_pubkey` / `seller_pubkey` and the side the solver flagged in [`bond_resolution`](./admin_settle_order.md#bond-resolution-payload): |
| 89 | + |
| 90 | +| Order kind | Solver flag | Bond on … | Recipient | |
| 91 | +|------------|----------------------------|-----------|------------------| |
| 92 | +| `sell` | `slash_buyer = true` | taker | maker (seller) | |
| 93 | +| `sell` | `slash_seller = true` | maker | taker (buyer) | |
| 94 | +| `buy` | `slash_buyer = true` | maker | taker (seller) | |
| 95 | +| `buy` | `slash_seller = true` | taker | maker (buyer) | |
| 96 | + |
| 97 | +## Expected client behaviour |
| 98 | + |
| 99 | +- **Dispatch on the action discriminator.** Route `add-bond-invoice` through its own handler. Collapsing it with `add-invoice` will misclassify the bolt11 as a trade-payout invoice and lead to user-visible accounting errors. |
| 100 | +- **Render the deadline locally.** Combine `slashed_at` from the `bond_payout_request` payload with `bond_payout_claim_window_days` from the kind-38385 info event ([Other events published by Mostro](./other_events.md)). Do not derive the deadline from receive time. |
| 101 | +- **Treat re-deliveries as idempotent reminders.** The same outstanding request is being repeated; do not re-prompt the user for a fresh invoice on every retry. `slashed_at` is re-emitted unchanged, so the deadline the client shows must not shift across retries. |
| 102 | +- **Sign the reply with the non-slashed side's trade key.** See [Keys management](./key_management.md). |
| 103 | + |
| 104 | +## Failure modes |
| 105 | + |
| 106 | +- The counterparty never replies before the deadline → no Lightning payment arrives and the share is forfeited; the slashed funds remain with the node. |
| 107 | +- A reply arrives after the deadline, or from a sender other than the resolved recipient, or after another reply already won the race → Mostro responds with a `cant-do` action carrying reason `not-allowed-by-status`. |
| 108 | +- The bolt11 principal does not match the requested counterparty share, or the invoice is otherwise undecodable / expired → Mostro responds with `cant-do` reason `invalid-invoice`. |
| 109 | +- On a node where the operator retains 100% of slashed bonds, **no `add-bond-invoice` message is emitted at all**. Clients should not surface a phantom payout request. |
| 110 | + |
| 111 | +On success, the counterparty receives their share as a Lightning payment from the node's wallet. The routing fee is paid separately from that wallet and is not deducted from the principal. |
0 commit comments