Offline lightning PoS



Cheap, offline(!), DIY bitcoin lightning-network PoS


Lightning-network uses hot wallets and real-world payments are made from phones. The burden of connectivity can be taken away from the point-of-sale and given to the phone.


For a traditional PoS experience see my LNPoS project.

The manufacturer of the microcontroller used have actually released a specific kit for LNURLPoS! LiLyGos repo.

LNURLPoS uses the LNURL-pay protocol. LNURL-pay allows your lightning-wallet to make a secure request to a server to get a lightning-network invoice. So instead of scanning a massive ugly lightning-network invoice QR, you can scan a lovely little LNURL QR (if you decode an LNURL you'll see its just a URL).


For online stuff I suppose massive QR codes are not an issue, but when fiddling with hardware devices they are. LNURLPoS using the LNURL-pay protocol, it can use a smaller screen for displaying the QR.

Setup workflow

  • LNURLPoS server set up and register PoS in a few clicks on LNbits using the LNURLPoS extension
  • Copy credentials (including a secret key) from server to the physical LNURLPoS device

Payment workflow

  • Merchant enters amount into LNURLPoS device
  • LNURL is generated in device and displayed for scanning (LNURL includes a unique pin encrypted using the secret key shared with the server)
  • Customer scans and pays
  • When the payment has cleared the customer is sent the decrypted unique pin
  • Merchant can compare and verify using the same pin displayed on the lNURLPoS


Stepan Snigerev for creating beautiful crypto and LNURL encoding functions.

Fiatjafs incredible OfflineShop extension. LNURLPoS is the same concept, but can run at scale, and is dependent on a device.

Belskomat for pironeering the idea of a shared secret for the microcontroller to encrypt data with.

LNURLPoS Tutorial

Hardware needed

Arduino software install

  • Download/install latest Arduino IDE
  • Install ESP32 boards, using boards manager
  • Copy these libraries into your Arduino IDE library folder
  • Plug in T-Display, from Tools>Board>ESP32 Boards select TTGO LoRa32 OLED V1

Note: If using MacOS, you will need the CP210x USB to UART Bridge VCP Drivers available here

Note: You may need to roll your ESP32 boards back to an earlier version in the Arduino IDE, by using tools>boards>boards manager, searching for esp. I use v1.0.5(rc6), and have also used v1.0.4 which worked.

LNbits extension

To make things easy (usually a few clicks on things like Raspiblitz), there is an LNbits extension. If you want to make your own stand-alone server software that would be fairly easy to do, by replicating the file in the extennsion.

Future updates

At the beginning of this article I said "LNURLPoS (currently) only uses LNURL-Pay". The next stage will be for the PoS to also create LNURL-Withdraws, which are essentially faucets. This means merchants can offer refunds, and also sell bitcoin over the counter, which creates an extremely powerful tool for local economies on-ramping and off-ramping from their local fiat currency.

At Adopting Bitcoin in San Salvador I will distribute 40 kits over x2 workshops, so hopefully some locals will start producing, selling and teaching others how to make these useful little units.

Deeper Dive


Much of the innovation that happens on lightning-network uses an additional protocol layer called LNURL.

LNURL is just a bech32 encoded URL string, that is a link to an LNURL server that your lightning wallet can request information from. By your wallet being able to communicate with a server, developers are no longer bound by the payee-generate-invoice workflow. There are many different types of LNURL. LNURLPoS (currently) only uses LNURL-Pay.

This is an LNURL-pay QR code:


This is the data in that QR code:


If we decode the LNURL we get this URL:

If you do a GET request to the URL this data will be returned (you can test this by just visiting the URL in a browser):

  "callback": "", 
  "maxSendable": 10000000000, 
  "metadata": "[[\"text/plain\", \"Lovely little QR\"]]", 
  "minSendable": 10000, 
  "tag": "payRequest"

When your wallet gets this json it asks you how much you want to send between the minSendable and maxSendable.

After a moment you get a “payment sent” confirmation and receipt.

So what happened?

When you verify you want to send say 10sats, your wallet sends that data (as a json) to the callback URL. The server then generates an invoice for that amount and sends it back to your wallet, which pays it. Once the payment has cleared, the wallet reveals a receipt to you.

LNURLPoS workflow

LNURLPoS generates and encodes the LNURL in the device, which means we can pass some data in the URL.

The LNURLPoS stores four important pieces of data:

  • URL to your LNURL server (we’re using an LNbits install, with dedicated extension)
  • PoS ID (Unique ID generated in the LNbits extension)
  • Secret (Secret shared with the LNURL server)
  • Currency denomination (being offline sats becomes too volatile)

LNURLPoS could use any LNURL server that performs some certain functions, but to make things easy I made an extension in LNbits specifically for LNURLPoS

image image

Once a PoS has been generated the extension gives you this data:

String server = "";
String posId = "L4aJNiQZyPxCREoB3KXiiU";
String key = "4TPLxRmv82yEFjUgWKdfPh";
String currency = "EUR";

The data can then be passed to the device when uploading its software through the Arduino IDE

When an amount is entered into the LNURLPoS, the device generates a unique pin, then encrypts the amount+pin using the shared secret and a nonce. The server endpoint, nonce, encypted data, and PoS ID are built into into the LNURL.<nonce>/<encrypted-data>/<pos-id>


When that first GET request happens from the wallet, the LNURL server can find the PoS record, fetch its secret use the secret to decrypt the amount+pin. The amount is converted from the fiat currency to sats, and sent back to the wallet as minSendable and maxSendable.

If the invoice passed to the wallet is paid the customer is given access to the decrypted pin.

  • 'QRCode' was not declared in this scope?

    'QRCode' was not declared in this scope?


    I've checked that I followed the instructions and I have Googled for this error, but none of the results seem conclusive or apply.

    It's my first time with the Arduino IDE, but it seems to have found and I've installed the libraries required, and though I'm still waiting for the hardware, thought I'd get about preparing for its arrival by verifying, by compilation, the set up.


    If there's any additional information required, please advise.

    opened by HamishMacEwan 12
  • Awesome work!

    Awesome work!

    This isn’t an issue. Just not sure if I can send messages in GitHub. Really just wanted to say thank you for such a cool project. You’re doing such cool stuff and it’s so cool that you’re being open about it and letting everyone have the code and even show them in a video how it all works. So cool!

    I had a pet project a few years back related to Ninja Warrior-type timing for a local business. It was such a fun hobby and seeing your work makes me get back that excitement. Now my mind is racing thinking about doing something with your project with something like an arcade, vending machine, art-piece, etc. Those things have been hampered in my mind due to lack of internet connection in some locations (and paying for cell access was not worth it).

    I think with your system the vendor has to look at a customer’s phone to see the pin code, right? Could that pin code be replaced by a QR code and then the arcade/vending-machine has a camera that confirms the code? That way everything could be totally unmanned, right?

    Anyway, now I have to think up a project. My wife will probably be mad at you. :)

    Thanks again!

    opened by brihogan 3
  • Which chip and memory is needed on the TTGO

    Which chip and memory is needed on the TTGO

    Love your work Ben...

    The link to the TTGO actually has 4 options (2 different chips and memory choices)

    Can you advise which one is needed (or if it doesn't matter). Thanks

    Options are

    • ch9102f chip (4 & 16 mb)
    • ch340k chip (4 & 16 mb)
    opened by bitcoina 3
  • implement better xor_encrypt, update uBitcoin

    implement better xor_encrypt, update uBitcoin


    In this PR I suggest a few changes in the encoding format of the lnurl data:

    • pack nonce together with encrypted data and HMAC
    • extend HMAC size to 8 bytes and cover everything with it
    • use base64url encoding instead of hex for shorter urls
    • make the whole thing more extendable (see below)

    Other changes:

    • improves nonce randomness (it was using random(9) for every byte, now it's random(256))
    • updates uBitcoin to the latest version that now includes base64 urlsafe encoding

    Encoding format

    Suggested data encoding has the following format:

    • first byte tells what encryption scheme is used - it's set to 0x01 for XOR-encryption (that is ok for data smaller than the key size), later we can extend it with other encryption formats like AES-CBC-HMAC and what not.
    • next we encode the nonce in the form <len><nonce>, in this implementation we use 8-byte nonce but it can be extended if required.
    • next we have the encrypted payload in the form <len><payload>
    • finally we have 8-byte HMAC (or more if needed). HMAC covers all the data before.


    Keys are derived from a shared secret. There are two keys - for encryption and for authentication. Round secret for encryption is calculated as hmac(key, "Round secret:" | nonce), HMAC at the end is calculated as hmac(key, "Data:" | payload).


    Payload is a simple XOR of the round key with actual data contains the following items:

    • PIN encoded as varint
    • Currency byte ($ for USD cents, can be extended to other currencies as well)
    • amount in cents encoded as varint Nice thing about varints is that it can encode any number between 0 and 2^64 with 1-byte overhead. For values up to 252 it takes only 1 byte. See

    Python decoding implementation

    Resulting LNURL can be decoded with the following python script (using embit library here, but can be easily adopted to any other bitcoin library):

    from embit import bech32
    from embit import compact
    import base64
    from io import BytesIO
    import hmac
    def bech32_decode(bech):
        """tweaked version of bech32_decode that ignores length limitations"""
        if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
                (bech.lower() != bech and bech.upper() != bech)):
        bech = bech.lower()
        pos = bech.rfind('1')
        if pos < 1 or pos + 7 > len(bech):
        if not all(x in bech32.CHARSET for x in bech[pos+1:]):
        hrp = bech[:pos]
        data = [bech32.CHARSET.find(x) for x in bech[pos+1:]]
        encoding = bech32.bech32_verify_checksum(hrp, data)
        if encoding is None:
        return bytes(bech32.convertbits(data[:-6], 5, 8, False))
    USD_CENTS = b'$'
    def xor_decrypt(key, blob):
        s = BytesIO(blob)
        variant =[0]
        if variant != 1:
            raise RuntimeError("Not implemented")
        # reading nonce
        l =[0]
        nonce =
        if len(nonce) != l:
            raise RuntimeError("Missing nonce bytes")
        if l < 8:
            raise RuntimeError("Nonce is too short")
        # reading payload
        l =[0]
        payload =
        if len(payload) > 32:
            raise RuntimeError("Payload is too long for this encryption method")
        if len(payload) != l:
            raise RuntimeError("Missing payload bytes")
        hmacval =
        expected =, b"Data:" + blob[:-len(hmacval)], digestmod="sha256").digest()
        if len(hmacval) < 8:
            raise RuntimeError("HMAC is too short")
        if hmacval != expected[:len(hmacval)]:
            raise RuntimeError("HMAC is invalid")
        secret =, b"Round secret:" + nonce, digestmod="sha256").digest()
        payload = bytearray(payload)
        for i in range(len(payload)):
            payload[i] = payload[i] ^ secret[i]
        s = BytesIO(payload)
        pin = compact.read_from(s)
        # currency
        currency =
        if currency != USD_CENTS:
            raise RuntimeError("Unsupported currency: %s" % currency)
        amount_in_cent = compact.read_from(s)
            raise RuntimeError("Unexpected data")
        return pin, amount_in_cent
    def extract_pin_and_amount(key, lnurl):
        # get normal url from lnurl
        url = bech32_decode(lnurl).decode()
        # get payload part
        payload = url.split("?p=")[1]
        # add padding
        if len(payload) % 4 > 0:
            payload += "="*(4-(len(payload)%4))
        # decode from urlsafe
        data = base64.urlsafe_b64decode(payload)
        pin, amount_in_cent = xor_decrypt(key, data)
        return pin, amount_in_cent/100
    if __name__ == "__main__":
        # shared key
        key = b"Enrt4QzajadmSu6hbwTxFz"
        # two example LNURLs
        lnurlarr = [
        for lnurl in lnurlarr:
            pin, amount_in_usd = extract_pin_and_amount(key, lnurl)
            print(f"Pin: {pin}, amount: ${amount_in_usd}")
    opened by stepansnigirev 1
  • LRURL Pay Error

    LRURL Pay Error

    The PoS have been working just fine, but recently, when I wanted to do another, this new one doesn't work. Everything is fine, the keypad woks, the display works, but there's seems to be a problem with the QRcode. The display shows the QRcode but when I scanned it with a LN wallet it shows this error: ¨An error occurred while attempting to retrieve an invoice from! Reason: Invalid character '<' This is in Breez but the same happens with other wallets, they say "invalid character '<' ". And when you try to pay with Muun wallet it doesn't even let you pay, it says: "This seems like a LNURL code. It allows you to receive bitcoin via lightning, instead of sending it. Do you want to use it?" But I think that's a different issue. If someone knows what that character means or how to fix it, please let me know. I really wanna use this PoS for accepting Bitcoin payments in my business.

    opened by Ganzosupremo 1
  • Using a clock chip instead of a screen

    Using a clock chip instead of a screen

    I have an idea of how to make this even cheaper by replacing the display with a clock chip (a cheap RTC).

    Let the URL contain an encrypted secret, and let the server add a timestamp to that secret using the current time and some simple maths (xor or similar). Other information may be encoded in as well. The result is returned as a PIN. User enters PIN, timestamp is removed by XOR with the device's time (use 30 second or minute intervals and check values for a few minutes back and forth) and compare the resulting PIN with the fixed PIN on the device. The device can even correct it's own time if it consistently gets PINs based on time in advance or in the past.

    Other information may be coded in as well.

    Example, a very simple slow car charger. Price is 100 sat per hour, maximum 8 hours:

    1. User scans fixed QR code with LNURLp and encrypted secret s.
    2. User pays 200 sat for 2 hours.
    3. Server decrypt s and xor decrypted s with encoded current time and 2 for 200 sat.
    4. Server returns PIN.
    5. User enters PIN.
    6. PoS terminal removes time and secret and check the result for valid values (1-8).
    7. In the first or few attempts it will get the valid value 2 and close the relay powering the charger for 2 hours.
    8. If all the 10 past PINs indicated a time in the future or past, advance or revert the RTC by one minute.

    The time and amount must be encoded in a way that minimizes the chance of a wrong amount becoming valid within a reasonable time scope.

    opened by sturles 1
  • Connecting a Lightning Network PoS to LoRaWAN

    Connecting a Lightning Network PoS to LoRaWAN

    Interesting work. I think it's a good step towards offline payments, but it still requires the customer to have connectivity. I'm looking for a system that enables payments in the case of an emergency, such as a large-scale blackout. LoRaWAN networks (e.g. Helium) would be a perfect technology to create a fail-safe method for connecting, since these devices don't require much power and can easily be kept online by emergency generators.

    So my idea would be a PoS that is connected to LoRaWAN and does not require the customer to have connectivity. The payment workflow would then look like this:

    1. The merchant enters the payment amount and creates a LN invoice.
    2. The merchant sends the invoice to the customer, who signs it and sends the signed message back to the PoS.
    3. The PoS uses its LoRaWAN connectivity to send the signed message off.

    I'm wondering if this is somehow possible?

    opened by tobias-kaiser 1
  • Reset satoshigo defaults. Defaults: 30 secs sleep timeout, no bat. display.

    Reset satoshigo defaults. Defaults: 30 secs sleep timeout, no bat. display.

    Reset default LNURLPoS endpoint details to

    Updated defaults for sleep timer - 30 seconds Battery display disabled by default until battery level code is fully tested on multiple devices.

    opened by blackcoffeexbt 0
  • Sleeping State Suggestions

    Sleeping State Suggestions

    The device does not appear to automatically go into PretendSleeping if the device is in a deep sleep mode. This means that if the device is sleeping and it is then plugged in, the device does not charge (according to Blackcofee). If it is possible, I believe we should trigger PretendSleeping as soon as a charging voltage is detected - although that might be a challenge since the device is in deep sleep and may not be able to execute those instructions from that state.

    Similarly, the device does not appear to go into a normal deep sleep automatically from a PretendSleeping state. Meaning, if I am charging the device and it is in the Pretend Sleeping state, but then I unplug it without waking it up, it seems to remain in the PretendSleeping state which likely will cause severe battery drain.

    While in normal deep sleep all keys in column 1 will wake the device. While in PretendSleeping state, only the reset (*) key will wake the device. This can be confusing for the user if there are different waking rules depending on the state.

    opened by adamsimecka 3
  • fixes weird qrcode and hmac_256 compile issues

    fixes weird qrcode and hmac_256 compile issues

    • I had problems with the compiler with the qrcode directory, I had to put it in a new director called qrhelper to get it to compile
    • there were two versions of the hmac_256 method in Bitcoin, causing a link error
    • Also update uBitcoin to 1.2
    opened by claytantor 0
Scroll pos - Provides some additional functions to ScrollController to define item position relative to the screen.

Scroll Position Provides some additional functions to ScrollController to define item position relative to the screen. A live version is available her

Kevin Vuilleumier 11 Nov 13, 2022
The KISS file manager: CLI-based, ultra-lightweight, lightning fast, and written in C

CliFM is a CLI-based, shell-like (non-curses) and KISS terminal file manager written in C: simple, fast, and lightweight as hell

leo-arch 819 Jan 8, 2023
PyMemoryExplorer - A lightning speed CPython object explorer via pure memory reading.

PyMemoryExplorer A lightning speed CPython object explorer via pure memory reading. Report Bug · Request Feature Table of Contents About The Project F

null 4 Dec 11, 2022
⚡️Lightning-fast linter for .env files. Written in Rust 🦀

⚡️ Lightning-fast linter for .env files. Written in Rust ?? Dotenv-linter can check / fix / compare .env files for problems that may cause the applica

dotenv-linter 1.5k Dec 30, 2022
A simple C library for sending messages over the lightning network

A simple C library for sending messages over the lightning network

William Casarin 44 Dec 23, 2022
A free, offline Pokémon Home alternative for Switch!

Eevee A free, offline Pokémon Home alternative for Switch! Features currently available: Bank Cloning Editing Features planned: Editing LGPE support O

Ben 21 Oct 20, 2022
🗺️ OMAPS.APP — Offline OpenStreetMap maps for iOS and Android. A community-driven fork of MAPS.ME.

OMaps is an open source cross-platform offline maps application, built on top of crowd-sourced OpenStreetMap data. It was publicly released for iOS and Android.

OMaps 4.4k Jan 7, 2023
Android-Login-Offline Simple login form in Java by Mahmoud Gaming

Android-Login-Offline Simple login form in Java by Mahmoud Gaming. I wanted to upload this project long time ago. This project is for experienced modd

Mahmoud Gaming 7 Mar 29, 2022
Organic Maps is a better fork of MAPS.ME, an Android & iOS offline maps app for travelers, tourists, hikers, and cyclists based on top of crowd-sourced OpenStreetMap data and curated with love by MAPS.ME founders.

?? Organic Maps is a better fork of MAPS.ME, an Android & iOS offline maps app for travelers, tourists, hikers, and cyclists based on top of crowd-sourced OpenStreetMap data and curated with love by MAPS.ME founders. No ads, no tracking, no data collection, no crapware.

Organic Maps 4.4k Jan 2, 2023
🦖This is a C port of Chrome's offline T-Rex Runner

?? Chrome T-Rex Runner (ported to C)

Shlomi Nissan 120 Nov 20, 2022
ContactGot is an offline desktop app, where clients can leave their info, while an administrator can manage which information they need to gather on certain projects.

ContactGot Contents Description How to use Requirements Engineering Installation Documentation Design Architecture Demonstration 1. Description During

Elizaveta 15 Sep 17, 2022
DeepSpeech is an open source embedded (offline, on-device) speech-to-text engine which can run in real time on devices ranging from a Raspberry Pi 4 to high power GPU servers.

Project DeepSpeech DeepSpeech is an open-source Speech-To-Text engine, using a model trained by machine learning techniques based on Baidu's Deep Spee

Mozilla 20.8k Jan 9, 2023
A GPS bicycle speedometer that supports offline maps and track recording

X-TRACK 开源GPS自行车码表。 拥有可显示实时位置的离线地图。 支持记录和显示实时轨迹以及导出标准GPX格式的轨迹文件。 全新设计的"页面生命周期管理"和"消息订阅发布框架"。 演示视频: GUI LVGL

_VIFEXTech 4k Jan 3, 2023
Command line tool for offline shader ISA inspection.

Intel Shader Analyzer Intel Shader Analyzer is a tool for offline static analysis of shaders for Intel GPU Architectures. It allows a user to compile

null 113 Jan 3, 2023
Offline fluid simulation solver adopted from

FluidEngine This is a fluid simulation engine for computer graphics applications. I adopt it from Doyub Kim's fluid-engine-dev. It's built on C++11 an

YangWC 55 Oct 26, 2022
A cross platform shader language with multi-threaded offline compilation or platform shader source code generation

A cross platform shader language with multi-threaded offline compilation or platform shader source code generation. Output json reflection info and c++ header with your shaders structs, fx-like techniques and compile time branch evaluation via (uber-shader) "permutations".

Alex Dixon 286 Dec 14, 2022
Pyramid is a free, open GUI tool for offline shader validation and analysis

Pyramid is a free, open GUI tool for offline shader validation and analysis. The UI takes HLSL or GLSL as input, and runs them through various shader compilers and static analyzers.

null 277 Dec 20, 2022
Official page of MLCPP (IROS'18 @ Barcelona, Spain): Offline Coverage Path Planner

MLCPP: Multi-layer coverage path planner for autonomous structural inspection of high-rise structures The purpose of the algorithm is to inspect high-

Sungwook Jung 14 Nov 2, 2022