Skip to content

Repository files navigation

LNURLcard

A credit-card-shaped, standalone Lightning wallet — eInk display, ESP32, WiFi, NFC, USB-C, no battery, as thin as possible. Concept sketch: lnurl-card.jpg ("LNURL CASH" / "C4RD").

Unlike its sibling repo lnurl-vault — an ESP32-S3 firmware that is deliberately a dumb, offline vault with no networking of its own, paired to a browser over USB-CDC/BLE — lnurl-card is a standalone wallet that makes its own LNURL HTTP calls over WiFi, while still supporting the existing paired-companion mode for compatibility. It also adds a capability lnurl-vault never had at all: NFC, in two roles — a tap-to-pay passive tag that must work with zero board power, and an active reader/writer for provisioning/reading other tags.

This is a real, money-custody device headed to PCB fab + assembly (JLCPCB/PCBWay-style PCBA). The hardware design below is an engineering spec and KiCad skeleton — routing (especially the NFC antennas) is intentionally left for a real KiCad session, DRC/ERC, and ideally a second set of EE eyes before anything is sent to a fab.

Confirmed design decisions:

  • NFC must work unplugged. A passive tag chip, powered only by the reader's field, holds the tap-to-pay payload. The ESP32 rewrites it when online.
  • Input is capacitive touch, not mechanical buttons — zero added thickness, matches "as thin as possible."

One correction made during research, not present in the original sketch: NTAG 424 DNA (the Boltcard-standard chip, chosen for the passive tag) has no I2C interface — it's contactless-only. The ESP32 cannot write it over a wired bus. It can only be (re)written the way Boltcard provisioning already works in the wild (see bitcoin-ring/Bolty, an ESP32+PN532 Boltcard provisioner — direct prior art): an active NFC front-end performs an RF write while the tag is in its field. For a self-contained card that means the passive tag's antenna and the active front-end's antenna both live inside the same PCB, near-field coupled to each other. This is the single hardest EE problem in the whole design (see below).

Hardware architecture

Block diagram

USB-C (mid-mount SMD receptacle, VBUS 5V + D+/D- to ESP32-S3 native USB)
   │
   ▼
Buck regulator (5V→3.3V) ──┬── ESP32-S3FH4R2 (bare QFN56, embedded flash+PSRAM)
                            │        │SPI (eInk)      │SPI (NFC active)  │I2C (touch controllers)
                            │        ▼                ▼                  ▼
                            │   eInk panel        ST25R3916B         CAP1208 ×2
                            │   (2.13" flex,       NFC front-end     (8-ch cap-touch
                            │   SSD1680, 0.44mm)   UFQFPN-32         controller, I2C)
                            │                          │                  │
                            │                     [coupling coil]    16 copper pads
                            │                          │              (silkscreened per
                            │                     NTAG 424 DNA           sketch: 0-9,
                            │                     (passive tag,          confirm/cancel/
                            │                      RF-only, own          arrows)
                            │                      perimeter loop
                            │                      antenna — reads
                            │                      from BOTH the
                            │                      internal coupling
                            │                      coil AND external
                            │                      POS/phone readers)
                            │
                            └── 3.3V rail also feeds ST25R3916B, CAP1208s, eInk

ESP32-S3's native BLE stays available (matches lnurl-vault's transport) alongside its WiFi station role — both ride the same radio silicon, no extra chip.

Component selection (real, sourceable parts)

Function Part Package Why
MCU ESP32-S3FH4R2 (bare die, not a WROOM module) QFN56, 7×7mm, ~0.9mm thick Embedded 4MB flash + 2MB PSRAM in-package avoids a separate flash chip. A WROOM-1 module is 3.1mm tall — over 2mm thicker than the bare chip for the same silicon. Directly reuses lnurl-vault's S3 firmware base (native USB-CDC, BLE, HW RNG).
Display Good Display GDEM0213I61-class 2.13" flexible eInk, 212×104, SSD1680 driver, SPI, FPC tail 0.44mm panel Thinnest sourceable eInk in the size range that fits the sketch's left-third display zone on an 85.6×54mm card. SSD1680 is ESP-IDF-friendly (same driver family as many Waveshare modules).
Active NFC (reader/writer + tag-provisioning) ST25R3916B UFQFPN-32, ~0.5mm Full ISO14443A/B reader+writer, SPI, strong antenna-tuning documentation from ST (needed given the dual-antenna problem below). Chosen over PN532: bare-die option (PN532 is normally sold as a thicker LQFP44 module part), better RF documentation for a from-scratch antenna.
Passive tap tag NXP NTAG 424 DNA (NT4H2421Gx) Small WLCSP/SOT die option Contactless-only, no I2C (see correction above). SUN/CMAC dynamic authentication — the exact chip Boltcard uses for LNURL-withdraw tap-to-pay, so LNURLcash/LUD-25 payload semantics carry over directly.
Touch input Microchip CAP1208 ×2 (8-channel cap-touch controller, I2C) TSSOP/QFN, thin Resolves a real mismatch: ESP32-S3's native touch peripheral has ~14 channels, not the 16 the sketch's keypad needs. Two I2C controllers give exactly 16 channels, free up GPIO for the two NFC buses + eInk SPI, and hand firmware a clean interrupt+I2C-read contract instead of raw touch-ADC calibration.
Power Small synchronous buck, 5V→3.3V (e.g. TI TLV62569 or similar, SOT-23, <1mm) SOT-23 No battery means no charge/fuel-gauge circuitry at all — this is a strict simplification vs. a typical wallet.
USB-C Mid-mount SMD 24-pin receptacle (e.g. GCT USB4105/4110-class or Hirose CX70-class) ~0.5mm above PCB centerline vs. ~1.6-3.2mm for top-mount This is the single biggest thickness lever available. A standard top-mount USB-C jack would by itself set the card's minimum thickness above any realistic "credit card" target.

Power budget

No battery simplifies this to "must never draw more than USB-C VBUS can supply, with margin for a basic (non-PD) 5V/500mA-900mA host port": ESP32-S3 WiFi TX peaks (~300-400mA), ST25R3916B field generation for active reading (~100mA+ when driving), eInk (negligible except brief refresh current spikes), CAP1208×2 (µA-range, always-on). Budget for a full current-limited 900mA rail and never assume a PD-negotiated 1.5A+ source, since the card should work off cheap USB-A→C cables and basic hub ports.

Thickness stack-up (honest estimate, not a promise)

Layer Thickness
PCB substrate (thin core, 4-layer likely needed for NFC ground clearance under the antenna area) ~0.6-1.0mm
Bare ESP32-S3 QFN ~0.9mm
ST25R3916B / CAP1208 dies ~0.5mm each, doesn't stack with MCU (different placement)
eInk panel + adhesive ~0.6-1.0mm
Mid-mount USB-C connector ~0.5-1.0mm above PCB centerline (dominant single item)
Solder mask, silkscreen, coverlay over touch pads ~0.1-0.2mm

Realistic total: roughly 2.5-3.5mm, not the 0.76mm of an ISO/IEC 7810 card. Card length and width (85.60 × 53.98mm, ID-1) are easy to hit exactly; thickness in that range is honest "as thin as we can reasonably make an eInk+ESP32+dual-NFC+USB-C board," not "literal credit-card thin." A pogo-pin/dock charging approach (dropping the USB-C receptacle for edge contacts + a separate dock) would get closer to true card thickness, but wasn't made the default since the sketch explicitly lists USB-C as an on-card connector — worth a follow-up if thickness ends up mattering more than a standard port.

The hard problem: dual-antenna NFC

The passive NTAG424 DNA needs a well-tuned perimeter loop antenna for good read range against external POS/phone readers. The active ST25R3916B needs its own antenna to (a) provision that same tag over RF from a few mm away, and (b) read/write external tags when something else is tapped against the card. Two coils sharing one small PCB will couple to and detune each other unless carefully separated/tuned. This is the one piece of this design that genuinely needs either lab measurement (network analyzer, ST's NFC tuning app notes/tools) or a specialist's review — nobody should hand-wave exact turn counts or trace widths without that.

KiCad project (hardware/)

A real KiCad 10 install (kicad-cli, no symbol/footprint libraries) became available partway through building this, which upgraded the PCB from a hand-authored guess to a generated, DRC-checked board — see hardware/README.md for the full account, including two real bugs the tooling caught (a ; comment that doesn't exist in KiCad's file grammar, and a footprint-position API-ordering mistake that collapsed every part toward the origin and produced ~1500 false DRC violations before being found and fixed).

  • hardware/lnurl-card.kicad_pcb is generated by hardware/build_pcb.py (KiCad's pcbnew Python API, since no footprint libraries exist here to place symbols from) and currently passes kicad-cli pcb drc with only 12 violations, all of them the same expected, by-design finding: the USB-C connector's pads at the board edge, correct for an edge-launched mid-mount part. Real footprints, real net assignments per hardware/NETLIST.md, a two-sided layout (front: display connector, touch grid, USB-C; back: every IC — the front alone can't physically fit both the keypad and six ICs on an 85.6×54mm card, confirmed by the ~1500-violation first attempt).
  • Routing is intentionally left undone — including the two NFC antenna loops — needing real RF measurement this environment has no way to perform. 13 nets are unrouted/ratsnest, honestly, not hidden behind a fake example trace (one existed briefly during development, was wrong, and was removed rather than fixed cosmetically).
  • hardware/lnurl-card.kicad_sch is still title-block-only — no scripting API covers schematic capture the way pcbnew covers the board, so placing real wired-up symbols blind carried the same risk the PCB had before build_pcb.py existed. It ERCs clean, but trivially (nothing in it yet).
  • Before ordering anything: get symbol/footprint libraries installed, do real schematic capture, replace the three _APPROX-flagged footprints (USB-C, NTAG424 DNA, eInk FPC) with real vendor footprints, route everything (antennas need bench measurement or a specialist), re-run DRC/ERC, get a DFM review from whichever fab runs the PCBA.

Firmware architecture

Extends lnurl-vault's existing module boundaries; the paired-companion protocol (src/proto/dispatcher.c and its wire commands) stays a strict superset — never modified, only additively extended — so it remains compatible with lnurl-wallet's existing device.ts client. Standalone WiFi/LNURL flows are a second, independent local-interaction path that drives vault_* directly under vault_lock, mirroring how lnurl-vault's own local browse/unveil gesture already bypasses the transport layer today.

New/changed modules (relative to lnurl-vault's src/ layout)

  • vault/, ota/unchanged, vendored as-is (portable, no ESP-IDF dependency).
  • proto/dispatcher.c's command set is additive only (new optional capabilities fields: wifi, standalone, nfc_passive, nfc_active, touch_pads, battery: false; new gated commands wifi_provision, nfc_rewrite following the same physical-confirm pattern as export_secret). button_fsm.c → new touch_fsm.c (portable, same shape). screen_sleep.c → new eink_refresh.c (different problem: ghosting/refresh cadence, not IPS burn-in). approval.c/approval.hreused verbatim: it already takes two booleans (approve_pressed/cancel_pressed), not raw GPIO reads, so the existing 2-second-hold / debounce / tie-break logic carries over to touch pads with zero change to that file.
  • net/new: wifi_prov.c, http_client.c (esp_http_client/esp_tls), lnurl_client.c (portable core — this is where lnurl-wallet's deviceOrchestration.ts logic gets ported to C, executed on-device instead of in a browser, per the flow docs/PROTOCOL.md already documents).
  • nfc/new: nfc_tag.c (NTAG424-class passive tag, written over RF via the active front-end, not I2C), nfc_reader.c (SPI, ST25R3916-class active reader/writer), boltcard.c (portable SUN/CMAC/AES key-diversification logic, host-testable like ota/ota_sign.c).
  • storage/nvs_storage.c reused; new namespaces for WiFi credentials and NFC key material, each getting its own explicit at-rest-trust decision in SECURITY.md rather than silently inheriting lnurl-vault's "NVS encryption off, physical possession is the model" call — the blast radius differs (a cloned NFC key is indefinite; a leaked note secret is one bearer note).
  • ui/display.h's state/content model is panel-agnostic and reused; new eink_display.c backend with a dirty-rect/partial-refresh scheduler (must never fire a full-refresh flash mid confirm-hold — same "don't look broken during a security gesture" bar lnurl-vault's own progress-bar UI was built to satisfy). buttons.ctouch.c (thin adapter over touch_fsm.c). ui_task.ckeypad_ui_task.c.
  • power/ — new, small: mostly documents that the passive NFC tag is the only thing that ever operates with the MCU unpowered; no battery means no graceful-shutdown logic to write.

Key risks flagged for firmware

  • ESP32-S3's native touch peripheral has ~14 channels, not 16 — resolved by moving to two external CAP1208 I2C controllers rather than assuming native touch stretches to fit.
  • eInk partial-refresh scheduling must be excluded from firing during an in-progress confirm hold.
  • I2C (passive tag path) and SPI (active reader path) don't contend on the bus, but the two antennas' RF coupling means firmware likely needs an explicit mutual-exclusion state machine between nfc_tag.c and nfc_reader.c (e.g., silence the active field while rewriting the tag).
  • WiFi/TLS (esp-tls, mbedTLS, esp_http_client) adds real RAM on top of what lnurl-vault already carries (cJSON, BLE, now an eInk framebuffer) — budget for possibly needing PSRAM, and report a measured pio run RAM/flash number once buildable, same discipline lnurl-vault's README already sets (36.3% RAM / 27.2% flash, not a guess).
  • Own HTTPS cert validation is new (lnurl-vault never needed it — the browser owned TLS). Use ESP-IDF's Mozilla crt_bundle as a fallback, but lean primarily on LUD-25's existing mint-key signature check (confirm's sig field, already validated in vault.c) as the real trust anchor for a device holding bearer cash.
  • WiFi provisioning UX: primary path is BLE-delivered credentials over the existing, already-tested companion transport; on-device 16-pad SSID/PSK entry is a fallback only, not the primary flow.
  • CONFIG_ESP_WIFI_ENABLED=y directly reverses lnurl-vault's core "no on-device networking" design principle — documented as prominently here as its absence is documented in lnurl-vault's, not as a silent flip.

Build/config

Mirrors platformio.ini's structure: [env:lnurl-card-v1] pinned to the same espressif32@7.0.1, plus a [env:native] for the portable core extended with boltcard.c and lnurl_client.c's parsing tests. sdkconfig.defaults deltas: flip CONFIG_ESP_WIFI_ENABLED to y, add mbedTLS/esp-tls tuning + CONFIG_MBEDTLS_CERTIFICATE_BUNDLE, keep NimBLE/BT enabled unchanged, both I2C and SPI ride the existing driver component. Vendor the eInk and ST25R3916B drivers via a pinned-commit + SHA-256-checked script, following the exact precedent tools/vendor_qrcode.sh already set in lnurl-vault for a library that doesn't resolve cleanly through PlatformIO's LDF under framework=espidf. Enlarge partitions.csv for WiFi credential storage and the bigger TLS-carrying app partition; keep the existing OTA dual-slot shape.

Repo layout

lnurl-card/
  hardware/     KiCad project (schematic sheets + board outline, see above)
  src/          firmware, mirroring lnurl-vault's src/ layout as described above
  test/native/  test_touch_fsm.c, test_boltcard.c, test_lnurl_client.c
  docs/         PROTOCOL.md (paired-companion, unchanged, vs. new standalone-mode section,
                vs. new NFC/Boltcard section), HARDWARE-TEST-CHECKLIST.md (mirrors lnurl-vault's)
  SECURITY.md   new at-rest-trust decisions for WiFi creds + NFC keys, written out explicitly
  platformio.ini, sdkconfig.defaults*, partitions.csv, CMakeLists.txt

Verification

  • Firmware: pio run -e native for the portable-core unit tests (touch FSM, Boltcard CMAC/SUN known-answer vectors, LNURL/bech32 parsing edge cases), then pio run -e lnurl-card-v1 for a real build once hardware exists, reporting actual RAM/flash usage the way lnurl-vault's README does.
  • Hardware: open hardware/lnurl-card.kicad_pro in a real KiCad install, run ERC then DRC, finish routing (especially both NFC antennas — ideally bench-tuned against real ST25R3916B eval hardware before committing trace geometry), get a fab DFM review, before ordering boards or assembly.
  • A docs/HARDWARE-TEST-CHECKLIST.md mirroring lnurl-vault's should cover: WiFi join + TLS handshake to a real LNURL mint, NFC tag write-then-external-tap-read round trip, NFC active-reader read of a known tag, all 16 touch pads individually, eInk full vs. partial refresh visually, USB-C enumeration, BLE companion-mode parity test against lnurl-wallet's existing device.test.ts.

About

LNURLcard is a new credit-card-shaped, ESP32-based Lightning device with eInk display, USB-C, WiFi (online), NO BATTERY, NFC, a 4×4 keypad

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages