Skip to content

Protocol

WoompaLoompa edited this page Aug 10, 2026 · 3 revisions

Protocol

The extension implements a hand-rolled, minimal CLINK protocol stack in the clink/nostr package. It has no new dependencies — it reuses LNbits' bundled Nostr utilities (lnbits.utils.nostr), coincurve, websockets, and bech32.

Reference

CLINK is specified by the shocknet/CLINK repository:

  • specs/clink-offers.md — kind 21001 offers
  • specs/clink-debits.md — kind 21002 debits
  • specs/clink-manage.md — kind 21003 management delegation (not yet implemented here)

NIP-44

v2 (offers, debits, user-pay payloads). content payloads for offers and debits are encrypted with NIP-44 v2, matching the audited paulmillr reference exactly:

  • 16-bit (u16) length prefix; plaintext 1–65535 bytes, encoded 132–87472 bytes, payload 99–65603 bytes.
  • clink/nostr/nip44.py implements encrypt/decrypt (and encrypt_with_keys/decrypt_with_keys helpers) and is tested against tests/vectors/nip44.vectors.json.

v1 (Lightning.pub user API). The funding-source account flow (kind 21000) uses NIP-44 v1, XChaCha20-Poly1305, implemented in clink/nostr/nip44v1.py and tested against libsodium/stablelib vectors.

Keys

  • Key generation/signing/verification reuse lnbits.utils.nostr + coincurve.
  • clink/nostr/keys.py adds pubkey_from_privkey(hex_secret) → the 64-hex, x-only (NIP-01) public key.
  • Outgoing requests (Pay Offers, subscription renewals) sign with a fresh ephemeral keypair per request, per the CLINK SDK's recommendation.

Pointers (bech32, NIP-19 style TLV)

clink/nostr/bech32.py encodes/decodes both pointer types. The TLV layout is type (1 byte) | length (1 byte) | value, and encoding emits TLVs in descending type order to match the reference SDK byte-for-byte.

noffer1...

TLV Field Notes
0 Service pubkey 32 bytes
1 Relay URL where the node listens
2 Offer id server-generated
3 Price type 0 fixed, 1 variable, 2 spontaneous
4 Price optional, 4-byte big-endian uint32 (sats)

ndebit1...

TLV Field Notes
0 Node service pubkey 32 bytes
1 Relay URL where the node listens
2 Pointer id optional, opaque
3 Session k1 optional, exactly 32 raw bytes

A pointer without TLV 3 is a static pointer (subscriptions, pre-approved debits). A pointer with TLV 3 is a session pointer (one-shot payout); the requestor MUST echo the hex of k1 in the request payload.

Event kinds

All CLINK events carry a mandatory ["clink_version", "1"] tag.

Kind Direction Purpose
21000 request / response Lightning.pub user API (account: PayInvoice, GetUserInfo, GetPaymentState, NIP-44 v1 envelope)
21001 request / response Offer request (payer → service), invoice response
21002 request / response Debit request (app → wallet node), ok/GFY response
21003 request / response Manage delegation (spec reference only)

Response events always include ["e", "<request_event_id>"] to reference the request.

Request payloads

Offer request (kind 21001, payer → service):

{
  "offer": "<offer_id>",
  "amount_sats": 10000,
  "description": "optional"
}

Response: {"bolt11": "<invoice>"} or an error payload.

Direct payment (kind 21002, app → wallet node):

{
  "pointer": "<pointer_id>",
  "amount_sats": 10000,
  "bolt11": "<BOLT11>",
  "description": "optional",
  "k1": "<64-hex, only for session pointers>"
}

Response: {"res": "ok", "preimage": "…"} (preimage omitted for internal settlement).

Budget request (kind 21002):

{
  "pointer": "<pointer_id>",
  "amount_sats": 50000,
  "frequency": { "number": 1, "unit": "month" },
  "description": "optional"
}

Response: {"res": "ok"}.

GFY codes

Failures are returned as {"res": "GFY", "code": <n>, "error": "<reason>"}.

Code Meaning
1 Request Denied (user/rule denied)
2 Temporary Failure (node issue)
3 Expired Request (too old, e.g. > 30 s delta)
4 Rate Limited
5 Invalid Amount (out of range/budget)
6 Invalid Request (malformed/missing fields)

The node rejects requests older than REQUEST_MAX_AGE_SECONDS = 30 with GFY(3) and uses the k1 table for single-use session correlation.

Clone this wiki locally