Overview

See also: User Guide (English) | Guía de Usuario (Español) | Run Your Own Node

Mostro uses Addressable Events to publish different types of information. Each event type has its own kind:

Event TypeKindDescription
Orders38383P2P order events for the shared order book
Ratings38384User rating events
Info38385Mostro instance status and information
Disputes38386Dispute events

You can find more details about the order event here

The Message

Transports

Mostro messages travel over one of two interchangeable wire transports. A given node speaks exactly one of them — there is no dual mode — and advertises which in its instance-info event (kind 38385) through the protocol_version tag ("1" or "2"):

ProtocolTransportEvent kindStatus
v1NIP-59 Gift Wrap1059DEPRECATED (default through v0.18.x)
v2NIP-44 direct message14current (default from v0.19.0)

Both transports carry the same logical message and, once unwrapped, yield the same structure to the daemon's handlers — only the envelope and how the identity key is proven differ. Client developers should support both during the transition and pick per node from the protocol_version tag; see the client migration guide.

The logical message itself (the first tuple element described below) is identical across transports, except for the version field: 1 on the gift-wrap transport, 2 on the NIP-44 direct transport.

The logical message

In protocol v1 all messages from/to Mostro are Gift wrap Nostr events; the content of the rumor event is a JSON-serialized array as a string (with no white space or line breaks), the first element is the message, the second element is the signature of the sha256 hash of the serialized first element. In protocol v2 the same array travels NIP-44 encrypted inside a signed kind-14 event and gains a third element (the identity proof) — see Keys management for the v2 wire format. Here the structure of the first element (the logical message, shared by both transports):

  • Wrapper: Wrapper of the Message
    • <version integer>: Version of the protocol — 1 on the gift-wrap transport, 2 on the NIP-44 direct transport
    • [id integer]: (optional) Wrapper Id
    • [request_id integer]: (optional) Mostro daemon should send back this same id in the response
    • [trade_index integer]: (optional) This field is used by users who wants to maintain reputation, it should be the index of the trade in the user's trade history
    • <action string>: Action to be performed by Mostro daemon
    • [payload string]: (optional) Payload of the message, it should be a JSON-serialized string. The content of this field depends on the action field.

Here an example of a new-order order message:

{
  "order": {
    "version": 2,
    "id": "<Order id>",
    "request_id": 123456,
    "trade_index": 1,
    "action": "new-order",
    "payload": {
      "order": {
        "id": "<Order id>",
        "kind": "sell",
        "status": "pending",
        "amount": 0,
        "fiat_code": "VES",
        "fiat_amount": 100,
        "payment_method": "face to face",
        "premium": 1,
        "created_at": 1698870173
      }
    }
  }
}

The content array (v1 vs v2)

The first element above is the logical message. The array that actually travels in the content differs by transport:

Protocol v1 (gift wrap) — a 2-element array:

[
  { "order": { "version": 1, "...": "..." } },
  "<trade-key signature, or null>"
]

Protocol v2 (NIP-44 direct) — a 3-element array, the v1 pair plus an identity proof, NIP-44 encrypted inside a signed kind-14 event:

[
  { "order": { "version": 2, "...": "..." } },
  "<trade-key signature, or null>",
  ["<identity pubkey>", "<identity signature>"]
]
elementmeaning
1the logical message (the version: 2 wrapper shown above)
2the trade key's signature over the serialized first element, or null (Mostro replies and full-privacy mode set this element to null; the outer kind-14 event is still signed, as in v1)
3the identity proof ["<identity pubkey>", "<identity signature>"], or null for full-privacy mode (where the identity is the trade key itself)

In v1 the identity key is carried, authenticated, by the gift-wrap seal. v2 has no seal, so the identity instead travels inside the ciphertext as element 3 — never visible at the event level, exactly as private as before. The full v2 envelope and identity-proof signing rule are documented in Keys management.

Payment Request Array Structure

The payment_request field in the payload can have different structures depending on the use case:

  1. Lightning invoice from buyer/seller to Mostro (action: add-invoice):

    • With amount: [null, "lnbc..."]
    • Without amount (0-amount invoice): [null, "lnbc...", amount_in_sats]
  2. Lightning address:

    • Taking a sell order with fixed amount: [null, "user@domain.com"]
    • Taking a sell Range order: [null, "user@domain.com", fiat_amount]

Optional Fields

Some fields in order objects may be null or omitted depending on the context:

  • request_id: Optional field that clients can include to correlate responses with requests. Mostro will echo this value back in responses.
  • trade_index: Required for users maintaining reputation, omitted when using full privacy mode.
  • expires_at: Unix timestamp when the order expires. Present in order confirmations and updates, but set to 0 or null when creating new orders.
  • created_at: Unix timestamp when the order was created. Set to 0 or current time when creating orders; Mostro will set the actual timestamp.
  • Signature (second array element): Required for reputation mode (signs the first element with trade key), set to null in full privacy mode or for Mostro responses.

Payment Method Format

Payment methods can be specified like this:

   ["pm", "face to face", "bank transfer", "mobile"]

Keys management

It is required to read NIP-59 (gift wrap) and NIP-06 to fully understand this document

Mostro clients should implement nip59 which creates newly fresh keys on each message to Mostro, but the key management is a bit more complex, here we will explain how to manage keys in Mostro clients.

Objectives:

  • Facilitate portability by using a deterministic key generation mechanism based on NIP-06.
  • Prevent users from mistakenly entering key material already used in other Nostr social media apps.
  • Rotate keys for every trade.

When a user started a Mostro client for first time, the client should create a new mnemonic seed phrase which is the only information users will need to share with other client to have the same Mostro session. Mostro clients should use the derivation path m/44'/1237'/38383'/0/0.

Clients will always use the first key (zero) m/44'/1237'/38383'/0/0 to identify itself with mostrod, users who wants to maintain reputation can send an event to Mostro signed with the zero key, the identity key, used to update their rating, for every new order created or taken the client will start deriving new keys from m/44'/1237'/38383'/0/1, users who don't want to maintain reputation simply don't send the identity key to mostrod, let's see it in more detail with an example:

  • Alice starts a Mostro client for first time, at that moment the client creates a new mnemonic seed phrase and derive two keys, the identity key (index 0) and the next trade key with index 1 m/44'/1237'/38383'/0/1, we will use identiy key to sign the gift wrap seal event and the trade key to sign the first element of the content of the rumor event.

  • Alice wants to buy some bitcoin and take a sell order, the client send a message in a Gift wrap Nostr event to mostrod with the seal signed with index 0 key and in the rumor we should demostrate we own the trade key (index 1), let's see a take-sell example in an unencrypted gift wrap event:

// external wrap layer
{
  "id": "<id>",
  "kind": 1059,
  "pubkey": "<Buyer's ephemeral pubkey>",
  "content": {
    // seal
    "id": "<seal's id>",
    "pubkey": "<index 0 pubkey (identity key)>",
    "content": {
      // rumor
      "id": "<rumor's id>",
      "pubkey": "<Index 1 pubkey (trade key)>",
      "kind": 1,
      "content": [
        {
          "order": {
            "version": 1,
            "id": "<Order Id>",
            "trade_index": 1,
            "action": "take-sell",
            "payload": null
          }
        },
        "<index 1 signature of the sha256 hash of the serialized first element of content>"
      ],
      "created_at": 1691518405,
      "tags": []
    },
    "kind": 13,
    "created_at": 1686840217,
    "tags": [],
    "sig": "<index 0 pubkey (identity key) signature>"
  },
  "tags": [["p", "<Mostro's pubkey>"]],
  "created_at": 1234567890,
  "sig": "<Buyer's ephemeral pubkey signature>"
}
  • After finish the deal the rate each other.

Then Alice wants to create a new buy order:

  • The client derives the next key, new key is index 2 (m/44'/1237'/38383'/0/2) and send a message in a Gift wrap Nostr event to mostrod with the seal signed with index 0 key, but let's see the complete example with a full unencrypted gift wrap:
// external wrap layer
{
  "id": "<id>",
  "kind": 1059,
  "pubkey": "<Buyer's ephemeral pubkey>",
  "content": {
    // seal
    "id": "<seal's id>",
    "pubkey": "<index 0 pubkey (identity key)>",
    "content": {
      // rumor
      "id": "<rumor's id>",
      "pubkey": "<Index 2 pubkey (trade key)>",
      "kind": 1,
      "content": [
        {
          "order": {
            "version": 1,
            "trade_index": 2,
            "action": "new-order",
            "payload": {
              "order": {
                "kind": "buy",
                "status": "pending",
                "amount": 0,
                "fiat_code": "VES",
                "fiat_amount": 100,
                "payment_method": "face to face",
                "premium": 1,
                "created_at": 1691518405
              }
            }
          }
        },
        "<index 2 signature of the sha256 hash of the serialized first element of content>"
      ],
      "created_at": 1691518405,
      "tags": []
    },
    "kind": 13,
    "created_at": 1686840217,
    "tags": [],
    "sig": "<index 0 pubkey (identity key) signature>"
  },
  "tags": [["p", "<Mostro's pubkey>"]],
  "created_at": 1234567890,
  "sig": "<Buyer's ephemeral pubkey signature>"
}

Now Alice waits for some seller to take her order, mostrod will show Alice's reputation but not Alice pubkey.

Full privacy mode

Clients must offer a more private version where the client never send the identity key to mostrod, in that case mostrod can't link orders to users, the tradeoff is that users who choose this option cannot have a reputation, let's see a take-sell example in an unencrypted gift wrap event:

// external wrap layer
{
  "id": "<id>",
  "kind": 1059,
  "pubkey": "<Buyer's ephemeral pubkey>",
  "content": {
    // seal
    "id": "<seal's id>",
    "pubkey": "<index N pubkey (trade key)>",
    "content": {
      // rumor
      "id": "<rumor's id>",
      "pubkey": "<index N pubkey (trade key)>",
      "kind": 1,
      "content": [
        {
          "order": {
            "version": 1,
            "id": "<Order Id>",
            // "trade_index": 1, // not needed
            "action": "take-sell",
            "payload": null
          }
        },
        null // Signature is not needed in full privacy mode
      ],
      "created_at": 1691518405,
      "tags": []
    },
    "kind": 13,
    "created_at": 1686840217,
    "tags": [],
    "sig": "<index N pubkey (trade key) signature>"
  },
  "tags": [["p", "<Mostro's pubkey>"]],
  "created_at": 1234567890,
  "sig": "<Buyer's ephemeral pubkey signature>"
}

Protocol v2 — NIP-44 direct messages

Everything above describes protocol v1 (NIP-59 gift wrap, kind 1059), which is DEPRECATED. Nodes that advertise protocol_version = "2" in their instance-info event speak protocol v2 instead: a single signed NIP-44 direct message of kind 14, with no gift-wrap or seal layer. The key derivation, indexing and rotation rules are unchanged — the same identity key (index 0) and per-trade keys (index 1, 2, …); only the envelope and how the identity key is proven differ.

What changes on the wire:

  • The trade key authors the event. The visible pubkey/sig are the trade key's (not a throwaway ephemeral key as in v1). This is deliberate: it lets relays rate-limit by sender and lets the node cheaply pre-filter spam before decrypting. The exposure is bounded because trade keys are single-trade and rotated.
  • content is NIP-44 ciphertext of the message array. The conversation key is derived from the trade key ↔ Mostro pair, so only those two can decrypt.
  • The array gains a third element, the identity proof — because there is no seal to carry the identity key authenticated. See below.
  • An expiration tag (NIP-40) is always present so trade messages do not linger on relays forever. The concrete expiration window is chosen by the node and is not part of the protocol.
  • version is 2 in the message.

Note (deliberate NIP-17 deviation): NIP-17 defines kind 14 as an unsigned rumor that only ever travels inside a gift wrap. Mostro publishes it signed, because the author is an ephemeral single-trade key — the association NIP-17 protects against is here intentional and bounded. These are not standard NIP-17 chat messages.

Reputation mode (take-sell example)

Reusing Alice's take-sell from above (trade key index 1, identity key index 0), the v2 event looks like this:

{
  "id": "<id>",
  "kind": 14,
  "pubkey": "<index 1 pubkey (trade key)>",
  "content": "<NIP-44 ciphertext of the array below>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1691518405,
  "sig": "<index 1 (trade key) signature>"
}

The decrypted content is the message array — the same first two elements as v1, plus the identity proof as a third element:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "trade_index": 1,
      "action": "take-sell",
      "payload": null
    }
  },
  "<index 1 signature of the sha256 hash of the serialized first element>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Identity proof

The identity signature (third element) is not taken over the message alone. The identity key signs a domain-tagged payload that binds the proof to the specific trade key authoring the event:

mostro-transport-v2-identity:<trade pubkey hex>:<message JSON>

where <message JSON> is the serialized first element and <trade pubkey hex> is this event's pubkey. The receiver recomputes the payload from event.pubkey, so a proof grafted onto an event authored by a different trade key fails verification. This binding is what the gift-wrap seal signature provided implicitly in v1. The signing scheme is the existing Schnorr-over-sha256 Message::sign / verify_signature, and the identity key signs once per message — the same custody model as v1, where it signs every seal.

Full privacy mode

As in v1, a client that does not want to maintain reputation never sends the identity key. In v2 that means both the trade signature and the identity proof are null, and the identity is taken to be the trade key itself:

{
  "id": "<id>",
  "kind": 14,
  "pubkey": "<index N pubkey (trade key)>",
  "content": "<NIP-44 ciphertext of the array below>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1691518405,
  "sig": "<index N (trade key) signature>"
}

Decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-sell",
      "payload": null
    }
  },
  null,
  null
]

Messages from Mostro

Mostro authors its replies with its own well-known key and NIP-44 encrypts them to the user's trade key (p-tagged). As in v1, Mostro's own messages are unsigned: the trade signature and identity proof are both null. Clients can subscribe with authors=[mostro] AND #p=[their trade keys].

Transport migration (v1 → v2)

Mostro is moving its wire transport from protocol v1 (NIP-59 gift wrap, kind 1059) to protocol v2 (NIP-44 direct message, kind 14). This page is the practical guide for client developers: what changes, how to detect which transport a node speaks, and how to support both during the transition.

The logical messages, key derivation, indexing and rotation rules are unchanged — only the envelope differs. The two formats are documented side by side in Keys management (the v2 wire format is under Protocol v2 — NIP-44 direct messages) and the message tuples in Overview.

Why the change

Gift wraps give strong metadata privacy, but their outer event is signed by a random throwaway key, so neither relays nor the daemon can tell legitimate traffic from garbage without paying the full NIP-44 decrypt cost — a spam flood ("Gift Wrap Apocalypse") cannot be rate-limited by sender. Protocol v2 makes the trade key the visible author of the event. Because trade keys are already single-trade and rotated, exposing one leaks little, while enabling relay-side rate limiting by sender and cheap daemon-side pre-validation before decryption. See the threat model in issue #626.

Capability discovery

A node speaks exactly one transport — there is no dual mode. It advertises which in its instance-info event (kind 38385) via the protocol_version tag:

  • ["protocol_version", "1"] → gift wrap (kind 1059)
  • ["protocol_version", "2"] → NIP-44 direct (kind 14)

A client should read this tag before sending anything and use the matching wire format. Old daemons that predate the tag emit nothing; treat their absence as v1.

What a client must change

  1. Read protocol_version from the node's kind-38385 event and branch on it.
  2. Subscribe to the right kind: 1059 for v1, 14 for v2 (authored by the node, #p-tagged to your trade keys for node replies).
  3. Wrap/unwrap with the matching path. mostro-core 0.13.0 ships both — wrap_message_with(transport, …) / unwrap_incoming(event, …) dispatch on the transport (or event kind), so a client holding both paths needs only to pass the node's transport.
  4. Set version: 2 in the message on the v2 transport (1 on v1).
  5. On v2, build the 3-element content tuple — message, trade signature (or null), identity proof ["<identity pubkey>", "<identity sig>"] (or null for full-privacy mode). The identity proof is a signature over the domain-tagged payload mostro-transport-v2-identity:<trade pubkey hex>:<message JSON>; see Keys management → Identity proof.
  6. On v2, add a NIP-40 expiration tag to outgoing events. Mostro fills a default (the node's dm_days, 30 days) on its own messages when none is supplied.
  7. Read the node's proof-of-work tags and mine the outer event accordingly — a new order or take is charged at the higher pow_first_contact rate. See Proof of work and the first-contact gate below.

Full-privacy mode and reputation mode work the same way as in v1: omit the identity key (proof and trade signature both null) for full privacy, or include them to maintain reputation.

Proof of work and the first-contact gate

Making the trade key the visible author is what lets a node filter before decrypting, and proof of work (NIP-13) is how it charges for that first look. Both difficulties are published in the instance-info event and are chosen per instance — many run with 0 and require nothing.

  • pow — required of every event the client sends, on either transport.
  • pow_first_contact — required of an event whose visible sender is a trade key the node does not currently associate with an active order or dispute. In practice that is the first event of a trade: creating an order, or taking one. It is never lower than pow and is typically higher, because that lane is where spam concentrates. Once the node associates the trade key with an active order or dispute, its later messages are back to needing only pow.

Two consequences for a client:

  1. Mine on the outer event. The difficulty is counted in leading zero bits of the event id — the kind-14 event's own id on v2, the gift wrap's id on v1 — not of the inner message. Grind the nonce tag (["nonce", "<counter>", "<target bits>"]) as NIP-13 describes; most Nostr libraries expose this as a "pow" option on the event builder.
  2. Under-powered events vanish. The check happens before the node decrypts anything, so there is no cant-do message and no error of any kind — the event is simply dropped. A client that mines against pow when the node asked for pow_first_contact sees its order creation silently do nothing. Read the info event before sending.

An absent pow_first_contact tag means unknown, not zero and not pow. Some protocol-v2 daemons enforce a configured first-contact difficulty but predate the tag, so assuming pow there is exactly the mistake that produces a silent drop. When the tag is missing and the node speaks v2, mine at least pow and, if a first contact goes unanswered, retry a freshly built event at a higher difficulty (doubling the bits up to a cap you choose) before concluding the node is unreachable — silence is the only feedback the gate gives. Nodes that publish the tag need none of this guesswork, which is the reason to prefer them.

On v2 a node also drops a re-sent identical event id for a short window as replay defense, so a retry must be a freshly built event rather than a rebroadcast of the same one. And independently of PoW, on both transports it discards messages whose inner created_at is older than a short freshness window (ten seconds in the current daemon) — so mine and publish promptly rather than preparing events far in advance.

Release timeline

  • v0.18.0 — protocol v2 ships. Default transport = "gift-wrap", so nothing changes for existing clients. Protocol v1 is DEPRECATED. Client developers have the 0.18.x cycle to ship v2 support.
  • v0.19.0 — protocol v2 becomes the default and only protocol. mostrod removes the v1 path entirely. mostro-core keeps its gift-wrap helpers so clients can still migrate at their own pace, but nodes will no longer accept kind-1059 traffic.

The recommendation is therefore: keep both wrap paths now and select per node from protocol_version. A client that supports both will work against every node throughout the transition, and against v2-only nodes after the v0.19.0 cutover with no further change.

Creating a new sell order

To create a new sell order the user should send a NIP-44 direct message (kind 14) to Mostro, the decrypted content should look like this:

[
  {
    "order": {
      "version": 2,
      "action": "new-order",
      "trade_index": 1,
      "payload": {
        "order": {
          "kind": "sell",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "min_amount": null,
          "max_amount": null,
          "fiat_amount": 100,
          "payment_method": "face to face,bank transfer,mobile",
          "premium": 1,
          "created_at": 0
        }
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Let's explain some of the fields:

  • kind: sell or buy
  • status: Is always pending when creating a new order
  • amount: 0 for when we want to sell with at market price, otherwise the amount in satoshis
  • created_at: No need to send the correct unix timestamp, Mostro will replace it with the current time

The event to send to Mostro would look like this:

{
  "id": "<Event id>",
  "kind": 14,
  "pubkey": "<Seller's trade pubkey>",
  "content": "<NIP-44 ciphertext of the content array>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1234567890,
  "sig": "<Seller's trade key signature>"
}

Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and apply_to is "make" or "both", the maker must lock a bond before the order is published. Instead of a new-order confirmation, Mostro first responds with a pay-bond-invoice message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is not visible on Nostr until the bond HTLC is accepted.

Only after the maker pays the bond does Mostro:

  1. Publish the order to Nostr with status pending.
  2. Send the new-order confirmation (shown below).

If the maker never pays the bond invoice it expires and no order is created. See Pay bond invoice — Maker bond for details.

Confirmation message

Mostro will send back a kind 14 event as a confirmation message to the user like the following (decrypted content example):

[
  {
    "order": {
      "version": 2,
      "id": "<Order id>",
      "action": "new-order",
      "payload": {
        "order": {
          "id": "<Order id>",
          "kind": "sell",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face,bank transfer,mobile",
          "premium": 1,
          "created_at": 1698870173
        }
      }
    }
  },
  null,
  null
]

Mostro publishes this order as an event kind 38383 with status pending:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702548701,
    "kind": 38383,
    "tags": [
      ["d", "<Order id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "pending"],
      ["amt", "0"],
      ["fa", "100"],
      ["pm", "face to face", "bank transfer", "mobile"],
      ["premium", "1"],
      ["rating", "[\"rating\",{\"days\":10,\"total_rating\":4.5,\"total_reviews\":7}]"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Creating a new sell range order

To create a new range order the user should send a NIP-44 direct message (kind 14) to Mostro with the following message:

{
  "order": {
    "version": 2,
    "action": "new-order",
    "trade_index": 1,
    "payload": {
      "order": {
        "kind": "sell",
        "status": "pending",
        "amount": 0,
        "fiat_code": "VES",
        "min_amount": 10,
        "max_amount": 20,
        "fiat_amount": 0,
        "payment_method": "face to face",
        "premium": 1,
        "created_at": 0
      }
    }
  }
}

Here we have two new fields, min_amount and max_amount, to define the range of the order. The fiat_amount field is set to 0 to indicate that the order is for a range of amounts.

When a taker takes the order, the amount will be set on the message.

Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and apply_to is "make" or "both", the maker of a range order must lock a bond before the order is published — same as non-range orders, with one key difference: the bond is sized against max_amount, not the minimum.

Mostro responds to the new-order with a pay-bond-invoice instead of the confirmation below. The order is not published to Nostr until the bond HTLC is accepted.

When a taker takes a slice of the range order, the bond obligation is reduced proportionally (the slice's fiat amount relative to max_amount). The bond hold invoice remains Locked for the lifetime of the range order and is settled once in full when the range closes; partial slashes are tracked as child bond rows rather than early partial settlements. See Pay bond invoice — Maker bond for details.

Confirmation message

Mostro will send back a kind 14 event as a confirmation message to the user like the following:

[
  {
    "order": {
      "version": 2,
      "id": "<Order id>",
      "action": "new-order",
      "payload": {
        "order": {
          "id": "<Order id>",
          "kind": "sell",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "min_amount": 10,
          "max_amount": 20,
          "fiat_amount": 0,
          "payment_method": "face to face",
          "premium": 1,
          "created_at": 1698870173
        }
      }
    }
  },
  null,
  null
]

Mostro publishes this order as an event kind 38383 with status pending:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702548701,
    "kind": 38383,
    "tags": [
      ["d", "<Order id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "pending"],
      ["amt", "0"],
      ["fa", "10", "20"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["rating", "[\"rating\",{\"days\":10,\"total_rating\":4.5,\"total_reviews\":7}]"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Creating a new buy order

To create a new buy order the user should send a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "action": "new-order",
      "trade_index": 1,
      "payload": {
        "order": {
          "kind": "buy",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "created_at": 0
        }
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

The nostr event will look like this:

{
  "id": "<Event id>",
  "kind": 14,
  "pubkey": "<Buyer's trade pubkey>",
  "content": "<NIP-44 ciphertext of the content array>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1234567890,
  "sig": "<Buyer's trade key signature>"
}

Optional: anti-abuse maker bond

When the Mostro node has bonds enabled and apply_to is "make" or "both", the maker must lock a bond before the order is published. Instead of a new-order confirmation, Mostro first responds with a pay-bond-invoice message asking the maker to pay a small hold invoice (typically ~1% of the trade amount). The order is not visible on Nostr until the bond HTLC is accepted.

Only after the maker pays the bond does Mostro:

  1. Publish the order to Nostr with status pending.
  2. Send the new-order confirmation (shown below).

If the maker never pays the bond invoice it expires and no order is created. See Pay bond invoice — Maker bond for details.

Confirmation message

Mostro will send back a kind 14 event as a confirmation message, the decrypted content looks like the following:

[
  {
    "order": {
      "version": 2,
      "id": "<Order id>",
      "action": "new-order",
      "payload": {
        "order": {
          "id": "<Order id>",
          "kind": "buy",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": null,
          "seller_trade_pubkey": null,
          "buyer_invoice": null,
          "created_at": 1698870173
        }
      }
    }
  },
  null,
  null
]

Mostro publishes this order as an event kind 38383 with status pending:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702548701,
    "kind": 38383,
    "tags": [
      ["d", "<Order id>"],
      ["k", "buy"],
      ["f", "VES"],
      ["s", "pending"],
      ["amt", "0"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["rating", "[\"rating\",{\"days\":10,\"total_rating\":4.5,\"total_reviews\":7}]"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

After a seller takes this order Mostro will request an invoice from the buyer, Mostro will pay the buyer's invoice when the seller releases the funds.

Creating the order with a lightning invoice

There are two ways where the buyer can create the order adding the invoice:

  1. If the buyer already knows the amount, e.g. buyer wants to buy 15000 sats with 10 euros, in this case the buyer knows from the beginning that the invoice should have amount 15000 - Mostro's fee, instead of the user doing the calculation, the client must do it and in some cases create the invoice with the right amount.

  2. If the buyer don't know the amount, e.g. buyer wants to buy sats with 10 euros, in this case the buyer can add an amountless invoice and Mostro will pay it with the market price amount - Mostro's fee automatically.

Creating a new order

Creating buy order with a lightning address would make the process way faster and easy going, to acomplish the buyer should send a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "action": "new-order",
      "trade_index": 1,
      "payload": {
        "order": {
          "kind": "buy",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face,mobile",
          "premium": 1,
          "buyer_invoice": "mostro_p2p@ln.tips",
          "created_at": 0
        }
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

The nostr event will look like this:

{
  "id": "<Event id>",
  "kind": 14,
  "pubkey": "<Buyer's trade pubkey>",
  "content": "<NIP-44 ciphertext of the content array>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1234567890,
  "sig": "<Buyer's trade key signature>"
}

Confirmation message

Mostro will send back a kind 14 event as a confirmation message to the user like the following:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "new-order",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "buy",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face,mobile",
          "premium": 1,
          "buyer_trade_pubkey": null,
          "seller_trade_pubkey": null,
          "buyer_invoice": "mostro_p2p@ln.tips",
          "created_at": 1698870173
        }
      }
    }
  },
  null,
  null
]

Mostro publishes this order as an event kind 38383 with status pending:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702548701,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "buy"],
      ["f", "VES"],
      ["s", "pending"],
      ["amt", "0"],
      ["fa", "100"],
      ["pm", "face to face", "mobile"],
      ["premium", "1"],
      ["rating", "[\"rating\",{\"days\":10,\"total_rating\":4.5,\"total_reviews\":7}]"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

After a seller takes this order Mostro will not ask for an invoice to the buyer, Mostro will get the buyer's invoice and paid it when the seller releases the funds.

Listing Orders

Mostro publishes new orders with event kind 38383 and status pending:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702548701,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "pending"],
      ["amt", "0"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["rating", "[\"rating\",{\"days\":10,\"total_rating\":4.5,\"total_reviews\":7}]"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Clients can query this events by nostr event kind 38383, nostr event author, order status (s), order kind (k), order currency (f), type (z)

Request order details

Clients can request detailed information for existing orders by sending a NIP-44 direct message (kind 14) with the action orders. This is useful for refreshing stale UI state or restoring a session from the mnemonic seed on a new device.

Request message

The client sends a message where the payload object includes an ids array of order IDs. At least one ID must be provided, and Mostro may reject the request if the array exceeds its configured limits.

[
  {
    "order": {
      "version": 2,
      "request_id": 8721,
      "action": "orders",
      "payload":  {
        "ids": [
          "c7dba9db-f13f-4c3f-a77f-3b82e43c2b1a",
          "751bc178-801a-4cc4-983c-68682e6fb6af"
        ]
      }
    }
  },
  null,
  null
]

Field:

  • ids: Array of order ids exactly as published in the order event d tag.

The client only can request their own orders; mostrod will not provide information about any ID if it does not belong to the requesting party.

Mostro response

Mostro replies with the same action and includes a structured payload describing each order that could be resolved, here is how the message look like:

[
  {
    "order": {
      "version": 2,
      "request_id": 8721,
      "action": "orders",
      "payload": {
        "orders": [
          {
            "id": "c7dba9db-f13f-4c3f-a77f-3b82e43c2b1a",
            "kind": "sell",
            "status": "active",
            "amount": 3307,
            "fiat_code": "ARS",
            "min_amount": 1000,
            "max_amount": 5000,
            "fiat_amount": 5000,
            "payment_method": "Mercado Pago,Lemon",
            "premium": 2,
            "buyer_trade_pubkey": "<trade pubkey>",
            "seller_trade_pubkey": "<trade pubkey>",
            "created_at": 1758889527,
            "expires_at": 1758975927
          },
          {
            "id": "751bc178-801a-4cc4-983c-68682e6fb6af",
            "kind": "sell",
            "status": "fiat-sent",
            "amount": 1201,
            "fiat_code": "ARS",
            "min_amount": null,
            "max_amount": null,
            "fiat_amount": 2000,
            "payment_method": "MODO",
            "premium": 0,
            "buyer_trade_pubkey": "<trade pubkey>",
            "seller_trade_pubkey": "<trade pubkey>",
            "created_at": 1759168820,
            "expires_at": 1759255220
          }
        ]
      }
    }
  },
  null,
  null
]

Orders that are missing or unauthorized for the requesting user are not listed in the orders array.

Limits and rate control

Mostrod enforces per-request limits to protect the daemon. Common policies include a max_orders_per_response cap (for example, 20 orders) and a rolling request quota per pubkey.

When a response is truncated because too many ids were requested, Mostrod returns only the first allowed ids. Clients should re-issue the call with the remaining ids after respecting the rate limits.

When a user exceeds the allowed request rate, Mostrod can answer with an error action such as too-many-requests. Implementations should surface these conditions to the user and back off accordingly.

Taking a sell order

If the order amount is 0 the buyer doesn't know the exact amount to create the invoice, buyer will send a message in a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-sell",
      "trade_index": 1,
      "payload": null
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Optional: anti-abuse bond step

When the receiving Mostro node has bonds enabled, the buyer (taker) first receives a pay-bond-invoice message; the published NIP-33 order event remains pending (re-takeability is preserved — see order event) and the bond message's embedded SmallOrder carries status: pending (the daemon tracks the internal waiting-taker-bond state in its database only — it is never emitted on the wire). Only after the bond HTLC is Accepted does Mostro send the add-invoice message described below. Clients that do not recognise pay-bond-invoice should expect the take to time out and surface a clear error to the user — do not silently retry the take.

Mostro response

In order to continue the buyer needs to send a lightning network invoice to Mostro, in this case the amount of the order is 0, so Mostro will need to calculate the amount of sats for this order, then Mostro will send back a message asking for a LN invoice indicating the correct amount of sats that the invoice should have, here the decrypted content of the message:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "amount": 7851,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": null,
          "seller_trade_pubkey": null
        }
      }
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to in-progress:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "in-progress"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face", "bank transfer"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Buyer sends LN invoice

The buyer sends a NIP-44 direct message (kind 14) to Mostro with the lightning invoice, the action should be the same the buyer just received in the last message from Mostro (add-invoice), here the decrypted content of the event for an invoice with no amount:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "payment_request": [
          null,
          "lnbcrt1pn9dvx0pp5935mskms2uf8wx90m8dlr60ytwn5vxy0e65ls42h7y7exweyvekqdqqcqzzsxqyz5vqsp5xjmllv4ta7jkuc5nfgqp8qjc3amzfewmlycpkkggr7q2y5mjfldq9qyyssqncpf3vm8hwujutqc99f0vy45zh8es54mn6u99q9t6rwm0q80dxszskzrp24y46lxqkc7ly9p80t6lalc8x8xhsn49yhy70a7wqyygugpv7chqs",
          3922
        ]
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

If the invoice includes an amount, the last element of the payment_request array should be set to null.

Mostro response

Mostro send a NIP-44 direct message (kind 14) to the buyer with a wrapped order in the decrypted content, it would look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "waiting-seller-to-pay",
      "payload": null
    }
  },
  null,
  null
]

Taking a sell order with a lightning address

The buyer can use a lightning address to receive funds and avoid to manually create and send lightning invoices on each trade, to acomplish this the buyer will send a message in a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-sell",
      "trade_index": 1,
      "payload": {
        "payment_request": [null, "mostro_p2p@ln.tips", null]
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

The event to send to Mostro would look like this:

{
  "id": "<Event id>",
  "kind": 14,
  "pubkey": "<Buyer's trade pubkey>",
  "content": "<NIP-44 ciphertext of the content array>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1234567890,
  "sig": "<Buyer's trade key signature>"
}

Mostro response

Mostro send a NIP-44 direct message (kind 14) to the buyer with a wrapped order in the decrypted content, it would look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "waiting-seller-to-pay",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to in-progress:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "in-progress"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Taking a sell range order

If the order fiat amount is a range like 10-20 the buyer must indicate a fiat amount to take the order, buyer will send a message in a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-sell",
      "trade_index": 1,
      "payload": {
        "amount": 15
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Mostro response

In order to continue the buyer needs to send a lightning network invoice to Mostro, Mostro calculates the amount of sats from the fiat amount the buyer selected, then sends back a message asking for a LN invoice indicating the correct amount of sats that the invoice should have, here the decrypted content of the message:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "amount": 7851,
          "fiat_code": "VES",
          "min_amount": 10,
          "max_amount": 20,
          "fiat_amount": 15,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": null,
          "seller_trade_pubkey": null,
          "buyer_invoice": null,
          "created_at": null,
          "expires_at": null
        }
      }
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to in-progress:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "in-progress"],
      ["amt", "7851"],
      ["fa", "15"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Using a lightning address

The buyer can use a lightning address to receive funds and avoid creating and sending lightning invoices on each trade, with a range order we set the fiat amount as the third element of the payment_request array, to accomplish this the buyer will send a message in a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-sell",
      "payload": {
        "payment_request": [null, "mostro_p2p@ln.tips", 15]
      }
    }
  },
  null,
  null
]

Taking a buy order

To take an order the seller will send to Mostro a message with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-buy",
      "trade_index": 1,
      "payload": null
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

The event to send to Mostro would look like this:

{
  "id": "<Event id>",
  "kind": 14,
  "pubkey": "<Seller's trade pubkey>",
  "content": "<NIP-44 ciphertext of the content array>",
  "tags": [
    ["p", "<Mostro's pubkey>"],
    ["expiration", "<unix timestamp>"]
  ],
  "created_at": 1234567890,
  "sig": "<Seller's trade key signature>"
}

Optional: anti-abuse bond step

When the receiving Mostro node has bonds enabled, the seller (taker) first receives a pay-bond-invoice message; the published NIP-33 order event remains pending (re-takeability is preserved — see order event) and the bond message's embedded SmallOrder carries status: pending (the daemon tracks the internal waiting-taker-bond state in its database only — it is never emitted on the wire). Only after the bond HTLC is Accepted does Mostro send the pay-invoice message described below.

This is the only flow on which a single user pays two hold invoices in sequence on the same order: the bond first, then the trade hold invoice. They arrive as distinct actions (pay-bond-invoice then pay-invoice) and clients must present them as separate steps. Clients that do not recognise pay-bond-invoice should expect the take to time out and surface a clear error to the user — do not silently retry the take.

Mostro response

Mostro respond to the seller with a message with the following content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "pay-invoice",
      "payload": {
        "payment_request": [
          {
            "id": "<Order Id>",
            "kind": "buy",
            "status": "waiting-payment",
            "amount": 7851,
            "fiat_code": "VES",
            "fiat_amount": 100,
            "payment_method": "face to face",
            "premium": 1,
            "created_at": 1698957793
          },
          "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e"
        ]
      }
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to in-progress:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "buy"],
      ["f", "VES"],
      ["s", "in-progress"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

And send a message to the buyer with the following content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "waiting-seller-to-pay",
      "payload": null
    }
  },
  null,
  null
]

Seller pays LN invoice

After seller pays the hold invoice Mostro send a message to the seller with the following content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "waiting-buyer-invoice",
      "payload": null
    }
  },
  null,
  null
]

Mostro sends a message to the buyer with the following content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "status": "waiting-buyer-invoice",
          "amount": 7851,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "created_at": null
        }
      }
    }
  },
  null,
  null
]

Buyer sends LN invoice

Buyer sends the LN invoice to Mostro.

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "payment_request": [
          null,
          "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e"
        ]
      }
    }
  },
  null,
  null
]

Now both parties have an active order and they can keep going with the trade.

Taking a buy range order

If the order fiat amount is a range like 10-20 the seller must indicate a fiat amount to take the order, seller will send a message in a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "take-buy",
      "trade_index": 1,
      "payload": {
        "amount": 15
      }
    }
  },
  null,
  null
]

Mostro response

Response is the same as we explained in the Taking a buy order section with the seller receiving a hold invoice to pay and the buyer waiting for that payment.

Pay bond invoice

When the receiving Mostro node has the anti-abuse bond feature enabled, makers and/or takers must lock a small Lightning hold invoice as a security deposit before the trade flow proceeds. Mostro asks for this payment with the pay-bond-invoice action.

The bond is independent from the trade escrow: it is a second hold invoice with its own payment hash, and it is released on every normal exit path — completion, cancel before timeout, or dispute resolution where the solver does not direct otherwise. Bonds are typically around 1% of the trade amount (with an operator-configured floor), much smaller than the trade hold invoice that may follow.

The bond_apply_to tag in the Mostro info event (kind 38385) tells clients which sides must post a bond: "take", "make", or "both" — see Other events published by Mostro.

Taker bond

Direction and trigger

  • Direction: Mostro → user (the taker).
  • Trigger: Sent immediately after a successful take-buy / take-sell when the operator has bonds enabled for takers.
  • Order status: The published NIP-33 order event keeps the s tag at pending while the bond is outstanding (per NIP-69's four-bucket model — see Peer-to-peer Order events. NIP-69). The DM payload's embedded SmallOrder echo carries the daemon-internal status waiting-taker-bond so the recipient client can render the bond-payment phase distinctly, but external observers (other potential takers, order-book aggregators) continue to see the order as pending and may still attempt to take it. This is deliberate: a taker who never pays the bond cannot park the order off the book.

Mostro message to the taker

The message's content has the same shape as pay-invoice; only the action discriminator differs:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "pay-bond-invoice",
      "payload": {
        "payment_request": [
          {
            "id": "<Order Id>",
            "kind": "sell",
            "status": "pending",
            "amount": 7851,
            "fiat_code": "VES",
            "fiat_amount": 100,
            "payment_method": "face to face",
            "premium": 1,
            "created_at": 1698937797
          },
          "lnbcrt780n1pj59wmepp5..."
        ]
      }
    }
  },
  null,
  null
]

Note: the SmallOrder echo carries "status": "pending" — the NIP-69 wire bucket the order is advertised in while the bond is outstanding. The daemon-internal state waiting-taker-bond (which Mostro uses to route subsequent messages) is tracked only in the daemon's database and is never emitted in the DM payload. Clients should dispatch on the pay-bond-invoice action, not on a status field.

Expected client behaviour

  • Decode the bolt11 and surface it to the user explicitly as a bond, not as the trade escrow. Do not reuse the same wording or UI step that you use for pay-invoice.
  • Pay it. The HTLC enters Accepted state — the sats are locked in the taker's wallet, not captured.
  • Do not collapse this into the same UI step as any subsequent pay-invoice on the same order. They are independent HTLCs (different payment hashes), and the user must explicitly approve each.

Follow-up flow

Once the bond HTLC is Accepted, Mostro proceeds with the normal trade flow:

  • Sell order taken (taker = buyer): order status moves to waiting-buyer-invoice; the taker next receives add-invoice to provide a payout invoice.
  • Buy order taken (taker = seller): order status moves to waiting-payment; the taker next receives pay-invoice for the trade hold invoice.

Important — buy order taken (seller-as-taker): this is the only flow on which a single user pays two hold invoices in sequence on the same order — first the bond (pay-bond-invoice), then the trade escrow (pay-invoice). They arrive as distinct actions and must be presented to the user as separate steps. Do not auto-pay either, do not coalesce them, and make the distinction obvious in the UI; this is the most error-prone path for client developers.

Daemon status waiting-taker-bond (internal only)

Mostro tags the order's internal state as waiting-taker-bond so it can route subsequent messages correctly while one or more taker bonds are outstanding. This status lives only in the daemon's database — it is neither published on the NIP-33 order event (whose s tag stays pending) nor echoed in any DM payload (the pay-bond-invoice SmallOrder carries pending, as shown above). It is documented here only to describe the daemon's lifecycle; clients never observe the literal waiting-taker-bond value on the wire.

Internal transitions (not visible to clients):

  • From pendingwaiting-taker-bond, after a successful take-buy / take-sell when bonds are enabled.
  • From waiting-taker-bondwaiting-payment (buy order taken) or → waiting-buyer-invoice (sell order taken), once the bond HTLC is Accepted.
  • From waiting-taker-bondpending, if the bond bolt11 is never paid and expires, or the taker cancels before locking. The published s tag was pending throughout — observers see only that the take attempt left no trace.

Failure modes

  • The user never pays the bond bolt11 → the invoice expires; the order's NIP-33 status was pending throughout, so the rollback only undoes the daemon-internal take state. The order remains takeable.
  • The user pays the bond and then cancels before trade completion → the bond HTLC is cancelled and the funds return to the taker.
  • Slashing conditions (solver-directed dispute resolution, or timeout while in a waiting state) can settle the bond rather than release it. The solver directs slashing via the bond_resolution payload documented under Admin Settle order and Admin Cancel order, and the non-slashed counterparty is then asked for their share of the bond via Bond payout invoice. When a timeout slash fires, the slashed party first receives a bond-slashed notification.

Maker bond

When apply_to is "make" or "both", the maker must lock a bond before the order is published to Nostr. The same pay-bond-invoice action is used, but the trigger and the order lifecycle differ from the taker case.

Direction and trigger

  • Direction: Mostro → user (the maker).
  • Trigger: Sent in response to a new-order message when the operator has bonds enabled for makers, before the order is published.
  • Order visibility: The order is not published to Nostr while the maker bond is outstanding. External observers see nothing — no pending order event, no order in the book. This differs from the taker bond, where the order remains visible and re-takeable throughout. The daemon tracks this internally as waiting-maker-bond, a status kept in its database only — it is never emitted on the wire (the SmallOrder echo carries pending, as shown below).

Mostro message to the maker

The action and wire shape are identical to the taker case:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "pay-bond-invoice",
      "payload": {
        "payment_request": [
          {
            "id": "<Order Id>",
            "kind": "sell",
            "status": "pending",
            "amount": 7851,
            "fiat_code": "VES",
            "fiat_amount": 100,
            "payment_method": "face to face",
            "premium": 1,
            "created_at": 1698937797
          },
          "lnbcrt780n1pj59wmepp5..."
        ]
      }
    }
  },
  null,
  null
]

Note: the SmallOrder echo always carries "status": "pending" — this is the NIP-69 wire bucket the order will appear in once the bond locks and the order is published. The daemon-internal state waiting-maker-bond is tracked only in the daemon's database and is never emitted in DM payloads. No NIP-33 order event has been emitted yet at this point.

For range sell orders, the bond is sized against max_amount (not the individual fiat_amount). The SmallOrder echo will reflect the range order fields (min_amount, max_amount).

Follow-up after bond locks

Once the maker's bond HTLC is Accepted:

  1. Mostro publishes the order to Nostr with status pending for the first time.
  2. Mostro sends the maker the new-order confirmation message (same as in the no-bond flow — see Creating a new sell order and Creating a new buy order).

Daemon status waiting-maker-bond (internal only)

Like waiting-taker-bond, this status lives only in the daemon's database — it is never published on a NIP-33 event (none exists yet) nor echoed in any DM payload (the pay-bond-invoice SmallOrder carries pending). It is documented here only to describe the daemon's lifecycle.

Internal transitions (not visible to clients):

  • On new-order receipt → waiting-maker-bond, before any NIP-33 event is emitted.
  • waiting-maker-bondpending (and order published), once the bond HTLC is Accepted.
  • waiting-maker-bond discarded (no NIP-33 ever emitted), if the bond invoice expires without payment.

Failure modes

  • Maker never pays → invoice expires, order discarded silently. No NIP-33 event was ever emitted so the order book is unaffected.
  • Maker pays the bond, order publishes as pending, then maker cancels → bond HTLC released, funds return to maker.
  • A waiting-state timeout slash (when slash_on_waiting_timeout = true) settles the maker's bond HTLC instead of releasing it. The maker first receives a bond-slashed notification, then the order is canceled. The winning counterparty (the taker) then receives add-bond-invoice for their share.

Backwards compatibility

Clients running an older mostro-core version that does not yet know pay-bond-invoice will fail to deserialize the message and silently drop it; from the user's perspective the operation stalls and eventually times out without surfacing a useful error. Operators are responsible for not enabling bonds in production until clients in the wild have adopted the mostro-core release that ships Action::PayBondInvoice. Clients should:

  • Recognise the action explicitly and surface it to the user.
  • If unable to handle it (e.g. an older build talking to a bond-enabled node), present a clear error rather than silently retrying.

The Mostro info event carries bond-related tags so clients can detect bond-enabled nodes ahead of a take or create — see Other events published by Mostro.

Bond payout invoice

The add-bond-invoice action is the counterparty-direction dual of pay-bond-invoice: 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 Cancel order or a timeout-driven slash.

Direction and trigger

  • Mostro → counterparty (request). Mostro emits this message after a bond_resolution 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.

  • 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 below).

Mostro → counterparty (request)

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:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-bond-invoice",
      "payload": {
        "bond_payout_request": {
          "order": {
            "id": "<Order Id>",
            "kind": "sell",
            "amount": 500,
            "fiat_code": "VES",
            "fiat_amount": 100,
            "payment_method": "face to face",
            "premium": 1
          },
          "slashed_at": 1734000000
        }
      }
    }
  },
  null,
  null
]
  • bond_payout_request.slashed_at is the Unix timestamp (seconds, UTC) at which the slash was recorded — see Forfeit deadline.

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.

Forfeit deadline

The client computes the forfeit deadline as:

deadline = slashed_at + bond_payout_claim_window_days * 86_400
  • slashed_at is read from the bond_payout_request payload above.
  • bond_payout_claim_window_days is a tag published on the Mostro instance's kind-38385 info event — see Other events published by Mostro.

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.

Counterparty → Mostro (reply)

The counterparty replies with a NIP-44 direct message (kind 14) whose decrypted content carries the bolt11 inside the standard payment_request array. The invoice carries its own amount, so the array has two elements (per Payment Request Array Structure):

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-bond-invoice",
      "payload": {
        "payment_request": [
          null,
          "lnbcrt5u1pj59wmepp5..."
        ]
      }
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

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.

Recipient resolution

The recipient of the request message 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:

Order kindSolver flagBond on …Recipient
sellslash_buyer = truetakermaker (seller)
sellslash_seller = truemakertaker (buyer)
buyslash_buyer = truemakertaker (seller)
buyslash_seller = truetakermaker (buyer)

Expected client behaviour

  • 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.
  • 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). Do not derive the deadline from receive time.
  • 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.
  • Sign the reply with the non-slashed side's trade key. See Keys management.

Payout confirmations (Phase 3.5)

After the counterparty submits their payout invoice, Mostro acknowledges receipt and later confirms payment with two dedicated actions.

bond-invoice-accepted

Sent by Mostro to the counterparty immediately after successfully receiving and validating their payout bolt11. This confirms Mostro has the invoice on file and is attempting the Lightning payment.

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "bond-invoice-accepted",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "amount": 500,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1
        }
      }
    }
  },
  null,
  null
]

Clients should surface this as a reassurance: "Your bond payout invoice has been received and the payment is being processed."

bond-payout-completed

Sent by Mostro to the counterparty when the Lightning payment to their invoice has been confirmed as settled. The amount in the payload reflects the amount paid.

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "bond-payout-completed",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "amount": 500,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1
        }
      }
    }
  },
  null,
  null
]

Clients should surface this as a terminal success: "Your bond payout of amount Sats has been completed!"

Both actions carry Payload::Order (the SmallOrder context) and are serde-additive: a client that does not recognise them ignores the message and falls back to the add-bond-invoice reply it already handles — no funds at risk.

Failure modes

  • The counterparty never replies before the deadline → no Lightning payment arrives and the share is forfeited; the slashed funds remain with the node.
  • 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.
  • 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.
  • 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.
  • If send_payment retries are exhausted (see payout_max_retries in the info event) the counterparty is re-prompted with a fresh add-bond-invoice request, provided the claim window has not yet elapsed. If the window has passed, the share is forfeited and no further messages are sent.

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.

Bond slashed notification

The bond-slashed action is a notification Mostro sends to a bonded party when their anti-abuse bond has been settled due to a waiting-state timeout. It is a forfeiture notice: the bond HTLC has already been claimed into Mostro's wallet by the time this message is sent.

Scope. This action is emitted on the timeout slash path (scheduler-driven, gated by bond_slash_on_waiting_timeout = "true" in the Mostro info event — see Other events published by Mostro). It is also sent on the dispute-slash path — when a solver slashes a bond via admin-settle or admin-cancel, the slashed party receives a bond-slashed notice in addition to the admin-settled / admin-canceled confirmation.

Direction and trigger

  • Direction: Mostro → the party whose bond was slashed.
  • Trigger: The waiting-state timeout elapsed while the responsible party had not performed their expected trade action (e.g. the seller never paid the hold invoice while in waiting-payment, or the buyer never submitted an invoice in waiting-buyer-invoice), and the operator has configured bond_slash_on_waiting_timeout = "true" in the info event.
  • The amount in the payload is the slashed bond amount in satoshis — not the trade amount.

Wire format

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "bond-slashed",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "status": null,
          "amount": 785,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "created_at": null
        }
      }
    }
  },
  null,
  null
]

The amount field in the embedded SmallOrder is the slashed bond amount (not the original trade amount). For a range-order maker bond, this is the proportional slice amount for the taken sub-order. The status field is null on this message — the SmallOrder carries only the bond context (id, kind, amounts), not an order status; clients should rely on the bond-slashed action itself, and on the separate order-status messages that follow (canceled for the order, plus a republished NIP-33 event when the order returns to the book).

What happens next

After bond-slashed is sent to the responsible party, Mostro also:

  1. Cancels or republishes the order depending on which party was responsible:

    • Maker responsible (e.g. maker-as-seller never paid the hold invoice): the order is canceled (since the maker cannot be trusted to fulfil it). The maker receives the bond-slashed notice followed by a canceled confirmation.
    • Taker responsible (e.g. taker-as-buyer never submitted their invoice): the order is republished to the book as pending so the maker can be matched again. The taker receives bond-slashed followed by canceled. The maker's bond (if any) remains Locked.
  2. Asks the winning counterparty for a payout invoice by sending them an add-bond-invoice message for their share of the slashed bond (governed by bond_slash_node_share_pct — the node retains its share, the rest goes to the counterparty).

Expected client behaviour

  • Surface bond-slashed to the user as an explicit forfeiture notice, not as a generic cancellation. Suggested wording: "Your bond of amount Sats has been forfeited due to a waiting-state timeout."
  • This action is distinct from canceled — clients should not suppress it or merge it into the cancel flow.
  • A canceled message will typically follow for the order itself; clients should handle both.

Note on cancels vs. slashes

A cancel sent by either party before the timeout elapses never triggers bond-slashed. Bonds are always released (never slashed) on user-initiated cancels. This action is emitted by the automated scheduler-driven timeout slash path (when slash_on_waiting_timeout = true is set by the operator) and by a solver's slash directive on a dispute.

Seller pays hold invoice

When the seller is the maker and the order was taken by a buyer, Mostro will send to the seller a message asking to pay the hold invoice, the decrypted content of the message will look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "pay-invoice",
      "payload": {
        "payment_request": [
          {
            "id": "<Order Id>",
            "kind": "sell",
            "status": "waiting-payment",
            "amount": 7851,
            "fiat_code": "VES",
            "fiat_amount": 100,
            "payment_method": "face to face",
            "premium": 1,
            "created_at": 1698937797
          },
          "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e"
        ]
      }
    }
  },
  null,
  null
]

After the hold invoice is paid and the buyer already sent the invoice to receive the sats, Mostro will send a new message to seller with the following decrypted content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "buyer-took-order",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "status": "active",
          "amount": 7851,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": "<Buyer's trade pubkey>",
          "seller_trade_pubkey": "<Seller's trade pubkey>",
          "buyer_invoice": null,
          "created_at": 1698937797
        }
      }
    }
  },
  null,
  null
]

Mostro also send a message to the buyer, this way they can both write to each other in private, this message would look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "hold-invoice-payment-accepted",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "status": "active",
          "amount": 7851,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": "<Buyer's trade pubkey>",
          "seller_trade_pubkey": "<Seller's trade pubkey>",
          "buyer_invoice": null,
          "created_at": 1698937797
        }
      }
    }
  },
  null,
  null
]

If the buyer didn't sent the invoice yet

Mostro send this message to the seller:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "waiting-buyer-invoice",
    "payload": null
  }
}

And this message to the buyer:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "add-invoice",
    "payload": {
      "order": {
        "id": "<Order Id>",
        "kind": "sell",
        "status": "waiting-buyer-invoice",
        "amount": 7851,
        "fiat_code": "VES",
        "fiat_amount": 100,
        "payment_method": "face to face",
        "premium": 1,
        "created_at": 1698937797
      }
    }
  }
}

Now buyer sends the invoice to Mostro:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "add-invoice",
    "payload": {
      "payment_request": [
        null,
        "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e"
      ]
    }
  }
}

And both parties receives each other pubkeys to start a direct conversation.

Fiat sent

After the buyer sends the fiat money to the seller, the buyer should send a message in a NIP-44 direct message (kind 14) to Mostro indicating that the fiat money was sent, message in the first element of the decrypted content would look like this:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "fiat-sent",
    "payload": null
  }
}

When the maker is the buyer on a range order

In most of the cases after complete a range order, a child order needs to be created, the client is rotating keys favoring privacy so Mostro can't know which would be the next trade pubkey of the maker, to solve this the client needs to send trade pubkey and trade index of the child order on the fiat-sent message, the message looks like this:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "fiat-sent",
    "payload": {
      "next_trade": ["<trade pubkey>", <trade index>]
    }
  }
}

Mostro response

Mostro send messages to both parties confirming fiat-sent action and sending again the counterpart pubkey, here an example of the message to the buyer:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "action": "fiat-sent-ok",
    "payload": {
      "peer": {
        "pubkey": "<Seller's trade pubkey>"
      }
    }
  }
}

And here an example of the message from Mostro to the seller:

{
  "order": {
    "version": 2,
    "id": "<Order Id>",
    "pubkey": "<Seller's trade pubkey>",
    "action": "fiat-sent-ok",
    "payload": {
      "peer": {
        "pubkey": "<Buyer's trade pubkey>"
      }
    }
  }
}

Release

After confirming the buyer sent the fiat money, the seller should send a message to Mostro indicating that sats should be delivered to the buyer, the message inside decrypted content will look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "request_id": "123456",
      "action": "release",
      "payload": null
    }
  },
  null,
  null
]

Mostro response

Here an example of the Mostro response to the seller:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "request_id": "123456",
      "action": "hold-invoice-payment-settled",
      "payload": null
    }
  },
  null,
  null
]

And a message to the buyer to let him know that the sats were released:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "released",
      "payload": null
    }
  },
  null,
  null
]

Buyer receives sats

Right after seller releases sats, Mostro will attempt to pay the buyer's Lightning invoice. When the payment succeeds, Mostro will send a message to the buyer indicating that the purchase was completed:

Note: If the Lightning payment fails, Mostro will retry automatically. See Payment Failed for details on how payment failures are handled.

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "purchase-completed",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates the addressable event with the d tag <Order Id> to change the status to success:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "success"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Release a range order

If the order is a range order probably after release a child order would need to be created, Mostro can't know which would be the next trade pubkey, so the client of the maker must send this information, here how the message must look like:

[
  {
    "order": {
      "version": 2,
      "id": "4fd93fc9-e909-4fc9-acef-9976122b5dfa",
      "action": "release",
      "payload": {
        "next_trade": ["<trade pubkey>", <trade index>]
      }
    }
  },
  null,
  null
]

Mostro will send to the maker the newly child order created with the same trade_index received in the payload, if the maker is the buyer the trade_index would be the one sent in the payload of the fiat-sent message by the buyer, the trade_index will be used by the client to get the next key, the message will look like this:

[
  {
    "order": {
      "version": 2,
      "id": "4fd93fc9-e909-4fc9-acef-9976122b5dfa",
      "action": "new-order",
      "trade_index": <trade index>,
      "request_id": "123456",
      "payload": {
        "order": {
          "id": "4fd93fc9-e909-4fc9-acef-9976122b5dfa",
          "kind": "sell",
          "status": "pending",
          "amount": 0,
          "fiat_code": "VES",
          "min_amount": <min amount>,
          "max_amount": <max amount>,
          "fiat_amount": 0,
          "payment_method": "face to face",
          "premium": 1,
          "created_at": 123456789,
          "expires_at": 123456789
        }
      }
    }
  },
  null,
  null
]

Payment Failed

After the seller releases the sats, Mostro attempts to pay the buyer's Lightning invoice. If this payment fails, Mostro sends a payment-failed action to the buyer.

⚠️ Important: payment-failed is an Action (notification), NOT an order Status. The order remains in settled-hold-invoice status while Mostro retries the payment.

When Payment Fails

When Mostro cannot pay the buyer's Lightning invoice, it sends this message to the buyer:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "payment-failed",
      "payload": {
        "payment_failed": {
          "payment_attempts": 3,
          "payment_retries_interval": 5
        }
      }
    }
  },
  null,
  null
]

Payload Fields

FieldTypeDescription
payment_attemptsintegerNumber of retry attempts remaining
payment_retries_intervalintegerMinutes between retry attempts

What Happens Next

  1. Automatic retries: Mostro will automatically retry the payment according to payment_attempts and payment_retries_interval

  2. Order status unchanged: The order remains in settled-hold-invoice status during retries

  3. All retries failed: If all payment attempts fail, Mostro sends add-invoice action requesting the buyer to provide a new Lightning invoice:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "add-invoice",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "amount": 50000,
          ...
        }
      }
    }
  },
  null,
  null
]
  1. Buyer provides new invoice: The buyer must submit a new Lightning invoice for the sats to be delivered

Client Implementation Notes

  • Do NOT create a "PaymentFailed" order status — this action is a notification only
  • Display a message to the buyer explaining the payment failed and retries are in progress
  • When add-invoice is received after failed retries, prompt the buyer to provide a new invoice
  • The sats remain safely locked in escrow (settled-hold-invoice) throughout this process

Flow Diagram

Seller releases sats
        │
        ▼
Order status: settled-hold-invoice
        │
        ▼
Mostro attempts Lightning payment
        │
        ├─── Success ──────────────► Order status: success
        │                            (buyer receives purchase-completed)
        │
        └─── Failure
              │
              ▼
        Action: payment-failed (to buyer)
        Order status: still settled-hold-invoice
              │
              ▼
        Mostro retries (up to payment_attempts times)
              │
              ├─── Retry succeeds ──► Order status: success
              │
              └─── All retries fail
                    │
                    ▼
              Action: add-invoice (to buyer)
              Buyer provides new invoice
                    │
                    ▼
              Mostro pays new invoice ──► Order status: success

User rating

After a successful trade Mostro send a NIP-44 direct message (kind 14) to both parties to let them know they can rate each other, here an example how the message look like:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "rate",
      "payload": null
    }
  },
  null,
  null
]

After a Mostro client receive this message, the user can rate the other party, the rating is a number between 1 and 5, to rate the client must receive user's input and create a new NIP-44 direct message (kind 14) to send to Mostro with this content:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "rate-user",
      "payload": {
        "rating_user": 5 // User input
      }
    }
  },
  null,
  null
]

Confirmation message

If Mostro received the correct message, it will send back a confirmation message to the user with the action rate-received:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "rate-received",
      "payload": {
        "rating_user": 5
      }
    }
  },
  null,
  null
]

Mostro updates the addressable rating event, in this event the d tag will be the user pubkey <Seller's trade pubkey> and looks like this:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702637077,
    "kind": 38384,
    "tags": [
      ["d", "<Seller's trade pubkey>"],
      ["total_reviews", "1"],
      ["total_rating", "2"],
      ["last_rating", "1"],
      ["max_rate", "5"],
      ["min_rate", "1"],
      ["days", "21"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "rating"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Tags

  • d < User trade pubkey >: The trade pubkey of the rated user.
  • total_reviews < Total reviews >: The total number of reviews the user has received.
  • total_rating < Total rating >: The overall reputation rating of the user.
  • last_rating < Last rating >: The rating received in the most recent review.
  • max_rate < Max rate >: The highest rating the user has received.
  • min_rate < Min rate >: The lowest rating the user has received.
  • days < Days >: The number of days since the user's first trade.
  • y < Platform >: Platform identifier tag values. Mostro publishes "mostro" and MAY include a second value with the Mostro instance name from settings.
  • z < Document >: rating.

Cancel Order

A user can cancel an order created by himself and with status pending sending action cancel, the decrypted content of the message will look like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cancel",
      "payload": null
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Mostro's response

Mostro will send a message with action cancel confirming the order was canceled, here an example of decrypted content of the message:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "canceled",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to canceled:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "canceled"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Bond race during take

Multiple takers may simultaneously attempt to take the same order: each creates their own bond hold invoice (a Requested bond row) and whichever HTLC is accepted first by LND wins. When a bond locks, all other concurrent Requested bonds on the same order are released and their takers each receive Action::Canceled.

A client that sent take-buy / take-sell and is waiting for pay-bond-invoice may therefore receive canceled instead — meaning another taker's bond locked first. Surface this clearly to the user, e.g. "Order was taken by another user before you locked the bond." Do not silently retry the take — the order may no longer be available.

Cancel during waiting-taker-bond

An order remains in the daemon-internal waiting-taker-bond state (NIP-33 s tag still pending) while one or more taker bonds are outstanding. Cancels during this window are routed differently from cancels on pending orders.

Taker self-cancel

A taker who has a Requested (unpaid) bond on an order may cancel it by sending cancel. Mostro releases only that taker's bond (LND invoice cancelled, funds returned). Other concurrent takers' bonds are unaffected and continue racing.

  • If the cancelling taker was the only prospective taker, the order drops back to pending and is visible on the order book again.
  • If other takers' bonds are still Requested, the order remains in waiting-taker-bond with those bonds still racing.

The maker is not notified; the NIP-33 order event stays pending throughout.

Maker cancel

The maker can cancel the order at any point before trade flow starts (i.e. while in pending or waiting-taker-bond). Sending cancel releases all concurrent taker bonds on the order, notifies each prospective taker with canceled, transitions the order to canceled, and publishes the updated NIP-33 event.

Cancel cooperatively

A user can cancel an active order, but will need the counterparty to agree, let's look at an example where the seller initiates a cooperative cancellation:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cancel",
      "payload": null
    }
  },
  null,
  null
]

Mostro will send this message to the seller:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cooperative-cancel-initiated-by-you",
      "payload": null
    }
  },
  null,
  null
]

And this message to the buyer:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cooperative-cancel-initiated-by-peer",
      "payload": null
    }
  },
  null,
  null
]

The buyer can accept the cooperative cancellation sending this message:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cancel",
      "payload": null
    }
  },
  null,
  null
]

And Mostro will send this message to both parties:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "cooperative-cancel-accepted",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates the addressable event with d tag <Order Id> to change the status to canceled:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1702549437,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "canceled"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["network", "mainnet"],
      ["layer", "lightning"],
      ["expiration", "1719391096"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Peer-to-peer Chat

To communicate directly, the buyer and the seller do not use the Message scheme explained here, because this communication excludes the Mostro daemon.

Messages are not gift wrapped. Each party publishes a kind 14 event signed with a key derived from the ECDH secret shared by the two trade keys, carrying a NIP-44 encrypted kind 1 event signed with the sender's trade key. No ephemeral keys are involved.

Why not NIP-59

Earlier revisions of this document used a simplified NIP-59 gift wrap: the outer event was signed with a random ephemeral key and p-tagged to the shared pubkey. That construction was replaced because it is vulnerable and buys nothing.

It buys nothing. In standard NIP-59 the ephemeral key matters because the p tag points at the recipient's real identity key: without it, an observer would see "identity X writes to identity Y". In this protocol the p tag already points at a shared key that is anonymous and unique per order — it is nobody's identity. An observer saw ephemeral → shared; it now sees shared → shared. Neither form ever exposes a trade key, and both group events identically. No privacy is lost.

It is vulnerable. The shared pubkey travels in clear text in the p tag of every event. Any observer scraping relays for these events harvests the shared pubkeys of every active conversation. Because the outer event is signed by a fresh random key, an attacker's events are indistinguishable from genuine ones until the recipient has already downloaded and attempted to decrypt them — one ECDH per event. There is nothing cheap to filter on, and no author to rate-limit: by design, every event appears to come from a different sender.

That enables a cheap, anonymous, and profitable attack. Flooding the shared pubkey of a trade that is waiting on a counterparty can exhaust the victim's client until the trade's deadline passes, and the attacker does not even need to be a party to that trade.

Signing the outer event with the shared key removes the attack at its root. The shared pubkey is still observable, but producing a valid event requires the shared private scalar, which can only be computed from one of the two trade private keys. A third party is cryptographically unable to publish into the conversation, so clients can filter on the author and the relay discards everything else before it ever reaches the client.

The only party who can still flood is the counterparty, who is a single, stable, attributable author — see Client security requirements.

Shared Key

Messages are not addressed to the counterparty's trade pubkey but to a key known only to both parties.

We use Elliptic Curve Diffie-Hellman (ECDH) over the two trade keys to obtain a shared secret:

Alice                            Bob
-----                            -----
Private Key: a                   Private Key: b
Public Key: A = a * G            Public Key: B = b * G
   (G is the curve’s base point)

1. Alice sends A to Bob  ----->  Bob receives A
2. Bob sends B to Alice  <-----  Alice receives B

Alice computes:                  Bob computes:
Shared Secret = a * B            Shared Secret = b * A
              = a * (b * G)      = b * (a * G)
              = ab * G           = ba * G
              = Same Shared Secret!

Because trade keys are derived per order, the shared secret is unique per order and carries no link to any long-lived identity.

Key derivation

The shared secret is not used directly. Two keys are derived from it with domain separation, using HKDF-SHA256 (RFC 5869) with an empty salt:

shared  = ECDH(own_trade_privkey, peer_trade_pubkey)          // 32 bytes

K_conv  = HKDF-SHA256(ikm = shared, info = "mostro:chat:conv:v1", L = 32)
K_sign  = HKDF-SHA256(ikm = shared, info = "mostro:chat:sign:v1", L = 32)

Both outputs are interpreted as secp256k1 secret keys. In the negligible event that an output is not a valid secret key (zero, or greater than or equal to the curve order), implementations MUST re-derive by appending a single incrementing byte to info until a valid key is produced.

KeyRoleWho holds it
K_convNIP-44 encryption and decryption of the payload. pub(K_conv) is the conversation address carried in the p tag.Both parties. Disclosed to a solver during a dispute.
K_signSigns the outer event. pub(K_sign) is the author every client filters on.Both parties only. Never disclosed.

Separating the two is what makes a read-only dispute disclosure possible: a solver given K_conv can read the entire conversation but cannot publish into it. See Dispute disclosure.

Event structure

1. Inner event

A kind 1 event with the message, signed by the sender's trade key, timestamped at the moment the message is sent:

{
  "id": "<Event Id>",
  "pubkey": "<Index N pubkey (trade key)>",
  "kind": 1,
  "created_at": 1691518405,
  "content": "Let’s reestablish the peer-to-peer nature of Bitcoin!",
  "tags": [],
  "sig": "<Index N (trade key) signature>"
}

The inner signature is the only authentication of the sender. Both parties hold K_sign, so the outer signature proves the event came from the conversation but not which side wrote it.

2. Outer event

The JSON-encoded inner event is NIP-44 encrypted under K_conv and placed in the content of a kind 14 event, p-tagged to pub(K_conv) and signed with K_sign:

{
  "id": "<Event Id>",
  "pubkey": "<pub(K_sign)>",
  "kind": 14,
  "created_at": 1691518405,
  "content": "<NIP-44 encrypted inner event>",
  "tags": [["p", "<pub(K_conv)>"]],
  "sig": "<K_sign signature>"
}

The outer created_at MUST be the real time the message is sent, and MUST equal the inner created_at up to a small clock tolerance. The timestamp tweaking that NIP-59 recommends does not apply here: it would break since-based synchronization, and with no identity key exposed there is nothing for time analysis to correlate to.

Encrypting payloads

Encryption follows NIP-44 v2 over the JSON-encoded inner event, using K_conv as both sides of the key exchange:

conversation_key = NIP-44_conversation_key(K_conv_privkey, pub(K_conv))

This is self-encryption: the NIP-44 conversation key is derived from ECDH(k, k·G), which is deterministic and computed identically by both parties, and by anyone holding K_conv. It is an unusual but valid use of NIP-44 — implementations MUST NOT reject an encryption or decryption call where the secret key and the public key belong to the same keypair. This is the most likely point of divergence between implementations; verify against the test vector.

Disambiguating from protocol v2 messages

Kind 14 is also used by the protocol v2 transport for client↔daemon messages. The two never collide, because the author differs:

TrafficAuthorp tag
Client → daemonSender's trade keyMostro node pubkey
Daemon → clientMostro node pubkeyRecipient's trade key
Peer chatpub(K_sign)pub(K_conv)

Clients MUST route incoming kind 14 events by author:

  • author is the active Mostro node pubkey → daemon message;
  • author is pub(K_sign) of an active conversation → peer chat;
  • anything else → ignore.

Routing by p tag alone is not sufficient, and reintroduces the vulnerability described above.

Client security requirements

Subscription

Clients MUST subscribe with authors = [pub(K_sign)].

This single rule is what eliminates third-party flooding: the relay drops every event that is not signed by the conversation key, so junk never reaches the client and costs it nothing. A client that filters only by #p is fully exposed to the attack this design exists to prevent, even though it will appear to work correctly.

Clients MUST also bound the backlog: subscribe with since set to the last processed timestamp, persisted locally, together with a limit. An unbounded subscription re-downloads the entire stored history on every reconnection and every application start, which turns a one-off flood into permanent damage that survives reinstalling the application.

The persisted since cursor MUST NOT be advanced beyond the client's own clock. A counterparty signs both events, so it can set both timestamps to the same far-future value and satisfy the relative check in step 13. If the client then stores that value as its cursor, its own subscription filters out every honest message that follows, and the conversation stays dead until that future date — a permanent denial of service from a single message, surviving restarts. Clamping the cursor to min(accepted_timestamp, local_now), together with the absolute bound in step 3, closes this.

Validation order

Each incoming event MUST be validated cheapest-check-first, so that an abusive peer cannot force expensive work:

  1. Author is pub(K_sign) — otherwise discard.
  2. p tag: exactly one, equal to pub(K_conv) — otherwise discard. See below.
  3. Absolute timestamp bound: outer.created_at is not further into the future than the client's tolerance for clock skew (60 seconds is a reasonable default), measured against the client's own clock — otherwise discard. Timestamps in the past are always acceptable, since offline catch-up is legitimate.
  4. Size is within the client's limit (64 KiB is a reasonable default) — otherwise discard.
  5. Outer event id has not been seen before (bounded LRU) — otherwise discard.
  6. Rate-limit budget for this conversation is available — otherwise discard.
  7. Outer signature verifies.
  8. Only now, NIP-44 decrypt with K_conv.
  9. Inner signature verifies — this is the sender authentication and MUST NOT be skipped. Reading the inner pubkey field without verifying the signature accepts forged senders.
  10. Inner pubkey is the buyer's or the seller's trade key for this order — otherwise discard. No other signer is accepted, including a dispute solver.
  11. Inner kind is 1 — otherwise discard.
  12. Inner event id has not been seen before, checked against durable state — otherwise discard.
  13. Relative timestamp bound: |inner.created_at − outer.created_at| is within the same tolerance — otherwise discard.

Steps 1 through 6 are all reachable without any cryptographic work, and steps 2 and 3 in particular cost a tag comparison and an integer comparison.

The p tag is part of the contract

Producers MUST emit exactly one p tag, set to pub(K_conv), and MUST NOT allow application-supplied tags to add or override it. Recipients MUST reject anything else.

This is not hygiene. A recipient filters by author, so a message carrying a wrong or missing p tag still reaches them and decrypts correctly — but a dispute solver locates the conversation by querying #p = pub(K_conv). Without this rule a party could send messages that their counterparty sees normally yet are invisible in the transcript the solver retrieves, letting them shape the evidence after the fact.

Replay protection

Both parties hold K_sign, so either can re-publish a previously received inner event inside a fresh wrapper. The inner signature is genuine, so it verifies — a peer could reinject an old "I sent the fiat" message, or reshape the transcript a solver will read during a dispute.

Deduplication on the inner event id (step 12) is what rejects this, in every case: the inner id is a hash over the sender's pubkey, content and created_at, so a re-wrapped message keeps the id it had the first time. The relative timestamp bound (step 13) is not the defence — a peer who re-wraps within the tolerance window would satisfy it. What that bound does is limit how far back deduplication state has to reach, by making a re-wrap detectable as stale once it falls outside the window.

Because of that, dedup state on the inner id MUST be durable, not a bounded in-memory cache: an entry evicted from an LRU makes the corresponding message replayable again. In practice this is free — clients already persist message history in order to advance the since cursor, so retaining each accepted inner event id alongside its message is one identifier per stored message. The bounded LRU in step 5 is a different thing: it applies to the outer id, is only a cheap pre-decryption filter against duplicate relay deliveries, and carries no security requirement.

Retention MUST cover at least every timestamp the client will still accept, which follows from the since cursor: an event older than the cursor is not requested, and one newer is inside the retained range.

Rate limiting

The counterparty is the only party who can flood, and is now a single stable author. Clients SHOULD apply a token bucket per conversation — on the order of 30 messages per minute sustained with a burst of 60 — and discard excess events before decrypting them.

On sustained violation, a client SHOULD mark the conversation as flooded, stop processing it, and inform the user, while leaving the trade fully operational.

Clients SHOULD also cap the number of messages and total bytes stored per trade.

Isolation

Chat processing MUST NOT be able to block, delay, or crash the order state machine, the daemon transport, or the ability to open a dispute. Chat must run on its own bounded queue; under pressure a client drops chat, never trade traffic. Processing the chat backlog MUST NOT block application startup.

This is the invariant that prevents any future flaw in this channel from costing a user their funds: a chat that stops working is an inconvenience, a trade that cannot be disputed is a loss.

Evidence

A flood is attributable to pub(K_sign), and every accepted message to a trade key. Clients SHOULD retain a bounded sample and counters, which are usable as evidence in a dispute.

Presentation

Clients SHOULD order messages by the validated inner created_at. Because that value is chosen by the sender, it MUST NOT be trusted beyond the tolerance enforced in step 11.

Dispute disclosure

Either party may voluntarily disclose K_conv to the solver, who can then decrypt the whole conversation and verify every inner signature to establish who wrote what.

Because K_sign is never disclosed, this grant is read-only: the solver cannot publish into the conversation. The chat therefore keeps only the messages exchanged by the two parties, and a solver who needs to talk to them does so privately with each side, outside this channel. Even a solver who obtained K_sign could not impersonate a party, since forging a message would require a trade private key.

pub(K_sign) is public data — it is the author of every event — and MAY be given to the solver alongside K_conv so they can filter efficiently by author. It is a locator, not a secret.

Relay considerations

Kind 14 falls outside the range NIP-01 defines as regular (1000 ≤ n < 10000), and NIP-17 specifies kind 14 as an unsigned rumor that is never published directly. Relay behaviour for a signed, published kind 14 is therefore not guaranteed by any NIP.

Offline delivery depends on relays storing these events. Operators and client implementers MUST verify empirically that the relays they target store and serve kind 14, and MUST NOT assume it.

Proof of work per NIP-13 remains optional. It is markedly less relevant than it was under gift wrap: with a stable author, relays can apply their usual per-author rate limiting, which was impossible when every event carried a fresh ephemeral key.

Migration

This is a breaking wire change affecting mostrod, the mobile clients, mostro-cli, and any solver tooling. Implementations SHOULD accept both the gift-wrapped form and the form specified here during a transition window, and MUST fix a deprecation date after which only this form is produced. Trades already in flight at the cutover keep the format they started with.

Code Example

Rust

// Leading `::` selects the `hkdf` crate: `nostr_sdk::prelude` also exports a
// module by that name, so a plain `use hkdf::Hkdf` is ambiguous.
use ::hkdf::Hkdf;
use nostr::util::generate_shared_key;
use nostr_sdk::prelude::*;
use sha2::Sha256;

/// HKDF `info` strings. Changing either value changes the wire format.
const CONV_INFO: &[u8] = b"mostro:chat:conv:v1";
const SIGN_INFO: &[u8] = b"mostro:chat:sign:v1";

/// Tolerance for clock skew, applied both between the inner and outer
/// `created_at` and against the recipient's own clock.
const MAX_CLOCK_SKEW_SECS: u64 = 60;

/// Upper bound on the encrypted payload, enforced before decrypting.
const MAX_CONTENT_BYTES: usize = 64 * 1024;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Per-order trade keys.
    let alice_trade =
        Keys::parse("548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a")?;
    let bob_trade =
        Keys::parse("f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943")?;

    // Both sides derive the same conversation and signing keys.
    let (alice_conv, alice_sign) = derive_chat_keys(&alice_trade, &bob_trade.public_key())?;
    let (bob_conv, bob_sign) = derive_chat_keys(&bob_trade, &alice_trade.public_key())?;
    assert_eq!(alice_conv.public_key(), bob_conv.public_key());
    assert_eq!(alice_sign.public_key(), bob_sign.public_key());

    println!("Conversation pubkey (p tag): {}", alice_conv.public_key());
    println!("Signing pubkey (author):     {}", alice_sign.public_key());

    // Alice sends a message.
    let message = "Let’s reestablish the peer-to-peer nature of Bitcoin!";
    let outer = mostro_wrap(&alice_trade, &alice_conv, &alice_sign, message, vec![]).await?;
    println!("Outer event: {outer:#?}");

    // Bob subscribes with `authors = [pub(K_sign)]` and validates what arrives.
    // Only the two trade keys of this order are accepted as inner signers.
    let allowed = [alice_trade.public_key(), bob_trade.public_key()];
    let inner = mostro_unwrap(
        &bob_conv,
        &bob_sign.public_key(),
        &allowed,
        &outer,
        Timestamp::now(),
    )?;
    println!("Inner event: {inner:#?}");
    assert_eq!(inner.pubkey, alice_trade.public_key());

    Ok(())
}

/// Derives the domain-separated conversation and signing keys for one order.
///
/// Both parties reach the same pair: the ECDH secret is symmetric, and HKDF is
/// deterministic.
///
/// # Arguments
/// - `own_trade`: this party's trade keys for the order.
/// - `peer_trade`: the counterparty's trade pubkey for the order.
///
/// # Returns
/// `(K_conv, K_sign)`.
pub fn derive_chat_keys(
    own_trade: &Keys,
    peer_trade: &PublicKey,
) -> Result<(Keys, Keys), Box<dyn std::error::Error>> {
    let shared = generate_shared_key(own_trade.secret_key(), peer_trade)?;
    let hkdf = Hkdf::<Sha256>::new(None, &shared);

    let derive = |info: &[u8]| -> Result<Keys, Box<dyn std::error::Error>> {
        // Retry with a counter byte on the negligible chance that the output
        // is not a valid secp256k1 secret key.
        for counter in 0u16..=255 {
            let mut labelled = info.to_vec();
            if counter > 0 {
                labelled.push(counter as u8);
            }
            let mut out = [0u8; 32];
            hkdf.expand(&labelled, &mut out)
                .map_err(|e| format!("HKDF expand failed: {e}"))?;
            if let Ok(sk) = SecretKey::from_slice(&out) {
                return Ok(Keys::new(sk));
            }
        }
        Err("HKDF failed to produce a valid secret key".into())
    };

    Ok((derive(CONV_INFO)?, derive(SIGN_INFO)?))
}

/// Builds the outer kind 14 event carrying an encrypted, trade-key-signed
/// kind 1 event.
///
/// The inner event authenticates the sender; the outer event authenticates the
/// conversation and is what clients filter on.
///
/// # Arguments
/// - `sender_trade`: the sender's trade keys, used to sign the inner event.
/// - `conv`: `K_conv`, used to encrypt and as the `p` tag.
/// - `sign`: `K_sign`, used to sign the outer event.
/// - `message`: the plaintext message.
/// - `extra_tags`: additional tags for the outer event.
pub async fn mostro_wrap(
    sender_trade: &Keys,
    conv: &Keys,
    sign: &Keys,
    message: &str,
    extra_tags: Vec<Tag>,
) -> Result<Event, Box<dyn std::error::Error>> {
    // One timestamp for both events: the real moment the message is sent.
    // Recipients reject a mismatch, which is what defeats replays.
    let now = Timestamp::now();

    let inner = EventBuilder::text_note(message)
        .custom_created_at(now)
        .build(sender_trade.public_key())
        .sign(sender_trade)
        .await?;

    // NIP-44 self-encryption: K_conv is both sides of the key exchange.
    let content = nip44::encrypt(
        conv.secret_key(),
        &conv.public_key(),
        inner.as_json(),
        nip44::Version::V2,
    )?;

    // Exactly one `p` tag, ours. A caller-supplied one could hide the message
    // from the `#p` query a dispute solver uses to rebuild the transcript.
    if extra_tags
        .iter()
        .any(|t| t.kind() == TagKind::p())
    {
        return Err("extra_tags must not contain a p tag".into());
    }
    let mut tags = vec![Tag::public_key(conv.public_key())];
    tags.extend(extra_tags);

    let outer = EventBuilder::new(Kind::PrivateDirectMessage, content)
        .tags(tags)
        .custom_created_at(now)
        .sign_with_keys(sign)?;

    Ok(outer)
}

/// Validates an incoming outer event and returns the inner event.
///
/// Every check here is mandatory; see "Client security requirements".
///
/// Two of the thirteen steps are the caller's, because they need state this
/// function does not own: the **rate-limit budget** and the **event-id
/// caches** (a bounded LRU on the outer id, and durable storage for the inner
/// id). A caller that skips the durable inner-id check accepts replays.
///
/// The caller is also expected to have applied a byte limit when the raw event
/// was read off the wire; the payload bound re-checked here only caps the
/// decryption work.
///
/// # Arguments
/// - `conv`: `K_conv`, used to decrypt.
/// - `sign_pubkey`: `pub(K_sign)` of this conversation.
/// - `allowed_signers`: the buyer's and the seller's trade pubkeys.
/// - `outer`: the received kind 14 event.
/// - `now`: the recipient's current time, for the absolute timestamp bound.
pub fn mostro_unwrap(
    conv: &Keys,
    sign_pubkey: &PublicKey,
    allowed_signers: &[PublicKey],
    outer: &Event,
    now: Timestamp,
) -> Result<Event, Box<dyn std::error::Error>> {
    // A third party cannot produce a valid signature for this author, so this
    // check is what makes flooding impossible. Relays enforce it too, via the
    // `authors` filter; clients re-check it locally.
    if outer.pubkey != *sign_pubkey {
        return Err("outer event is not authored by the conversation signing key".into());
    }
    if outer.kind != Kind::PrivateDirectMessage {
        return Err("outer event is not kind 14".into());
    }

    // Exactly one `p` tag, addressing this conversation. Anything else could be
    // a message engineered to stay out of a dispute solver's `#p` query.
    let mut p_tags = outer.tags.iter().filter(|t| t.kind() == TagKind::p());
    match (p_tags.next().and_then(|t| t.content()), p_tags.next()) {
        (Some(pk), None) if pk == conv.public_key().to_hex() => {}
        _ => return Err("outer event must carry exactly one p tag for this conversation".into()),
    }

    // Absolute bound against our own clock. Without it a counterparty can date
    // both events far in the future — they agree with each other, so the
    // relative check below passes — and poison the `since` cursor, silencing
    // the conversation until that date. The past is unbounded: catching up
    // after being offline is legitimate.
    if outer.created_at.as_secs() > now.as_secs().saturating_add(MAX_CLOCK_SKEW_SECS) {
        return Err("outer event is dated too far in the future".into());
    }

    if outer.content.len() > MAX_CONTENT_BYTES {
        return Err("encrypted payload exceeds the accepted size".into());
    }

    outer.verify()?;

    let decrypted = nip44::decrypt(conv.secret_key(), &conv.public_key(), &outer.content)?;
    let inner = Event::from_json(&decrypted)?;

    // The only authentication of who wrote the message: both parties can sign
    // the outer event, so it cannot tell the two sides apart.
    inner.verify()?;
    if !allowed_signers.contains(&inner.pubkey) {
        return Err("inner event is signed by a key that is not a party to this order".into());
    }
    if inner.kind != Kind::TextNote {
        return Err("inner event is not kind 1".into());
    }

    // Bounds how far back the caller's durable inner-id dedup has to reach: a
    // re-wrap older than the tolerance is stale and rejected here, while one
    // inside the window is caught by that dedup, never by this check.
    let skew = inner.created_at.as_secs().abs_diff(outer.created_at.as_secs());
    if skew > MAX_CLOCK_SKEW_SECS {
        return Err("inner and outer timestamps disagree — stale re-wrap".into());
    }

    Ok(inner)
}

Test vector

Derived from the trade keys used above:

Alice trade private key: 548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a
Alice trade public key:  000053c3b4773182e7c4c1b72b272d34be01bf4414a6a25c998977c516a46a01
Bob trade private key:   f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943
Bob trade public key:    000009ae5cff9f6ba9b05159ec5ed58c187f5882ea77c81ed5dd19163272a5d7

ECDH shared secret:      def6633a53d07d1e829484c4d4bdbbeed2f4b14c21743e63871c174338e39475

pub(K_conv), the p tag:  bceb1cd2a8e98ee9729122a1693edcc39c3ace04582ff96a26705c5e4078a6f2
pub(K_sign), the author: 1dba04571059183f76b148119cfa6f8004dad30cb4e810180a6df17386a7f0b4

Both parties derive that pair from their own side of the ECDH, and these values are what the example above prints. NIP-44 v2 uses a random nonce, so ciphertexts are not reproducible: check the derived pubkeys against this vector first, then verify a round trip through mostro_wrap / mostro_unwrap.

Reference implementation

mostro-chat is a terminal (TUI) chat client that implements this specification end to end and serves as the reference implementation: shared-key derivation with domain separation, the kind 14 outer / kind 1 inner event structure, NIP-44 self-encryption under K_conv, and the full validation order described in Client security requirements. Its code is kept up to date with this document — when in doubt about how the P2P chat should behave, consult that repository alongside the test vector.

Dispute

A user can start a dispute in an order with status active or fiat-sent sending action dispute, here is an example where the seller initiates a dispute:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "dispute",
      "payload": null
    }
  },
  "<index N signature of the sha256 hash of the serialized first element of content>",
  ["<index 0 pubkey (identity key)>", "<index 0 identity proof signature>"]
]

Mostro response

Mostro will send this message to the seller:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "dispute-initiated-by-you",
      "payload": {
        "dispute": "<Dispute Id>"
      }
    }
  },
  null,
  null
]

And here is the message to the buyer:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "dispute-initiated-by-peer",
      "payload": {
        "dispute": "<Dispute Id>"
      }
    }
  },
  null,
  null
]

Mostro will not update the addressable event with d tag <Order Id> to change the status to dispute, this is because the order is still active, the dispute is just a way to let the admins and the other party know that there is a problem with the order.

Mostro sends an addressable event to show the dispute

Here is an example of the event sent by Mostro:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703016565,
    "kind": 38386,
    "tags": [
      ["d", "<Dispute Id>"],
      ["s", "initiated"],
      ["initiator", "seller"], // seller or buyer
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "dispute"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Mostro admin will see the dispute and can take it using the dispute Id from d tag, here how should look the message sent by the admin:

[
  {
    "dispute": {
      "version": 2,
      "id": "<Dispute Id>",
      "action": "admin-take-dispute",
      "payload": null
    }
  },
  null,
  null
]

Mostro will send a confirmation message to the admin with the order details:

[
  {
    "dispute": {
      "version": 2,
      "id": "<Dispute Id>",
      "action": "admin-took-dispute",
      "payload": {
        "order": {
          "id": "<Order Id>",
          "kind": "sell",
          "status": "active",
          "amount": 7851,
          "fiat_code": "VES",
          "fiat_amount": 100,
          "payment_method": "face to face",
          "premium": 1,
          "buyer_trade_pubkey": "<Buyer's trade pubkey>",
          "seller_trade_pubkey": "<Seller's trade pubkey>",
          "buyer_invoice": "lnbcrt11020n1pjcypj3pp58m3d9gcu4cc8l3jgkpfn7zhqv2jfw7p3t6z3tq2nmk9cjqam2c3sdqqcqzzsxqyz5vqsp5mew44wzjs0a58d9sfpkrdpyrytswna6gftlfrv8xghkc6fexu6sq9qyyssqnwfkqdxm66lxjv8z68ysaf0fmm50ztvv773jzuyf8a5tat3lnhks6468ngpv3lk5m7yr7vsg97jh6artva5qhd95vafqhxupyuawmrcqnthl9y",
          "created_at": 1698870173
        }
      }
    }
  },
  null,
  null
]

Then mostrod send messages to each trade participant, the buyer and seller for them to know the pubkey of the admin who took the dispute, that way the client can start listening events from that specific pubkey, by default clients should discard any messages received from any pubkey different than Mostro node or dispute solver, the message looks like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-took-dispute",
      "payload": {
        "peer": {
          "pubkey": "<Solver's pubkey>"
        }
      }
    }
  },
  null,
  null
]

Also Mostro will broadcast a new addressable dispute event to update the dispute status to in-progress:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703020540,
    "kind": 38386,
    "tags": [
      ["d", "<Dispute Id>"],
      ["s", "in-progress"],
      ["initiator", "seller"], // seller or buyer
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "dispute"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Dispute Chat

The dispute chat uses the same shared key encryption scheme as the Peer-to-peer Chat. Instead of computing a shared key between buyer and seller, each party computes an independent shared key with the admin who took the dispute.

Establishing the shared key

When an admin takes a dispute, Mostro sends an admin-took-dispute message to each party (buyer and seller) containing the admin's pubkey:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-took-dispute",
      "payload": {
        "peer": {
          "pubkey": "<Admin's pubkey>"
        }
      }
    }
  },
  null,
  null
]

Upon receiving this message, the client computes the shared key using ECDH:

Shared Key = ECDH(tradeKey.private, adminPubkey)

The admin computes the same shared key from their side:

Shared Key = ECDH(adminPrivateKey, tradeKey.public)

Each party (buyer and seller) has its own independent shared key with the admin. A session can have both a peer shared key (for the P2P chat) and an admin shared key (for the dispute chat) simultaneously.

That shared secret is then split into K_conv and K_sign exactly as in Key derivation, using the same HKDF info strings:

shared  = ECDH(tradeKey.private, adminPubkey)     // admin: ECDH(adminPrivateKey, tradeKey.public)

K_conv  = HKDF-SHA256(ikm = shared, info = "mostro:chat:conv:v1", L = 32)
K_sign  = HKDF-SHA256(ikm = shared, info = "mostro:chat:sign:v1", L = 32)

Sending and receiving messages

Messages use the same envelope as the Peer-to-peer Chat: a kind 1 inner event signed by the sender's own key, NIP-44 encrypted under K_conv, carried in a kind 14 event signed with K_sign and p-tagged to pub(K_conv). There is no gift wrap and no ephemeral key.

{
  "id": "<Event Id>",
  "pubkey": "<pub(K_sign) of the admin shared key>",
  "kind": 14,
  "created_at": 1703021488,
  "content": "<NIP-44 encrypted inner event>",
  "tags": [["p", "<pub(K_conv) of the admin shared key>"]],
  "sig": "<K_sign signature>"
}

Unlike the peer chat, the admin is a legitimate writer here — this channel is how a solver talks to each party privately. The accepted inner signers are therefore that party's trade key and the admin's pubkey, and nothing else.

Subscribing to messages

Clients subscribe by author, not by p tag:

{
  "kinds": [14],
  "authors": ["<pub(K_sign) of the admin shared key>"]
}

Only the party and the admin can compute K_sign, so no third party can publish into this conversation and the relay discards everything else. Filtering by #p instead would let anyone who has observed that tag flood the channel; see Why not NIP-59.

Client requirements

Every rule in Client security requirements applies unchanged — mandatory author filter, bounded backlog with a cursor never advanced past the local clock, the cheapest-check-first validation order, durable inner-id deduplication, rate limiting, and the isolation invariant — with one substitution: the accepted inner signers are the party's trade key and the admin's pubkey, the latter learned from the admin-took-dispute message above.

Listing Disputes

Mostro publishes new disputes with event kind 38386 and status initiated:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703016565,
    "kind": 38386,
    "tags": [
      ["d", "<Dispute Id>"],
      ["s", "initiated"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "dispute"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Clients can query these events by nostr event kind 38386, nostr event author, dispute status (s), type (z)

Settle order

An admin can settle an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an order message to Mostro with action admin-settle with the id of the order like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-settle",
      "payload": null
    }
  },
  null,
  null
]

Bond resolution payload

When solving a dispute, the admin can optionally slash one or both parties' bonds independently from the trade outcome (settle vs. cancel). To do this, the payload is set to a bond_resolution object with two booleans, slash_seller and slash_buyer:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-settle",
      "payload": {
        "bond_resolution": {
          "slash_seller": false,
          "slash_buyer": true
        }
      }
    }
  },
  null,
  null
]

Accepted combinations on admin-settle:

  • { "slash_seller": false, "slash_buyer": false } — settle without slashing any bond
  • { "slash_seller": true, "slash_buyer": false } — settle and slash the seller's bond
  • { "slash_seller": false, "slash_buyer": true } — settle and slash the buyer's bond
  • { "slash_seller": true, "slash_buyer": true } — settle and slash both bonds
  • payload: null — legacy clients; interpreted server-side as "no slash"

bond_resolution is only valid on admin-settle and admin-cancel; it is rejected on every other action.

Whenever a side is slashed and the operator has configured slash_node_share_pct < 1.0, the non-slashed counterparty is subsequently asked to provide a Lightning invoice for their share of the bond via Bond payout invoice.

Each slashed party is also sent a bond-slashed forfeiture notice for the slashed bond amount, in addition to the admin-settled confirmation below.

Mostro response

Mostro will send this message to the both parties buyer/seller and to the admin:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-settled",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates addressable dispute event

Mostro updates the addressable dispute event with status settled:

[
  "EVENT",
  "RAND",
  {
    "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703274022,
    "kind": 38386,
    "tags": [
      ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"],
      ["s", "settled"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "dispute"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Payment of the buyer's invoice

At this point Mostro is trying to pay the buyer's invoice, right after complete the payment Mostro will update the status of the order addressable event to success:

[
  "EVENT",
  "RAND",
  {
    "id": "6170892aca6a73906142e58a9c29734d49b399a3811f6216ce553b4a77a8a11e",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703274032,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "success"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Cancel order

An admin can cancel an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an order message to Mostro with action admin-cancel with the id of the order like this:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-cancel",
      "payload": null
    }
  },
  null,
  null
]

Bond resolution payload

When solving a dispute, the admin can optionally slash one or both parties' bonds independently from the trade outcome (settle vs. cancel). To do this, the payload is set to a bond_resolution object with two booleans, slash_seller and slash_buyer:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-cancel",
      "payload": {
        "bond_resolution": {
          "slash_seller": true,
          "slash_buyer": false
        }
      }
    }
  },
  null,
  null
]

Accepted combinations on admin-cancel:

  • { "slash_seller": false, "slash_buyer": false } — cancel without slashing any bond
  • { "slash_seller": true, "slash_buyer": false } — cancel and slash the seller's bond
  • { "slash_seller": false, "slash_buyer": true } — cancel and slash the buyer's bond
  • { "slash_seller": true, "slash_buyer": true } — cancel and slash both bonds
  • payload: null — legacy clients; interpreted server-side as "no slash"

bond_resolution is only valid on admin-cancel and admin-settle; it is rejected on every other action.

Whenever a side is slashed and the operator has configured slash_node_share_pct < 1.0, the non-slashed counterparty is subsequently asked to provide a Lightning invoice for their share of the bond via Bond payout invoice.

Each slashed party is also sent a bond-slashed forfeiture notice for the slashed bond amount, in addition to the admin-canceled confirmation below.

Mostro response

Mostro will send this message to the both parties buyer/seller and to the admin:

[
  {
    "order": {
      "version": 2,
      "id": "<Order Id>",
      "action": "admin-canceled",
      "payload": null
    }
  },
  null,
  null
]

Mostro updates addressable events

Mostro will publish two addressable events, one for the order to update the status to canceled, this means that the hold invoice was canceled and the seller's funds were returned:

[
  "EVENT",
  "RAND",
  {
    "id": "3d74ce3f10096d163603aa82beb5778bd1686226fdfcfba5d4c3a2c3137929ea",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703260182,
    "kind": 38383,
    "tags": [
      ["d", "<Order Id>"],
      ["k", "sell"],
      ["f", "VES"],
      ["s", "canceled"],
      ["amt", "7851"],
      ["fa", "100"],
      ["pm", "face to face"],
      ["premium", "1"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "order"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

The second event updates the addressable dispute event with status seller-refunded:

[
  "EVENT",
  "RAND",
  {
    "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1703274022,
    "kind": 38386,
    "tags": [
      ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"],
      ["s", "seller-refunded"],
      ["y", "mostro", "[Mostro instance name]"],
      ["z", "dispute"]
    ],
    "content": "",
    "sig": "<Mostro's signature>"
  }
]

Admin add solver

Solvers are users appointed by the Mostro administrator and are responsible for helping resolve disputes.

The administrator can add or remove them at any time.

The administrator can also solve disputes.

To add a solver the admin sends an order message to Mostro with action admin-add-solver.

Payload format

The payload uses text_message.

Supported formats:

  • npub1... -> register solver with read-write permission (default)
  • npub1...:read -> register solver with read permission only
  • npub1...:read-write -> register solver with read-write permission
  • npub1...:write -> alias for read-write

Permission meanings:

  • read: solver can take disputes, receive dispute context, and communicate with users, but cannot execute admin-settle or admin-cancel
  • read-write: solver can do everything above and can also execute admin-settle and admin-cancel

The default remains read-write for backward compatibility.

Example: default read-write solver

[
  {
    "order": {
      "version": 2,
      "action": "admin-add-solver",
      "payload": {
        "text_message": "npub1qqq884wtp2jn96lqhqlnarl4kk3rmvrc9z2nmrvqujx3m4l2ea5qd5d0fq"
      }
    }
  },
  null,
  null
]

Example: read-only solver

[
  {
    "order": {
      "version": 2,
      "action": "admin-add-solver",
      "payload": {
        "text_message": "npub1qqq884wtp2jn96lqhqlnarl4kk3rmvrc9z2nmrvqujx3m4l2ea5qd5d0fq:read"
      }
    }
  },
  null,
  null
]

Mostro response

Mostro sends this message to the admin:

[
  {
    "order": {
      "version": 2,
      "action": "admin-add-solver",
      "payload": null
    }
  },
  null,
  null
]

Restore Session

To restore a session from the mnemonic seed on a new device (e.g., moving from mobile to desktop), the client sends a restore-session message. Mostro will respond with the relevant orders and disputes so the client can rebuild the session state using the same trade_index values.

Request

Client sends a NIP-44 direct message (kind 14) to Mostro with the following decrypted content:

[
  {
    "restore": {
      "version": 2,
      "action": "restore-session",
      "payload": null
    }
  },
  null,
  null
]

Response

Mostro will respond with a message containing all non-finalized orders (e.g., statuses such as pending, active, fiat-sent, waiting-buyer-invoice, waiting-payment, settled-hold-invoice) and any active disputes. The response format will be:

[
  {
    "restore": {
      "version": 2,
      "action": "restore-session",
      "payload": {
        "restore_data": {
          "orders": [
            {
              "order_id": "<Order Id>",
              "trade_index": 1,
              "status": "pending"
            },
            {
              "order_id": "<Order Id>",
              "trade_index": 2,
              "status": "active"
            },
            {
              "order_id": "<Order Id>",
              "trade_index": 3,
              "status": "fiat-sent"
            }
          ],
          "disputes": [
            {
              "dispute_id": "<Dispute Id>",
              "order_id": "<Order Id>",
              "trade_index": 4,
              "status": "initiated",
              "initiator": "seller"
            }
          ]
        }
      }
    }
  },
  null,
  null
]

Fields

  • restore_data: Wrapper object that contains the session recovery data.
  • restore_data.orders: An array of active or ongoing orders with their order_id, trade_index, and current status.
  • restore_data.disputes: An array of ongoing disputes with dispute_id, the associated order_id, trade_index, current status, and initiator ("buyer", "seller", or null if unknown).

Example Use Case

A user has the following:

  • Two pending orders (trade index 1 and 2)
  • One active order (trade index 3)
  • One active dispute (trade index 4)

When switching to desktop, after restoring the mnemonic, the client sends restore-session and receives:

[
  {
    "restore": {
      "version": 2,
      "action": "restore-session",
      "payload": {
        "restore_data": {
          "orders": [
            { "order_id": "abc-123", "trade_index": 1, "status": "pending" },
            { "order_id": "def-456", "trade_index": 2, "status": "pending" },
            { "order_id": "ghi-789", "trade_index": 3, "status": "active" },
            { "order_id": "xyz-999", "trade_index": 4, "status": "dispute" }
          ],
          "disputes": [
            { "dispute_id": "dis-001", "order_id": "xyz-999", "trade_index": 4, "status": "initiated", "initiator": "seller" }
          ]
        }
      }
    }
  },
  null,
  null
]

Last Trade Index

Defines the last-trade-index action used to retrieve the user's last trade_index.

Request

Client sends a NIP-44 direct message (kind 14) to Mostro with the following decrypted content. The request sends a null payload to indicate that the client is querying for the last trade index.

[
  {
    "restore": {
      "version": 2,
      "action": "last-trade-index",
      "payload": null
    }
  },
  null,
  null
]

Response

Mostro responds with the user's last trade index as a u32 directly in the trade_index field. If the user has never created a trade, the value SHOULD be 1.

{
  "restore": {
    "version": 2,
    "action": "last-trade-index",
    "trade_index": 42,
    "payload": null
  }
}

Fields

  • restore.version: Protocol version — 1 on the gift-wrap transport (DEPRECATED), 2 on the NIP-44 direct transport. Current is 2.
  • restore.action: Must be last-trade-index.
  • restore.trade_index (response): u32 representing the last trade_index for the user. 1 if none.
  • restore.payload (response): Must be null.

Example

Client requests the last trade index and receives 7, meaning the next trade the client creates SHOULD use trade_index = 8.

{
  "restore": {
    "version": 2,
    "action": "last-trade-index",
    "trade_index": 7,
    "payload": null
  }
}

Actions

mostro_core::Action

Action is used to identify each message between Mostro and users

You can see details in mostro core documentation

Message Suggestions for Actions

Below are suggestions for messages that clients can show to users when receiving specific actions. These messages can be customized, translated, enhanced with emojis, or modified to provide a better user experience. Clients should replace placeholders in monospace format with the corresponding values.

Actions

  • new-order:
    Your offer has been published! Please wait until another user picks your order. It will be available for expiration_hours hours. You can cancel this order before another user picks it up by executing: cancel.

  • canceled:
    You have canceled the order ID: id.

  • pay-invoice:
    Please pay this hold invoice of amount Sats for fiat_code fiat_amount to start the operation. If you do not pay it within expiration_seconds, the trade will be canceled.

  • pay-bond-invoice:
    Please pay this bond hold invoice of amount Sats as a security deposit before the trade begins. The bond is separate from the trade escrow and is released when the trade completes normally. If you do not pay it within expiration_seconds, the take will be canceled — the order remains visible on the order book as pending and may be taken again by anyone.

  • add-bond-invoice:
    Please send me a Lightning invoice for amount Sats — this is your share of a slashed bond on order id. You have until deadline to submit it, or your share will be forfeited and the entire bond will be retained by the node.

  • bond-invoice-accepted:
    Your bond payout invoice has been received. The payment is being processed — you'll be notified once it completes.

  • bond-payout-completed:
    Your bond payout of amount Sats has been sent successfully. The funds should arrive in your Lightning wallet shortly.

  • bond-slashed:
    You have lost your anti-abuse bond of amount Sats for your order id.

  • add-invoice:
    Please send me an invoice for amount satoshis equivalent to fiat_code fiat_amount. This is where I will send the funds upon trade completion. If you don’t provide the invoice within expiration_seconds, the trade will be canceled.

  • waiting-seller-to-pay:
    Please wait. I’ve sent a payment request to the seller to send the Sats for the order ID: id. If the seller doesn’t complete the payment within expiration_seconds, the trade will be canceled.

  • waiting-buyer-invoice:
    Payment received! Your Sats are now "held" in your wallet. I’ve requested the buyer to provide an invoice. If they don’t do so within expiration_seconds, your Sats will return to your wallet, and the trade will be canceled.

  • buyer-invoice-accepted:
    The invoice has been successfully saved.

  • hold-invoice-payment-accepted:
    Contact the seller at seller-npub to arrange how to send fiat_code fiat_amount using payment_method. Once you send the fiat money, notify me with fiat-sent.

  • buyer-took-order:
    Contact the buyer at buyer-npub to inform them how to send fiat_code fiat_amount through payment_method. You’ll be notified when the buyer confirms the fiat payment. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first.

  • fiat-sent-ok:

    • To the buyer: I have informed seller-npub that you sent the fiat money. If the seller confirms receipt, they will release the funds. If they refuse, you can open a dispute.
    • To the seller: buyer-npub has informed you that they sent the fiat money. Once you confirm receipt, release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, send me release-order-message.
  • released:
    seller-npub has released the Sats! Expect your invoice to be paid shortly. Ensure your wallet is online to receive via Lightning Network.

  • purchase-completed:
    Your purchase of Bitcoin has been completed successfully. Your invoice has been paid. Enjoy sound money!

  • hold-invoice-payment-settled:
    Your sale of Bitcoin has been completed after confirming the payment from buyer-npub.

  • rate:
    Please rate your counterparty.

  • rate-received:
    The rating has been successfully saved.

  • cooperative-cancel-initiated-by-you:
    You’ve initiated the cancellation of order ID: id. Your counterparty must agree. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.

  • cooperative-cancel-initiated-by-peer:
    Your counterparty wants to cancel order ID: id. Send cancel-order-message to confirm. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message.

  • cooperative-cancel-accepted:
    Order ID: id has been successfully canceled.

  • dispute-initiated-by-you:
    You’ve initiated a dispute for order ID: id. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly.

  • dispute-initiated-by-peer:
    Your counterparty initiated a dispute for order ID: id. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly.

  • admin-took-dispute:

    • Admin: Here are the details of the dispute: details. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed.
    • Users: Solver admin-npub will handle your dispute. You can contact them directly.
  • admin-canceled:

    • Admin: You have canceled order ID: id.
    • Users: The admin has canceled order ID: id.
  • admin-settled:

    • Admin: You have completed order ID: id.
    • Users: The admin has completed order ID: id.
  • payment-failed:
    I couldn’t send the Sats. I’ll retry payment_attempts times in payment_retries_interval minutes. Please ensure your node/wallet is online.

  • invoice-updated:
    The invoice has been successfully updated.

  • hold-invoice-payment-canceled:
    The invoice was canceled. Your Sats are available in your wallet again.

  • admin-add-solver:
    Solver npub has been successfully added.

  • cant-do:
    You are not allowed to perform the action: action.

Cant Do Reasons

Mostro also handles messages with the CantDo action for various reasons. The details of the failure are included in the payload section of the event, providing a structured explanation of the issue. Below are suggested texts that clients can display to users based on the CantDo reason received:

  • invalid-trade-index:
    The provided trade index is invalid. Please ensure your client is synchronized and try again.

  • invalid-amount:
    The provided amount is invalid. Please verify it and try again.

  • invalid-invoice:
    The provided Lightning invoice is invalid. Please check the invoice details and try again.

  • invalid-peer:
    You are not authorized to perform this action.

  • invalid-order-status:
    The action cannot be completed due to the current order status.

  • invalid-parameters:
    The action cannot be completed due to invalid parameters. Please review the provided values and try again.

  • invalid-pubkey:
    The action cannot be completed because the public key is invalid.

  • order-already-canceled:
    The action cannot be completed because the order has already been canceled.

  • cant-create-user:
    The action cannot be completed because the user could not be created.

  • is-not-your-dispute:
    This dispute is not assigned to you.

  • not-found:
    The requested dispute could not be found.

  • invalid-signature:
    The action cannot be completed because the signature is invalid.

  • is-not-your-order:
    This order does not belong to you.

  • not-allowed-by-status:
    The action cannot be completed because order Id id status is order-status.

  • out-of-range-fiat-amount:
    The requested fiat amount is outside the acceptable range (min_amountmax_amount).

  • out-of-range-sats-amount:
    The allowed Sats amount for this Mostro is between min min_order_amount and max max_order_amount. Please enter an amount within this range.

  • too-many-requests: User exceeds the allowed request rate.

  • invalid-fiat-currency: Prevents proceeding with unrecognized fiat currencies.

Peer-to-peer Order events. NIP-69

Mostro Event Kinds

Mostro uses different event kinds for different types of data:

Event TypeKindDocument (z tag)
Orders38383order
Ratings38384rating
Info38385info
Disputes38386dispute

This document focuses on the Order event (kind 38383), which is used for the P2P order book.

Abstract

Peer-to-peer (P2P) platforms have seen an upturn in recent years, while having more and more options is positive, in the specific case of p2p, having several options contributes to the liquidity split, meaning sometimes there's not enough assets available for trading. If we combine all these individual solutions into one big pool of orders, it will make them much more competitive compared to centralized systems, where a single authority controls the liquidity.

This NIP defines a simple standard for peer-to-peer order events, which enables the creation of a big liquidity pool for all p2p platforms participating.

The event

Events are addressable events and use 38383 as event kind, a p2p event look like this:

{
  "id": "<Event id>",
  "pubkey": "<Mostro's pubkey>",
  "created_at": 1702548701,
  "kind": 38383,
  "tags": [
    ["d", "<Order Id>"],
    ["k", "sell"],
    ["f", "VES"],
    ["s", "pending"],
    ["amt", "0"],
    ["fa", "100"],
    ["pm", "face to face", "bank transfer"],
    ["premium", "1"],
    [
      "rating",
      "{\"total_reviews\":1,\"total_rating\":3.0,\"last_rating\":3,\"max_rate\":5,\"min_rate\":1,\"days\":21}"
    ],
    ["source", "https://t.me/p2plightning/xxxxxxx"],
    ["network", "mainnet"],
    ["layer", "lightning"],
    ["name", "Nakamoto"],
    ["g", "<geohash>"],
    ["bond", "0"],
    ["expires_at", "1719391096"],
    ["expiration", "1719995896"],
    ["y", "lnp2pbot", "[Platform instance name]"],
    ["z", "order"]
  ],
  "content": "",
  "sig": "<Mostro's signature>"
}

Tags

  • d < Order ID >: A unique identifier for the order.

  • k < Order type >: sell or buy. This specifies the type of transaction in terms of bitcoin. "sell" means selling bitcoin, while "buy" indicates buying bitcoin.

  • f < Currency >: The fiat asset being traded, using the ISO 4217 standard.

  • s < Status >: pending, canceled, in-progress, success, expired.

    An order with s = pending may already be matched to a taker who is in the middle of paying their anti-abuse bond. It remains takeable in this window — another user may attempt the take, and whichever bond locks first wins (the prior taker is notified with Action::Canceled, see Cancel). The internal daemon state that tracks this (waiting-taker-bond, kept in the daemon's database only and never emitted on the wire) is not part of NIP-69's four-bucket wire model. Clients must not gray out or hide a pending order from the local order-book view just because their user has initiated a take.

  • amt < Amount >: The amount of Bitcoin to be traded, the amount is defined in satoshis, if 0 means that the amount of satoshis will be obtained from a public API after the taker accepts the order.

  • fa < Fiat amount >: The fiat amount being traded, for range orders two values are expected, the minimum and maximum amount.

  • pm < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma.

  • premium < Premium >: The percentage of the premium the maker is willing to pay.

  • source [Source]: The source of the order, it can be a URL that redirects to the order.

  • rating [Rating]: The rating of the maker, this document does not define how the rating is calculated, it's up to the platform to define it.

  • network < Network >: The network used for the trade, it can be mainnet, testnet, signet, etc.

  • layer < Layer >: The layer used for the trade, it can be onchain, lightning, liquid, etc.

  • name [Name]: The name of the maker.

  • g [Geohash]: The geohash of the operation, it can be useful in a face to face trade.

  • bond [Bond]: The bond amount, the bond is a security deposit that both parties must pay.

  • expires_at < Expires At>: The expiration date of the event being published in pending status, after this time the event status SHOULD be changed to expired.

  • expiration < Expiration>: The expiration date of the event, after this time the relay SHOULD delete it (NIP-40).

  • y < Platform >: Platform identifier tag values. For Mostro this is always "mostro" and MAY include a second value with the Mostro instance name from settings.

  • z < Document >: order.

Mandatory tags are enclosed with <tag>, optional tags are enclosed with [tag].

Implementations

Currently implemented on the following platforms:

This document is inspired on

Other events published by Mostro

Each Mostro instance publishes several types of events to Nostr relays. These include identity metadata, instance status, relay lists, and development fee records. Below, we provide details on each of these events.

Node Identity (NIP-01 Kind 0)

Each Mostro instance publishes a NIP-01 kind 0 metadata event on startup so that Nostr clients can display the node's profile information. This is the standard Nostr profile mechanism — every relay-aware client already knows how to fetch and display kind 0 metadata.

The event is a replaceable event, meaning relays keep only the latest version. It is re-published on every restart, ensuring the profile stays fresh.

The content field contains a stringified JSON object with the following optional fields:

{
  "name": "Mostro P2P",
  "about": "A peer-to-peer Bitcoin trading daemon over the Lightning Network",
  "picture": "https://example.com/mostro-avatar.png",
  "website": "https://mostro.network"
}

The full event looks like this:

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "kind": 0,
    "tags": [],
    "content": "{\"name\":\"Mostro P2P\",\"about\":\"A peer-to-peer Bitcoin trading daemon over the Lightning Network\",\"picture\":\"https://example.com/mostro-avatar.png\",\"website\":\"https://mostro.network\"}",
    "sig": "<Mostro's signature>",
    "created_at": 1731701441
  }
]

Fields

  • name: Human-readable name for the Mostro instance (e.g., "LatAm Mostro", "Bitcoin Munich Exchange").
  • about: Short description of the instance and the community it serves.
  • picture: URL to an avatar image. Recommended: square, max 128×128 pixels, PNG or JPEG.
  • website: Operator's website URL.

All fields are optional. If no metadata fields are configured, no kind 0 event is published. These fields are configured in the [mostro] section of settings.toml:

[mostro]
name = "Mostro P2P"
about = "A peer-to-peer Bitcoin trading daemon over the Lightning Network"
picture = "https://example.com/mostro-avatar.png"
website = "https://mostro.network"

This allows clients like Mostro Mobile to display meaningful information about each Mostro instance — its name, description, avatar, and website — so users know which node they are trading on.

Mostro Instance Status

Each Mostro instance periodically publishes events with relevant information about its status, such as the code version it is using, the latest commit, the fees it charges, allowed exchange limits, the relays it publishes to, and much more. Below, we provide details on these events.

Mostro Instance Status

This event contains specific data about a Mostro instance. The instance is identified by the d label.

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "kind": 38385,
    "tags": [
      [
        "d",
        "<Mostro's pubkey>"
      ],
      [
        "mostro_version",
        "0.12.8"
      ],
      [
        "mostro_commit_hash",
        "1aac442058720c05954850bcffca6bcdfc87d150"
      ],
      [
        "max_order_amount",
        "1000000"
      ],
      [
        "min_order_amount",
        "100"
      ],
      [
        "expiration_hours",
        "1"
      ],
      [
        "expiration_seconds",
        "900"
      ],
      [
        "fiat_currencies_accepted",
        "USD,EUR,ARS,CUP,VES"
      ],
      [
        "max_orders_per_response",
        "10"
      ],
      [
        "fee",
        "0.006"
      ],
      [
        "pow",
        "0"
      ],
      [
        "pow_first_contact",
        "0"
      ],
      [
        "protocol_version",
        "1"
      ],
      [
        "hold_invoice_expiration_window",
        "120"
      ],
      [
        "hold_invoice_cltv_delta",
        "144"
      ],
      [
        "invoice_expiration_window",
        "120"
      ],
      [
        "lnd_version",
        "0.18.4-beta commit=v0.18.4-beta"
      ],
      [
        "lnd_node_pubkey",
        "0220e4558a8d9af4988ef6c8def0e73b05403819e49b7fb2db79d322ac3be1547e"
      ],
      [
        "lnd_commit_hash",
        "ddeb8351684a611f6c27f16f09be75d5c039f08c"
      ],
      [
        "lnd_node_alias",
        "alice"
      ],
      [
        "lnd_chains",
        "bitcoin"
      ],
      [
        "lnd_networks",
        "regtest"
      ],
      [
        "lnd_uris",
        "0220e4558a8d9af4988ef6c8def0e73b05403819e49b7fb2db79d322ac3be1547e@172.26.0.2:9735"
      ],
      [
        "bond_enabled",
        "true"
      ],
      [
        "bond_apply_to",
        "take"
      ],
      [
        "bond_slash_on_waiting_timeout",
        "false"
      ],
      [
        "bond_amount_pct",
        "0.01"
      ],
      [
        "bond_base_amount_sats",
        "1000"
      ],
      [
        "bond_slash_node_share_pct",
        "0.5"
      ],
      [
        "bond_payout_claim_window_days",
        "15"
      ],
      [
        "y",
        "mostro",
        "[Mostro instance name]"
      ],
      [
        "z",
        "info"
      ]
    ],
    "content": "",
    "sig": "<Mostro's signature>",
    "created_at": 1731701441
  }
]

Below is an explanation of the meaning of some of the labels in this event, all of which can be modified by anyone running a Mostro instance.

  • mostro_version: The version of the Mostro daemon running on the instance.
  • mostro_commit_hash: The ID of the last commit used by the instance.
  • max_order_amount: The maximum amount of Satoshis allowed for exchange.
  • min_order_amount: The minimum amount of Satoshis allowed for exchange.
  • expiration_hours: The maximum time, in hours, that an order can remain in pending status before it expires.
  • expiration_seconds: The maximum time, in seconds, that an order can remain in waiting-payment or waiting-buyer-invoice status before being canceled or reverted to pending status.
  • fiat_currencies_accepted: Fiat currencies accepted by the Mostro. If no currency is specified, all are accepted.
  • max_orders_per_response: Maximum complete orders data per response in orders action.
  • fee: The fee percentage charged by the instance. For example, "0.006" means a 0.6% fee.
  • pow: Proof of work (NIP-13) required of every event a client sends the instance, expressed as the number of leading zero bits the event id must have. Measured on the outer event (the gift wrap on protocol v1, the kind-14 event on v2) and checked before anything else, so an under-powered event is discarded without any reply. "0" means no work is required. See Proof of work.
  • pow_first_contact: Proof of work required of a first-contact event — one whose visible sender is a trade key the instance does not currently associate with an active order or dispute, i.e. a new order or a take. Always at least pow, and typically higher: it is the anti-spam toll on the only lane an unknown sender can arrive through. Read this tag, not pow, before mining the first event of a trade. Daemons that predate the tag omit it; absence means the difficulty is unknown, not that it equals pow. See Proof of work.
  • protocol_version: The Mostro protocol (wire transport) this node speaks — "1" for NIP-59 gift wrap (kind 1059, DEPRECATED) or "2" for NIP-44 direct messages (kind 14). A node speaks exactly one; clients read this tag to pick the matching wire format. See the client migration guide.
  • hold_invoice_expiration_window: The maximum time, in seconds, for the hold invoice issued by Mostro to be paid by the seller.
  • hold_invoice_cltv_delta: The number of blocks in which the Mostro hold invoice will expire.
  • invoice_expiration_window: The maximum time, in seconds, for a buyer to submit an invoice to Mostro.
  • lnd_version: The version of the LND daemon running on the instance.
  • lnd_node_pubkey: The pubkey of the LND node running on the instance.
  • lnd_commit_hash: The ID of the last commit used by the LND node.
  • lnd_node_alias: The alias of the LND node.
  • lnd_chains: The chains supported by the LND node.
  • lnd_networks: The networks supported by the LND node.
  • lnd_uris: The URIs of the LND node.
  • y: Platform identifier tag values. Mostro publishes "mostro" and MAY include a second value with the Mostro instance name from settings.
  • z: The type of event.

Anti-abuse bond policy tags

These tags advertise the node's anti-abuse-bond configuration so clients can show users — before they enter a trade — whether a bond is required, how much it will cost, and under what conditions it can be slashed. bond_enabled is always emitted on daemons that support the feature; the remaining six tags are present only when bond_enabled = "true".

  • bond_enabled: "true" if the node enforces an anti-abuse bond on at least one trade side, otherwise "false".
  • bond_apply_to: which side must lock a bond — "take", "make", or "both".
  • bond_slash_on_waiting_timeout: "true" if a bonded user's bond is slashed when a waiting-state timeout elapses, "false" if such timeouts only release the bond.
  • bond_amount_pct: bond size as a fraction of the trade amount (e.g. "0.01" = 1%). The actual bond is max(bond_amount_pct × order_amount_sats, bond_base_amount_sats).
  • bond_base_amount_sats: minimum bond size in satoshis. Floor applied to bond_amount_pct × order_amount_sats so small trades still carry a non-trivial bond.
  • bond_slash_node_share_pct: fraction of a slashed bond the node retains. The remainder is paid out to the winning counterparty as a Lightning payment. Always within [0.0, 1.0].
  • bond_payout_claim_window_days: number of days the winning counterparty has, from the slash moment, to submit a Lightning invoice for their share of a slashed bond. After this window the share is forfeited to the node. Clients use this together with the slashed_at field carried on add-bond-invoice messages (see Bond payout invoice) to render the forfeit deadline locally.

Disambiguation. When bond_enabled is absent from the event, the daemon predates the bond feature; treat the node as not enforcing bonds. When bond_enabled = "false", the daemon supports the feature but the operator has not enabled it on this instance. When bond_enabled = "true", the remaining six bond tags are present and clients should expect bond messages on this node — pay-bond-invoice for the bonded user and add-bond-invoice for the payout recipient on a slashed trade.

Information about the Relays Where Events Are Published

The operator of a Mostro instance decides which relays the events from that instance are published to. This information can be accessed in events kind 10002, which are published by the Mostro instances.

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "kind": 10002,
    "tags": [
      ["r", "wss://relay.mostro.network/"],
      ["r", "wss://nos.lol/"]
    ],
    "content": "",
    "sig": "<Mostro's signature>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1731680102
  }
]

The r label indicates the relays through which the Mostro instance is publishing its events.

Exchange Rates

Each Mostro instance publish Bitcoin/fiat exchange rates to Nostr relays as addressable events (kind 30078):

Event Structure

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "pubkey": "<Mostro's pubkey>",
    "kind": 30078,
    "tags": [
      ["d", "mostro-rates"],
      ["published_at", "1732546800"],
      ["source", "yadio"],
      ["expiration", "1732550400"]
    ],
    "content": "{\"BTC\": {\"USD\": 50000.0, \"EUR\": 45000.0, \"VES\": 850000000.0, \"ARS\": 105000000.0, ...}}",
    "sig": "<Mostro's signature>",
    "created_at": 1732546800
  }
]

Tags

  • d: "mostro-rates" — NIP-01 identifier that makes this event replaceable. Each new rate update replaces the previous one.
  • published_at: Unix timestamp when the daemon published the event (daemon time, not source timestamp).
  • source: Rate source identifier (e.g., "yadio").
  • expiration: Unix timestamp for event expiration (NIP-40). Prevents stale rates from being served.

Content Format

The content field contains the full rate response in JSON format matching the source API structure (Yadio format):

{
  "BTC": {
    "BTC": 1,
    "USD": 50000.0,
    "EUR": 45000.0,
    "VES": 850000000.0,
    "ARS": 105000000.0,
    "AED": 260491.35,
    "..."
  }
}

Rate semantics: Each value under "BTC" represents the price of 1 BTC in that currency.

Development Fee

The development fee mechanism provides sustainable funding for Mostro development by automatically sending a configurable percentage of the Mostro fee to a lightning address on each successful order, this a regular event with kind 8383 which is expected to be stored by relays.

[
  "EVENT",
  "RAND",
  {
    "id": "<Event id>",
    "tags": [
      [
        "order-id",
        "<Order id>"
      ],
      [
        "amount",
        "8" // sats amount
      ],
      [
        "hash",
        "ca2f47b7c2169b8c42ef135e8ee32706e1fd3722b65e5a16f21ce675d2affb6b"
      ],
      [
        "destination",
        "dev@mostro.network"
      ],
      [
        "network",
        "mainnet"
      ],
      [
        "y",
        "mostro",
        "[Mostro instance name]"
      ],
      [
        "z",
        "dev-fee-payment"
      ]
    ],
    "content": "",
    "sig": "<Mostro's signature>",
    "pubkey": "<Mostro's pubkey>",
    "created_at": 1768256716,
    "kind": 8383
  }
]