An off-grid protocol for transporting pre-authorized Sepolia ETH claims over Meshtastic and redeeming them later on-chain.
Radio Note Protocol is the core system in this repository. It defines a model where real ETH is locked in a Sepolia smart contract, off-grid redemption notes are prepared in advance, those notes are transported over Meshtastic, and the receiver later redeems on-chain.
Radio Note Wallet is the reference local wallet and UI included in this repo for creating wallets, preparing notes, sending them over radio, receiving them, and redeeming them.
This is not "ETH over LoRa" in the literal sense. The radio leg carries signed redemption data for value that was already reserved in the contract. The ETH itself stays in RadioNoteVault on Sepolia until the receiver redeems.
Experimental prototype. This repo is for Sepolia + Meshtastic testing and protocol exploration, not production custody.
This project explores a practical off-grid crypto transport model:
- keep settlement anchored to Ethereum on Sepolia
- move value-bearing claims over Meshtastic
- redeem later when the receiver is back online
What the app already does:
- runs a local seed-based Sepolia wallet inside the app
- shows Sepolia ETH balance and supports normal on-chain ETH send
- uses the
RadioNoteVaultcontract for deposit, prepare, withdraw, cancel, and redeem flows - uses a Python Meshtastic USB bridge for radio transport
- sends only prepared off-grid chunks, not live on-chain transactions
- checks the recipient node before sending
- transmits radio-note bundles in chunks with sender and receiver progress
- tracks redeem status from Sepolia contract state, not just mesh messages
- presents the flow in a retro local UI aligned with the
blackbox_nodestyle
- A wallet is created locally inside the app.
- The sender deposits Sepolia ETH into
RadioNoteVault. - The sender prepares one or more off-grid chunks on-chain. Each prepared chunk reserves value inside the contract under a unique
noteIdand expiry. - When sending, the app checks the recipient node, signs recipient-bound redemption notes for the selected prepared chunks, bundles them, and transports them over Meshtastic.
- The receiver stores those notes locally and sees them as claimable offline value.
- When internet is available again, the receiver redeems the notes on Sepolia and the contract releases ETH to the receiver wallet.
Sender-side redemption visibility is confirmed from the smart contract. The sender does not have to trust a radio message to know whether a note was redeemed.
The UI revolves around four main states:
-
On-chain ETHETH sitting directly on the local wallet address. -
Vault AvailableETH already deposited intoRadioNoteVault, but not yet prepared for off-grid transfer. -
Spendable Off-gridETH already prepared as off-grid chunks and ready to send without internet. -
Claimable (offline)Notes received over Meshtastic that are valid for the local wallet but not yet redeemed on-chain.
Under the hood, prepared notes are represented by reserved commitments in the vault contract. The send slider builds a summed send amount from those prepared chunks.
- Meshtastic node discovery
- recipient
Check - off-grid send from already prepared chunks
- receiving radio notes
- viewing incoming transfer progress
- viewing
Claimable (offline)value
- reading fresh Sepolia balances
- depositing to the vault
- withdrawing from the vault
- preparing off-grid chunks
- redeeming received notes
- normal on-chain ETH send
- sender-side refresh of redeemed note state
- Install Node.js 18+ and Python 3.10+.
- Install project dependencies:
npm install
python -m pip install -r requirements.txt- Create
.envfrom.env.exampleand set at leastSEPOLIA_RPC_URL. - Compile and deploy
RadioNoteVault, or point the app at an existing Sepolia deployment. - Start the app:
npm start- Open
http://127.0.0.1:7861, create a wallet inSettings, fund it with Sepolia ETH, deposit into the vault, prepare chunks, and send off-grid fromSend.
- Node.js 18+
- Python 3.10+
- a Meshtastic device connected over USB
- a Sepolia RPC endpoint
- Sepolia ETH for testing
npm install
python -m pip install -r requirements.txtCreate .env from .env.example.
Example:
# Sepolia RPC endpoint used by the wallet app and deploy scripts
SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
# Optional legacy alias; leave empty unless you explicitly want to override SEPOLIA_RPC_URL
RPC_URL=
# Deployed RadioNoteVault contract address on Sepolia
CONTRACT_ADDRESS=0x0000000000000000000000000000000000000000
# Optional fixed Meshtastic serial port, for example COM7 on Windows
MESHTASTIC_PORT=
# Optional Python executable override for the Meshtastic bridge
# Windows examples: python, py
# macOS/Linux examples: python3, /usr/bin/python3
PYTHON_EXECUTABLE=
# Test-only deployer key for Sepolia contract deployment
DEPLOYER_PRIVATE_KEY=0xYOUR_TESTNET_PRIVATE_KEY-
SEPOLIA_RPC_URLSepolia RPC used by the wallet app and deploy script. -
RPC_URLOptional legacy alias. The code prefersSEPOLIA_RPC_URL. -
CONTRACT_ADDRESSAddress of the deployedRadioNoteVaultcontract. -
MESHTASTIC_PORTOptional fixed serial port. Examples:COM7,/dev/cu.usbmodemXXXX,/dev/ttyACM0 -
PYTHON_EXECUTABLEOptional override for the Python command used to launchbridge.py. Examples:python,py,python3,/usr/bin/python3 -
DEPLOYER_PRIVATE_KEYTest-only key used to deploy the contract.
Compatibility note: scripts/deploy.js also accepts legacy aliases WALLET_PRIVATE_KEY and RPC_URL.
The app logic is cross-platform. The OS-specific part is the Meshtastic USB serial bridge in bridge.py.
- usually works with
pythonandCOMports such asCOM7 - if
pythonis not available inPATH, setPYTHON_EXECUTABLE=py
- serial ports usually look like
/dev/cu.usbmodem*or/dev/cu.usbserial* - if needed, set
PYTHON_EXECUTABLE=python3 - terminal access to USB serial devices may need to be allowed
- serial ports usually look like
/dev/ttyACM0or/dev/ttyUSB0 - if needed, set
PYTHON_EXECUTABLE=python3 - you may need serial permissions, for example:
sudo usermod -aG dialout $USERThen log out and back in.
npm run contract:compileArtifact output:
artifacts/RadioNoteVault.json
npm run contract:deployThe deploy script prints the Sepolia contract address. Put that address in:
.envasCONTRACT_ADDRESS- or the app
Settings
npm startOpen:
http://127.0.0.1:7861
The backend tries Python commands in this order:
-
Windows:
PYTHON_EXECUTABLEif set, thenpython, thenpy -3 -
macOS / Linux:
PYTHON_EXECUTABLEif set, thenpython3, thenpython
If the bridge does not start, the first fix to try is setting PYTHON_EXECUTABLE explicitly in .env.
- Create wallet.
- Fund it with Sepolia ETH.
- Deposit
0.02 ETHinto the vault. - Prepare:
0.002 ETH0.008 ETH0.01 ETH
- Create wallet.
- Make sure Meshtastic is connected.
- Open
Receive.
- On Node A, open
Send. - Choose Node B.
- Press
Check. - Move the prepared slider to the amount you want.
- Press
Send off-grid.
- On Node B, watch incoming chunk progress.
- Confirm the note appears in
Receive -> Radio Notes. - Confirm
Homeshows claimable offline value.
- On Node B, restore internet if it is offline.
- Press
Redeem. - Confirm on-chain ETH increases on Node B.
- On Node A, refresh and confirm
Activityeventually showsnote_redeemed.
- wallet address
Spendable Off-grid ETHOn-chain ETH- claimable offline amount
- live incoming transfer progress
- wallet address and QR code
- incoming radio-transfer progress
- received radio notes
- redeem button
- recipient node selection
Checkhandshake- prepared-amount slider
- off-grid send progress
- normal on-chain ETH send
- latest sent transfer card and older history
- vault balances
- deposit into vault
- prepare off-grid chunks with expiry
- ready off-grid inventory
- withdraw available vault ETH
- on-chain events
- vault events
- announcement and handshake events
- sent, delivered, and redeemed radio-note events
- RPC URL
- contract address
- Meshtastic port
- wallet creation
- seed reveal
- wallet delete / reset controls
Screenshot assets are not included in the repo yet.
If you want the README to render screenshots, add PNG files here:
docs/screenshots/home.png
docs/screenshots/prepare.png
docs/screenshots/send.png
docs/screenshots/receive.png
docs/screenshots/settings.png
Recommended captures:
HomewithSpendable Off-grid ETH,On-chain ETH, and claimable offline value visiblePreparewith deposit flow, vault balances, and prepared chunksSendwith recipient check and prepared-amount sliderReceivewith incoming chunk progress or received radio notesSettingswith RPC, contract, Meshtastic port, and wallet controls
-
contracts/RadioNoteVault.solSolidity vault contract for deposits, commitments, redemption, and expiry cancellation -
scripts/compile.jsContract compile script -
scripts/deploy.jsContract deploy script -
server.jsNode backend, wallet logic, vault integration, off-grid protocol flow, and local API -
bridge.pyPython Meshtastic USB bridge -
static/index.htmlUI markup -
static/wallet-ui.jsFrontend wallet logic -
static/wallet-ui.cssFrontend wallet styling -
data/Local runtime state, wallet data, settings, logs, and cached summaries -
docs/screenshots/Optional README screenshots
- click the device status block
- choose a specific port
- press
Connect
- connect the device over USB
- set
MESHTASTIC_PORTif auto-detect picks the wrong port - confirm the serial device is visible to the OS
Install the bridge dependencies:
python -m pip install -r requirements.txtThis means your wallet may have On-chain ETH, but Vault Available is still 0.
Fix:
- Open
Prepare. - Deposit ETH into the vault.
- Then prepare off-grid chunks.
Your RPC URL is wrong, unavailable, or rate-limited. Set a valid SEPOLIA_RPC_URL.
Sender-side redemption status comes from the smart contract, not from a radio message.
Make sure the sender has:
- internet access
- a valid Sepolia RPC
Then refresh the app state.
This is a Sepolia testnet prototype for Meshtastic and off-grid note transport experiments.
- do not treat it as production-grade custody software
- do not use mainnet funds
- protect the local seed phrase like a real wallet secret
- expect rough edges in transport, local state handling, and recovery flows
The app writes sensitive local state under data/ and the repo root, including:
data/wallet.jsondata/settings.jsondata/radio_state.jsondata/logs.jsondata/wallet_summary.json.env
Keep those files local. The repo already includes .gitignore and .env.example to support that workflow.