This project is experimental. It is published as open-source to share the concept and invite the community to build on it, improve it, and take it further. Contributions, forks, and new ideas are all welcome.
Send Base (Ethereum L2) transactions over Meshtastic/LoRa mesh networks.
BaseMesh enables cryptocurrency transfers in off-grid environments using LoRa radio. Transactions are signed locally (private keys never leave your device), chunked to fit within LoRa's bandwidth constraints, and relayed through a gateway node to the Base network. Supports native ETH and ERC-20 token transfers (including USDC and BNKR). Built in collaboration with Mirra.
Sign an Ethereum transaction on your local device, send the signed transaction over the mesh to an internet-connected gateway that broadcasts it to Base. The gateway provides nonce and gas price on request so your transaction is built with current chain state.
Exchange Ethereum addresses with other mesh nodes over LoRa. Address sharing includes ACK-based delivery confirmation with automatic retry.
A gateway node holds a hot wallet. Remote offline nodes send authenticated transfer requests, and the gateway signs and broadcasts on their behalf. Requests are authorized by secp256k1 ECDSA signature verification against an Ethereum address allowlist.
pip install -e .Or with dev dependencies:
pip install -e ".[dev]"To enable the HTTP API (for Mirra Resource registration):
pip install -e ".[http]"basemesh wallet create --name mywalletYou will be prompted to type back one word from your mnemonic to confirm you have written it down. Use --skip-backup-check to skip this verification (e.g., for scripting).
This generates a BIP39 mnemonic (12 words) and derives an Ethereum keypair using the standard BIP-44 path (m/44'/60'/0'/0/0). The mnemonic is displayed once -- write it down and store it safely. The mnemonic is not stored on disk.
To create a wallet without a mnemonic:
basemesh wallet create --name mywallet --no-mnemonicbasemesh wallet recover --name restoredYou will be prompted for the mnemonic phrase and encryption passphrase. Compatible with MetaMask, Ledger, and other BIP-44 wallets using the standard Ethereum derivation path.
basemesh gateway --rpc-url https://sepolia.base.orgThe gateway validates the RPC connection and chain ID at startup. If the RPC is unreachable or the chain ID doesn't match your configuration, the gateway will exit with a clear error message.
The gateway broadcasts periodic beacons so clients can auto-discover it. Configure the beacon interval:
basemesh gateway --rpc-url https://sepolia.base.org --beacon-interval 120To enable Mode 3 (hot wallet transfers):
basemesh gateway --rpc-url https://sepolia.base.org --hot-wallet mywalletMode 1 - Sign locally and relay:
basemesh send relay \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 0.01 \
--gateway-node '!aabbccdd'You will be prompted to confirm before sending. Use --yes / -y to skip confirmation (for scripting).
The nonce and gas price are fetched automatically from the gateway.
With gateway auto-discovery (no need to know the gateway node ID):
basemesh send relay \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 0.01 \
--auto-discoverMode 3 - Request gateway to send from its hot wallet:
basemesh send request \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 0.1 \
--gateway-node '!aabbccdd'Send USDC (auto-resolves contract address for the configured network):
basemesh send relay \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 10.0 \
--usdc \
--auto-discoverSend BNKR (auto-resolves contract address for the configured network):
basemesh send relay \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 100.0 \
--bnkr \
--auto-discoverSend any ERC-20 token by contract address:
basemesh send relay \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 100.0 \
--token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
--decimals 6 \
--auto-discoverThe --decimals flag specifies the token's decimal places (default: 18). For --usdc, this is automatically set to 6.
basemesh balance --address 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 --auto-discoverERC-20 token balance:
basemesh balance --address 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 --usdc --auto-discoverbasemesh share-address --wallet mywallet --label "Field Node Alpha"Queue a transaction when no gateway is available:
basemesh send deferred \
--wallet mywallet \
--to 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 \
--amount 0.5 \
--mode 3The intent is stored locally at ~/.basemesh/queue.json. No mesh connection is needed. The passphrase is validated at queue time but not persisted to disk.
View pending intents:
basemesh queue list
basemesh queue list --status pending --jsonManually flush when a gateway is available:
basemesh queue flush --auto-discoverOr run a long-lived listener that auto-sends when a gateway beacon is detected:
basemesh listen --wallet mywalletThe listener caches your passphrase in memory and automatically signs and sends all pending intents whenever a gateway comes into range. Mode 3 intents are re-signed with a fresh timestamp at send time. Mode 1 intents fetch fresh nonce and gas price from the gateway before signing.
Remove intents:
basemesh queue remove <intent-id>
basemesh queue clear --yes
basemesh queue clear --status sent --yesThe gateway can expose a REST API for external integrations such as Mirra Resource registration. The API auto-generates an OpenAPI 3.0 spec at /openapi.json.
basemesh gateway --rpc-url https://sepolia.base.org --http-port 8420 --api-key your-secret-keyEndpoints:
| Method | Path | Description |
|---|---|---|
| GET | /v1/status |
Gateway info (uptime, chain, capabilities) |
| GET | /v1/balance/{address}?token= |
ETH or ERC-20 balance |
| GET | /v1/gas |
Current gas price and chain ID |
| GET | /v1/nonce/{address} |
Account nonce (transaction count) |
| POST | /v1/transfer |
Submit transfer from gateway hot wallet |
All endpoints require an X-API-Key header. Rate limiting is applied per API key.
# Check gateway status
curl -H "X-API-Key: your-secret-key" http://localhost:8420/v1/status
# Query balance
curl -H "X-API-Key: your-secret-key" http://localhost:8420/v1/balance/0x742d...
# Get OpenAPI spec (no auth required)
curl http://localhost:8420/openapi.jsonThe HTTP API requires optional dependencies: pip install basemesh[http]
Copy config.example.yaml to config.yaml and edit:
cp config.example.yaml config.yamlThen run with:
basemesh -c config.yaml gatewayThe gateway rate-limits requests per sender using a token bucket algorithm:
max_requests_per_minute: 10.0
rate_limit_burst: 3The gateway periodically broadcasts a beacon advertising its capabilities and uptime:
beacon_interval: 60 # secondsMode 3 requests are authorized by verifying the secp256k1 signature against the sender's Ethereum address. The allowed_requesters list contains checksummed Ethereum addresses:
allowed_requesters:
- "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
- "0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"An empty list allows all authenticated requesters.
The max_transfer_eth setting limits the maximum ETH amount per Mode 3 request:
max_transfer_eth: 0.1 # per-request limit in ETHFor ERC-20 token transfers, use max_transfer_token_units (raw token units, 0 = no limit):
max_transfer_token_units: 1000000000 # e.g., 1000 USDC (6 decimals)If both limits are unset, native ETH uses the max_transfer_eth default (0.1 ETH) and ERC-20 has no limit. Use the allowed_requesters allowlist for additional access control.
Global flags (apply to all commands):
--verbose/-v: Enable DEBUG-level logging--json-log: Output logs in structured JSON format (useful for production monitoring)--config/-c: Path to YAML config file
Send flags (send relay and send request):
--yes/-y: Skip the confirmation prompt before sending--check-balance: Check sender balance before sending (advisory warning only)--ack-timeout: Result wait timeout in seconds (default: 120)--discovery-timeout: Gateway discovery timeout in seconds (default: 120)
Wallet flags (wallet create):
--skip-backup-check: Skip mnemonic backup verification prompt
Deferred send flags (send deferred):
--mode/-m: Transfer mode:1(relay) or3(gateway request, default)
Queue flags (queue flush):
--wallet/-w: Only flush intents for this wallet--auto-discover: Auto-discover gateway via beacon--discovery-timeout: Gateway discovery timeout in seconds (default: 120)
Listen flags (listen):
--wallet/-w: Wallet name (required for passphrase caching)--gateway-node/-g: Gateway mesh node ID (optional, auto-discovers otherwise)
Gateway HTTP flags (gateway):
--http-port: Port for HTTP API server (enables REST API for Mirra)--api-key: API key for HTTP API authentication (required if--http-portis set)
Balance flags (balance):
--discovery-timeout: Gateway discovery timeout in seconds (default: 120)
| Network | Chain ID | RPC URL |
|---|---|---|
| Base Mainnet | 8453 | https://mainnet.base.org |
| Base Sepolia (testnet) | 84532 | https://sepolia.base.org |
| Token | Network | Address | Decimals |
|---|---|---|---|
| USDC | Base Mainnet | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
6 |
| USDC | Base Sepolia | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
6 |
| BNKR | Base Mainnet | 0x22aF33FE49fD1Fa80c7149773dDe5890D3c76F3b |
18 |
BaseMesh uses a compact binary protocol designed for LoRa's ~233-byte message limit:
- 10-byte header: magic (2B) + version (1B) + message type (1B) + message ID (2B) + chunk number (1B) + total chunks (1B) + payload length (1B) + CRC-8 (1B)
- Up to 210 bytes payload per chunk
- Magic bytes:
0x42 0x4D("BM") -- independent from SolMesh ("SM") - Addresses: 20-byte Ethereum addresses (saves 12 bytes vs Solana's 32-byte pubkeys)
- Signatures: 65-byte secp256k1 ECDSA (r + s + v) with EIP-191 personal sign
- A typical ETH transfer fits in 2 chunks
Message types: TX_CHUNK, TX_REQUEST, ADDR_SHARE, ACK, NACK, BALANCE_REQ, BALANCE_RESP, NONCE_REQ, NONCE_RESP, GAS_REQ, GAS_RESP, TX_RESULT, GATEWAY_BEACON
- Private keys are never transmitted over LoRa
- Wallet files are encrypted with AES-256-GCM (PBKDF2-derived key, 480K iterations)
- Wallet files are created with
0600permissions (owner read/write only) - Wallet names are validated to prevent path traversal attacks
- BIP39 mnemonic backup -- recovery phrase displayed once, never stored
- Standard BIP-44 derivation path:
m/44'/60'/0'/0/0(compatible with MetaMask, Ledger, etc.) - Mode 3 requests are authenticated via secp256k1 signatures using EIP-191 personal sign, verified against the sender's Ethereum address (not the spoofable mesh node ID)
- Gateway enforces an allowlist of authorized Ethereum addresses and per-transfer ETH limits
- Per-sender token bucket rate limiting protects the gateway from abuse
- Stale rate limiter entries are automatically cleaned up
- CRC-8 integrity check on all protocol messages (on top of LoRa's FEC)
- Chunk reassembly is keyed by
(sender_id, msg_id)to prevent cross-sender collisions - Chunk bounds validation prevents out-of-range chunk injection
- Gateway uses a local nonce counter with thread-safe locking to prevent nonce race conditions
- Amount conversion uses
Decimalarithmetic to prevent floating-point precision errors - Balance query failures return explicit error NACKs (not silent zero balances)
- Gateway validates RPC connection and chain ID at startup (fails fast on misconfiguration)
- ERC-20 transfer limits configurable via
max_transfer_token_units - Transaction confirmation prompts prevent accidental sends
- Mnemonic backup verification during wallet creation
- TX_REQUEST replay protection via 4-byte timestamp (5-minute window) and signature deduplication
- Hot wallet balance verified before spending (includes gas estimate for ETH)
- Store-and-forward queue contains no secrets (wallet names and public addresses only, no keys or passphrases)
- Queue file created with
0600permissions, atomic writes via write-tmp-rename - Passphrases for auto-flush are held in memory only (never written to disk)
- Deferred intents are re-signed at send time (no stale pre-signed transactions stored)
- HTTP API requires API key authentication on all endpoints
- HTTP API rate limiting uses separate token buckets from mesh senders
- HTTP API enforces the same transfer limits (
max_transfer_eth/max_transfer_token_units) as mesh Mode 3
IMPORTANT: READ THIS BEFORE USING BASEMESH
BaseMesh is experimental software provided "as-is" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
Loss of Funds Risk: Cryptocurrency transactions are irreversible. Errors in transaction construction, network configuration, or software bugs may result in permanent loss of funds. The authors and contributors are not responsible for any loss of cryptocurrency, tokens, or other digital assets resulting from the use of this software.
Self-Custody: BaseMesh is a self-custody tool -- keys are generated and stored locally on your device and are never sent to any server or transmitted over LoRa. You are solely responsible for your own wallets, private keys, mnemonic phrases, and funds. If you lose your private key or mnemonic phrase, your funds cannot be recovered by anyone. The authors and contributors do not have access to, do not store, and cannot retrieve your private keys or wallet files under any circumstances.
Hot Wallet Risk: Mode 3 gateway operation involves holding a hot wallet with real funds. The gateway operator is solely responsible for securing the hot wallet, configuring appropriate transfer limits and allowlists, and monitoring for unauthorized activity. The authors bear no responsibility for funds lost from hot wallets.
No Financial Advice: This software does not constitute financial advice. The authors make no recommendations regarding the purchase, sale, or transfer of any cryptocurrency or token.
Network Risks: LoRa mesh radio communication is subject to interference, range limitations, and potential eavesdropping. While transaction data is signed and integrity-checked, the mesh transport layer is not encrypted. Protocol messages (addresses, amounts, transaction hashes) are visible to any node on the same mesh network.
No Custody or Recovery: The authors and contributors do not custody, manage, or have any access to user wallets, private keys, or funds. There is no recovery service, no support process, and no mechanism by which the authors can access, freeze, reverse, or restore any wallet or transaction. You alone control your keys and bear full responsibility for their safekeeping.
Liability Limitation: The authors and contributors shall not be held liable for any loss of funds, lost keys, failed transactions, security breaches, hacks, or any other damages arising from the use of this software. By using BaseMesh, you agree to hold the authors and contributors harmless from any and all claims, losses, or liabilities.
Regulatory Compliance: You are solely responsible for compliance with all applicable laws and regulations in your jurisdiction, including but not limited to sanctions, export controls, tax obligations, money transmission laws, and financial regulations. The authors make no representations regarding the legality of using this software in any jurisdiction.
Testing Recommendation: Always test on Base Sepolia (testnet) before using real funds on Base Mainnet. Verify wallet creation, transaction signing, and end-to-end relay flow with testnet ETH before risking any real assets.
Use at your own risk.
pip install -e ".[dev,http]"
PYTHONPATH=src python3 -m pytest tests/ -v- Python 3.9+
- Meshtastic device (USB or WiFi connected)
meshtastic,web3,eth-account,click,pyyaml,cryptography,mnemonicPython libraries
Built with Mirra and their team.
This is an experimental project and we encourage the community to build on it. Whether it's bug fixes, new features, protocol improvements, or entirely new directions -- contributions are welcome. Fork it, open a PR, or just take the ideas and run with them.
MIT - Heathen