NKIPs

NKIP-0001 · Draft

Verifiable Deposit Receipts

Abstract. Every swap order gets a receipt signed by the exchange before the deposit address is shown, and an issuer-signed commitment to it is stored by independent witnesses, so a dispute never depends on whether the user remembered to download a letter of guarantee.

NKIP1
TitleVerifiable Deposit Receipts
StatusDraft
TypeStandards Track
Created2026-10-26
LicenseCC0-1.0

Files: manifest.schema.json · receipt.schema.json · vectors/v1.json

Contents

Motivation

A letter of guarantee (LoG) protects a user only if they download it, and most don't. Recent no-KYC swap disputes have come down to one side's word against the other's because nobody kept a receipt.

Goals:

  • G1 Every order has a receipt the exchange signs and can't later deny.
  • G2 The receipt is saved by someone other than the user, without them doing anything.
  • G3 Anyone can check a disputed order without the exchange's cooperation.
  • G4 "No receipt" counts against the exchange, not the user.

Non-goals:

  • Proving delivery on the user's behalf. Once a receipt and the deposit are proven, the issuer has to prove the payout (txid, or tx key for XMR payouts).
  • KYC or identity of either party.
  • A central registry of orders. Witnesses hold hashes, not plaintext addresses.

Roles

Role Who Job
Issuer The exchange Signs a receipt for every order and publishes its signing key
User The customer Deposits only after the deposit address is shown; nothing to save
Witness Nostr relays; aggregators routing the order Store the attestation or receipt, independently of the issuer
Verifier Any directory or tool implementing this NKIP Pins issuer keys after domain-control verification, checks disputes
Log Hourly Merkle roots of attestations (Nostr events, optional OpenTimestamps) Proves an attestation existed at a given time

Specification

The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.

Receipt

A JSON object signed by the issuer at order creation, before the deposit address is shown. Schema: nkip-0001/receipt.schema.json.

Field Meaning
v Spec version, nkip01/1
issuer Issuer domain, lowercase, e.g. example.swap
order_id The issuer's order ID
deposit_asset, deposit_address, deposit_amount What the user sends, and where
payout_asset, payout_address, payout_amount What the issuer promises, and where; for float orders the amount is a quoted floor
rate_type fixed or float
created_at, expires_at Integer Unix seconds
refund_address Optional
policy_hash SHA-256 (hex) of the exact bytes of the refund/tolerance policy in force at order time
key_id First 8 bytes of SHA-256 of the signing public key, hex
sig Ed25519 signature, base64url without padding

Rules:

  • Canonical form is RFC 8785 (JCS). Amounts are decimal strings in the asset's base unit (piconero, satoshi, ...). sig is computed over the JCS bytes of the receipt with the sig field removed.
  • Ed25519 is the only v1 algorithm. A PGP-signed LoG MAY be offered as a human-readable copy, but it is not the receipt.
  • deposit_address MUST be unique to the order. A reused address makes the order ambiguous, and a dispute on it counts against the issuer.
  • Amount tolerance, partial or over-payment and late deposits follow the policy named by policy_hash.

Issuer manifest (nkip01.json)

The issuer publishes https://<issuer>/.well-known/nkip01.json, served with Content-Type: application/json. Schema: nkip-0001/manifest.schema.json.

{
  "v": "nkip01/1",
  "issuer": "example.swap",
  "adopted_at": 1793000000,
  "keys": [
    {
      "key_id": "3b1f0c9e5a7d2e41",
      "alg": "ed25519",
      "public_key": "<base64url, 32 bytes>",
      "not_before": 1793000000,
      "revoked_at": null,
      "rotated_from": null,
      "rotation_sig": null
    }
  ],
  "nostr_pubkey": "<hex, 32 bytes>",
  "relays": ["wss://relay.example.org", "wss://relay.example.net", "wss://relay.example.com"],
  "policies": [{"policy_hash": "<sha256 hex>", "url": "https://example.swap/policy/v1.txt"}],
  "disputes": {"email": "disputes@example.swap", "nostr": "<npub or hex>"}
}
  • keys[].rotation_sig is the old key's signature (base64url) over the JCS of the new key entry without rotation_sig. A new key without it is valid only after the verifier re-checks domain control.
  • revoked_at revokes a key. Receipts with created_at >= revoked_at are invalid. Earlier receipts stay valid only if their attestation is in a log root published before revoked_at.
  • Every policy_hash used in a receipt MUST be listed with a URL serving those exact bytes. Policy URLs are never edited; a new policy gets a new URL. Verifiers SHOULD archive policy texts when they first see them.
  • adopted_at starts the issuer's obligations. The "no receipt" rule (G4) applies to orders created 14 days or more after adopted_at.
  • Verifiers MUST NOT pin a key on first sight. They pin only after a domain-control verification, and publish the pin in a signed feed.

Commitment and attestation

  • Commitment: SHA-256("nkip01/1" || 0x00 || issuer || 0x00 || deposit_address || 0x00 || payout_address), lowercase hex. The user can recompute it from their own wallet history without having saved anything. order_id is deliberately excluded; uniqueness of deposit_address links it to one order.
  • Attestation: the JCS object {"v","issuer","commitment","created_at","key_id","sig"}, where sig is the Ed25519 signature over the JCS of the other five fields.

Nostr event

The attestation is published as a Nostr event (NIP-01) signed by nostr_pubkey:

Field Value
kind 3711 (provisional; to be registered upstream)
content The attestation JSON
tags ["c", <commitment>], ["i", <issuer>], ["v", "nkip01/1"]
created_at Equal to the attestation's created_at (±300 s)

Anyone can find an attestation with the relay filter {"kinds":[3711],"#c":[<commitment>]}.

A verifier accepts the event only if the Nostr signature matches the manifest's nostr_pubkey and the Ed25519 signature matches a pinned key. Either key alone is not enough.

Witnessing

Before showing the deposit address, the issuer MUST get an attestation into at least one counted witness.

Channel Holds Proves later Counts
Nostr relays The attestation event, on 3 or more relays the issuer doesn't control The issuer committed to this deposit/payout pair at this time Yes, with OK true from at least 2 relays
Aggregator The full receipt, for orders it routes The full terms of the order Yes, with a signed acceptance
Browser extension A local copy of the receipt The full terms, if the user still has it No
  • Acceptance signal: a NIP-01 ["OK", <event id>, true, ...] from at least 2 relays not operated by the issuer, or an aggregator response that echoes the commitment. The order page SHOULD show the event id next to the deposit address.
  • Nostr is the default path and needs no aggregator, so an aggregator outage never blocks a compliant issuer. Without an acceptance signal, the issuer MUST NOT show the address and retries.
  • Aggregators SHOULD delete full receipts 180 days after expires_at.

Log

Batchers timestamp attestations. Anyone MAY run one, and a verifier MAY use several.

  • Every hour, a batcher collects the kind 3711 events it saw in that hour and sorts their commitments. It builds a Merkle tree as in RFC 6962: leaf = SHA-256(0x00 || commitment bytes), node = SHA-256(0x01 || left || right).
  • It publishes kind 3712 (provisional) with content {"v":"nkip01/1","hour":<unix hour start>,"root":<hex>,"count":<n>,"leaves_url":<url>}, and MAY anchor root with OpenTimestamps.
  • leaves_url serves the sorted commitment list. An inclusion proof is that list, or the audit path computed from it.
  • Roots from different batchers need not agree, because each sees different relays. An attestation's existence time is the earliest root that includes it. A batcher that leaves out an event it demonstrably received is untrustworthy for that hour.

Order and dispute flow

  1. The user requests a quote and enters the payout address.
  2. The issuer creates the order with a one-time deposit address and signs the receipt.
  3. The issuer publishes the attestation event to its relays, and hands the receipt to the aggregator for routed orders.
  4. Only after an acceptance signal does the page show the deposit address (and the event id).
  5. The user deposits.
  6. In a dispute, the user gives a verifier the deposit address, the payout address and a deposit proof. For XMR that is the tx key (check_tx_key) or a get_tx_proof proof; for BTC, the txid.
  7. The verifier recomputes the commitment, queries relays for the attestation (or checks a receipt), and checks the deposit on-chain.
  8. The verifier notifies the issuer through disputes in the manifest. Within 7 days the issuer shows the payout (txid, or tx key for an XMR payout) to the committed payout address, or a refund under its policy.
  9. No payout proof means a substantiated claim against the issuer. The verifier publishes the outcome either way.

v1 verifiers MUST support BTC and XMR deposits and payouts. Other chains MAY be supported where a deposit or payout can be proven from public data.

"No receipt" claims

For orders created 14 days or more after adopted_at, a deposit to an address the issuer gave out with no attestation anywhere counts against the issuer. The claim needs an on-chain deposit plus at least one of these:

  • a timestamped capture of the issuer's page showing the address (web archive, TLS notary or OpenTimestamps of the saved page);
  • the same address in another attested order from that issuer;
  • a message from the issuer's disputes contact, or its support, that names the address;
  • the aggregator's routing record.

Without any of these, the claim is published as unsubstantiated and does not count.

Requirements

ID Level Goal Requirement Acceptance check
R1 MUST G1 Issuer signs a JCS/Ed25519 receipt with all required fields for every order Test vectors pass; schema validates
R2 MUST G2 Deposit address is not shown before an attestation is accepted by a counted witness Order page exposes the event id
R3 MUST G1 Manifest at /.well-known/nkip01.json validates against the schema; rotations signed by the old key Schema check; rotation chain verifies
R4 MUST G2 Receipt is machine-readable on the order page (<script type="application/nkip01+json">) and in the create-order API Aggregator and extension parse it
R5 MUST G3 Commitments and attestations carry no plaintext address or amount Log sample check
R6 MUST G3 Within 7 days of a valid claim, the issuer produces a payout or refund proof Recorded either way
R7 MUST G2 Attestations go to 3 or more relays the issuer doesn't control Relay query finds the event
R8 SHOULD — A human-readable LoG of the same receipt is offered for download Manual
R9 SHOULD G3 Order status opens with the order ID alone Manual
R10 MUST G2 Aggregators implementing this NKIP reject quotes from NKIP-0001 issuers that lack a valid receipt Aggregator test
R11 MUST G3 A public verify tool needs no account, keeps nothing after the check, and is rate-limited Code review
R12 SHOULD G2 Extensions save receipts locally only and warn before deposit when one is missing Extension test
R13 MUST G3 Verifier key/domain feeds are signed Signature check
R14 MUST G1 This NKIP ships a JSON schema, test vectors and a version field; v1 receipts stay valid after later versions CI on this repo

Security considerations

Threat Likelihood Control
Issuer skips the receipt for one order it plans to keep High Detect, don't prevent: an address shown without an attestation is unreceipted; extensions warn before deposit; the claim counts once the evidence standard is met
False "no receipt" claims Medium Evidence standard: an on-chain deposit plus proof the issuer gave out that address (timestamped page capture, the same address in another receipted order, or the issuer's admission); otherwise unsubstantiated
Issuer denies a receipt Medium Attestation signed with the pinned key, plus a timestamped root
User forges or edits a receipt Medium Signature fails; amounts must match the on-chain deposit
Replay Low One-time deposit address, expires_at, one deposit per receipt
Clipboard hijack of the payout address Medium Extension compares the on-page address with the receipt
Phishing clone High Key doesn't match the pin
Key substituted at first contact Low No first-sight pinning
Relays delete or censor events Low 3+ relays and independent hourly roots
Witness collusion Low Only issuer-signed attestations count: a witness can drop data but not forge it
Lapsed domain bought by someone else Medium Key pinned to the verifier's listing, not just the domain
Issuer key stolen Low Rotation signed by the old key; revocations published
Timing/amount correlation Low No amounts in the log; hourly batching

Open questions

  • Floating-rate orders: v1 signs a floor. Re-signing on rate lock is left for a later version.
  • Register kinds 3711 and 3712 upstream (nostr-protocol/nips), or pick replacements.
  • Should the verifier feed be its own NKIP?

Changelog

  • 2026-10-26: Draft. Added manifest format, Nostr event, acceptance signal, log and proofs, revocation rule, policy hosting, v1 chains, dispute channel and claim evidence.

Markdown source