Android app to provision and wipe NTAG424 DNA NFC cards as Boltcards for a
LaWallet / Boltcard server. Tap a
blank card to write its keys and lnurlw, or tap a programmed card to wipe it — a
contactless, paywave-like experience for the Lightning Network.
This is the lawalletio fork of boltcard/bolt-nfc-android-app, with LaWallet integration, instance-aware QR-JWT login, a bulk-provisioning flow, tap-to-wipe, and license-free native NFC.
Android only.
v0.3.4 — see the latest release.
- No NXP TapLinX license required. Every NTAG424 operation (authentication,
key change, file settings, wipe, verification) runs in JavaScript via
react-native-nfc-manager- AES/CMAC (
crypto-js). The app never registers TapLinX, so there's no "TapLinX registration failed — provide a valid license" wall.
- AES/CMAC (
- Instance-aware login via a QR-encoded device JWT — scan the token from your LaWallet admin to authenticate against a specific server instance.
- Bulk Create — pick a card design, then tap blank cards one after another to provision them; an animated progress ring fills clockwise through each write step.
- Tap-to-wipe — tap a programmed card to read its UID, fetch its keys, wipe them to zero over NFC, and delete the card server-side.
- Read NFC — inspect any card's NDEF /
lnurlw.
- NXP NTAG424 DNA
- NXP NTAG424 DNA TT (Tag Tamper)
Download the APK from the latest release and install it on your Android phone.
⚠️ Release APKs are currently signed with the debug key (the production upload keystore is not committed to this repo). They install on any device but are not Play-Store-grade. See Releasing below.
This project is locked to a specific build toolchain. Versions are declared in repo config and consumed automatically by the helper scripts — no manual env-var juggling.
| Tool | Version | Source of truth |
|---|---|---|
| Java | Zulu 11.0.26 | .sdkmanrc |
| Node | 18.15 | .nvmrc, .node-version |
| Yarn | 1.x (classic) | installed by scripts/setup.sh |
| Gradle | 7.5.1 | android/gradle/wrapper/gradle-wrapper.properties |
| Android Gradle Plugin | 7.3.1 | android/build.gradle |
| Kotlin | 1.7.0 | android/gradle.properties |
| Android SDK | API 33 (Android 13) | android/build.gradle |
| Min SDK | API 23 (Android 6) | android/build.gradle |
| Build Tools | 33.0.0 | android/build.gradle |
| NDK | 23.1.7779620 | android/build.gradle |
Bumping any of these is a deliberate change — they're committed to git and shared across machines.
The release build targets real ARM hardware only —
reactNativeArchitectures=armeabi-v7a,arm64-v8a in android/gradle.properties —
dropping the x86/x86_64 emulator ABIs. This installs on the 32-bit
armeabi-v7a deployment device (the Z92 NFC handheld) and modern 64-bit phones,
cutting the release APK from ~91 MB to ~53 MB. Hermes is enabled. To build for an
x86 emulator, override the ABI on the command line:
./gradlew assembleRelease -PreactNativeArchitectures=x86_64The split is enforced with
ndk { abiFilters }on the app module and onreact-native-vision-camera(in the rootsubprojectsblock) — with ABI splits disabled, library CMake modules don't otherwise honorreactNativeArchitectures, and a mismatch makes vision-camera fail to find reanimated's prefab (CXX1210).
Requires Android Studio + Android SDK installed separately (see https://reactnative.dev/docs/environment-setup → "React Native CLI Quickstart").
git clone <repo>
cd card-installer
yarn setup # installs SDKMAN, nvm, Zulu 11, Node 18.15, yarn, JS depsThe setup script is idempotent — safe to re-run after pulling changes.
After setup:
cp .env-example .envand fill in any required values. (The native NFC path no longer needs an NXPMIFARE_KEY.)- Connect an Android device with USB debugging enabled, or start an emulator.
- Build & run (see commands below).
yarn build:debug # build debug APK (no install)
yarn build:release # build signed release APK (arm64-v8a)
yarn build:bundle # build AAB for Google Play
yarn android # build + install debug on connected device/emulator
yarn start # start Metro bundler
yarn clean # gradle clean
yarn clean:full # nuke gradle daemons + local caches (use after JDK changes)All build commands route through ./scripts/build, which sets JAVA_HOME to a
JDK 11 install automatically (via SDKMAN, then macOS java_home, then known
Linux JDK paths). You don't need to set JAVA_HOME manually.
The release signingConfig uses the upload keystore at
android/app/my-upload-key.keystore when present, and falls back to the debug key
when it isn't (so test builds still sign and install). For a production-grade build:
- Place your upload keystore at
android/app/my-upload-key.keystore. Keep its credentials out of committed files — put them in~/.gradle/gradle.propertiesor environment variables. yarn build:bundle→android/app/build/outputs/bundle/release/app-release.aab- Upload the AAB to the Google Play Console.
First-time keystore generation:
keytool -genkeypair -v -keystore android/app/my-upload-key.keystore \
-alias onesandzeros-key -keyalg RSA -keysize 2048 -validity 10000Click to expand
- Install Android Studio + Android 13 (API 33) SDK, build-tools 33.0.0, NDK 23.1.7779620
- Install SDKMAN:
curl -s https://get.sdkman.io | bash - In the repo root, run
sdk env install(reads.sdkmanrc, installs Zulu 11) - Install nvm: https://github.com/nvm-sh/nvm
- In the repo root, run
nvm install(reads.nvmrc, installs Node 18.15) npm install -g yarnyarn installcp .env-example .env- Connect a device or start an emulator
yarn android
- Login — on the Login tab, scan the QR-encoded device token from your LaWallet admin. This authenticates the app against that server instance.
- Bulk Create — pick a card design, tap Tap Card to Write, then hold a blank
NTAG424 card to the phone. The progress ring fills clockwise as the keys and
lnurlware written and verified. Provision more cards by tapping them in turn. - Wipe Card — tap a programmed card; the app reads its UID, fetches the reset
keys from the server's
/api/cards/:id/wipeendpoint (which also unpairs the card from its user), wipes all keys to zero over NFC, clears the NDEF, and deletes the card server-side. - Read NFC — tap any card to inspect its
lnurlwURL and PICC/CMAC parameters.
⚠️ Writing/wiping is destructive. If you lose a card's keys you may be unable to reprogram it. Do not move the card until an operation completes.
- NTAG424 keys are generated and held by your LaWallet / Boltcard server. Regular
card reads (
GET /api/cards/:id) never expose them — the app fetches keys only from the dedicated/write(program) and/wipe(reset) endpoints, each of which unpairs the card from its user as a side effect of exporting its keys. - Keep your keys secret and avoid other listening NFC devices in range while writing.
- Do not commit signing credentials. If real keystore passwords were ever
committed to
android/gradle.properties, rotate them and move the values to~/.gradle/gradle.propertiesor environment variables.
Bulk Create now mints a single-use /write token after creating each card and
fetches the replay-protected, tokenized /write endpoint on the reachable
instance host, so the server-derived lnurlw_base (public scan host) is burned
onto the chip. A POST-keys fallback is retained for older servers without the
/write-token route.
Read NFC: Activate Card — mint a single-use activation QR for a registered
LaWallet card (the end-user scans it with their wallet to claim the card). Wipe
flow now fetches reset keys from the server's /wipe endpoint.
License-free native NFC (TapLinX no longer registered); write progress ring that fills clockwise with a success animation; tap-to-wipe flow; instance-aware QR-JWT login; bulk provisioning; NFC read/write UX overhaul; Android 16 / Pixel 9 startup & camera fixes.
Various fixes to attempt to prevent card programming errors.
Added support for random UID to increase privacy.
MIT. This is a fork of boltcard/bolt-nfc-android-app (also MIT); the original copyright is retained in the LICENSE alongside the fork's.