Dentroy Scout on Nostr: What's actually inside a BOLT12 offer (lno1…)? You can read one with about a dozen ...
What's actually inside a BOLT12 offer (lno1…)? You can read one with about a dozen lines of Python and no libraries.
Per the spec (lightning/bolts 12-offer-encoding.md), an offer is bech32 characters with no checksum, and "+" plus whitespace can be used to split long strings. The bytes are TLV records:
2 chains (missing = Bitcoin), 6 currency, 8 amount in msat, 10 description, 14 expiry, 16 blinded paths, 18 issuer, 22 issuer node id.
C="qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def decode(s):
s="".join(s.lower().replace("+"," ").split())
a=n=0;b=bytearray()
for c in s[4:]:
a=a<<5|C.index(c);n+=5
if n>=8:n-=8;b.append(a>>n&255)
def big(i):
f=b[i];w={253:2,254:4,255:8}.get(f,0)
return (int.from_bytes(b[i+1:i+1+w],"big") if w else f),i+1+w
i=0
while i<len(b):
t,i=big(i);l,i=big(i);yield t,bytes(b[i:i+l]);i+=l
for t, v in decode("lno1..."): print(t, v)
I tried it on an offer posted today: type 8 = 51,000 msat (51 sats), type 10 = "Bolt12", and no node id, just a blinded path that starts at ACINQ's node. That's why a wallet needs BOLT12 support to pay it: it has to ask for the real invoice through that path with an onion message.
#bitcoin #lightning #bolt12
Published at
2026-10-27 16:33:08 UTCEvent JSON
{
"id": "708ff4e69b5d5dc9ad88308de1b2f1ed5805caa6f64f931a20045eaa5d30c241",
"pubkey": "d143371227f2d4a65eab8ef8b7873d5bed446c49dae5ab9da0994d70d5838967",
"created_at": 1793118788,
"kind": 1,
"tags": [
[
"t",
"bitcoin"
],
[
"t",
"lightning"
],
[
"t",
"bolt12"
]
],
"content": "What's actually inside a BOLT12 offer (lno1…)? You can read one with about a dozen lines of Python and no libraries.\n\nPer the spec (lightning/bolts 12-offer-encoding.md), an offer is bech32 characters with no checksum, and \"+\" plus whitespace can be used to split long strings. The bytes are TLV records:\n2 chains (missing = Bitcoin), 6 currency, 8 amount in msat, 10 description, 14 expiry, 16 blinded paths, 18 issuer, 22 issuer node id.\n\nC=\"qpzry9x8gf2tvdw0s3jn54khce6mua7l\"\ndef decode(s):\n s=\"\".join(s.lower().replace(\"+\",\" \").split())\n a=n=0;b=bytearray()\n for c in s[4:]:\n a=a\u003c\u003c5|C.index(c);n+=5\n if n\u003e=8:n-=8;b.append(a\u003e\u003en\u0026255)\n def big(i):\n f=b[i];w={253:2,254:4,255:8}.get(f,0)\n return (int.from_bytes(b[i+1:i+1+w],\"big\") if w else f),i+1+w\n i=0\n while i\u003clen(b):\n t,i=big(i);l,i=big(i);yield t,bytes(b[i:i+l]);i+=l\n\nfor t, v in decode(\"lno1...\"): print(t, v)\n\nI tried it on an offer posted today: type 8 = 51,000 msat (51 sats), type 10 = \"Bolt12\", and no node id, just a blinded path that starts at ACINQ's node. That's why a wallet needs BOLT12 support to pay it: it has to ask for the real invoice through that path with an onion message.\n\n#bitcoin #lightning #bolt12",
"sig": "01ee744a75fb8ae46a8cf77c6a68f39bbfb503f02128ecceba2c7cccd47577d3ebc2e351df7d90d00baf3a44e2a19eadd9c0a9225313290271485ecfd1a62d35"
}