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
Announcements38387announcement
Reputation attestations38388reputation-attestation
Health38389health

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

Every kind above is published by a Mostro daemon except Announcements (kind 38387), which is published by the keys of the project that ships a client and is described in Announcement events, and Reputation attestations (kind 38388), which are signed by a reputation issuer and never published at all: they travel inside encrypted messages, as described in Reputation attestation. The Health kind (38389) is published by the daemon, and also by monitoring tools for their own heartbeat (z = watcher), as described in Health event.

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",
      "[\"rating\",{\"total_reviews\":1,\"total_rating\":3.0,\"since\":1700697600,\"days\":21}]"
    ],
    ["source", "https://t.me/p2plightning/xxxxxxx"],
    ["network", "mainnet"],
    ["layer", "lightning"],
    ["name", "Nakamoto"],
    ["g", "<geohash>"],
    ["bond", "0"],
    ["published_at", "1702548701"],
    ["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, a stablecoin code from Stablecoins, or a code from Bitcoin on other networks (BTC, LBTC, RBTC).

  • 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. The event is not republished with the prospective taker's amounts during that window: a range order keeps amt "0" and fa [min, max] until a bond locks and the order leaves pending.

  • 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. For a code from Bitcoin on other networks it counts sats of the asset, not whole units.

  • pm < Payment method >: The payment method used for the trade, if the order has multiple payment methods, they should be separated by a comma. For a stablecoin, each method names the network, as in USDT (Tron) (see Stablecoins); for bitcoin on another network, as in RBTC (Rootstock) (see Bitcoin on other networks).

  • 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, and so is the shape of the value. Mostro publishes a JSON-serialized two-element array, ["rating", {…}], whose object carries the fields below — or {} when it has no reputation data for the maker. Clients SHOULD also accept a bare object, the shape NIP-69 uses in its example:

    • total_reviews: ratings the user has received.
    • total_rating: the user's average rating.
    • since: Unix timestamp of the user's first trade, truncated to the start of its UTC day (created_at - created_at % 86400). Clients compute the age at display time. Day precision is deliberate: the tag travels on every order of the same user, and a second-precision value would make their trade pubkeys trivially correlatable.
    • days: DEPRECATED in favour of since — a day count computed at publish time is stale on any event that sits on relays. Published alongside since, with no removal release fixed: Mostro drops it only as the last step of the reputation portability rollout, because clients update at their users' pace and one that predates since would lose the age. Clients MUST prefer since when present and MAY fall back to days while it is published.
  • 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.

  • cashu_mint_url [Cashu mint]: Mostro only. On a node with escrow_mode = "cashu" (see the info event), this is the URL of the mint the order's escrow is locked on. The maker chose it when creating the order. By taking the order, a taker accepts this mint, so clients SHOULD show it before the take. Lightning orders omit it.

  • published_at [Published At]: The unix timestamp when the order was created, named as in NIP-23. Unlike the event's created_at, which changes on every update of this addressable event, it stays the same across updates, so clients can show the order's age and sort by it. Clients SHOULD fall back to the event's created_at when the tag is absent (nodes that predate it).

    In Mostro this is when the daemon created the order, which is not always when its first event went out: with an anti-abuse bond on the maker, the order exists before it is published, once the bond locks. The remainder of a partially taken range order is a new order with its own published_at.

  • 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