IoT Development
Neurai gives a microcontroller three things it does not get from a cloud platform: keys it controls, a public ledger that says what it owns and which groups it belongs to, and an encrypted channel to the other members of those groups. This page describes the libraries that exist for that, what each one has been validated on, and the patterns you can build with them today.
Building blocks
| Library | Language | Runs on | What it does |
|---|---|---|---|
uNeurai | C++ | ESP32 family, STM32, other 32-bit MCUs. Arduino, mbed, bare metal | Keys, BIP39 and BIP32, addresses, PSBT signing, asset scripts, signmessage-compatible signing. Optional ML-DSA-44 and AuthScript behind UNEURAI_ENABLE_PQ |
NeuraiDepinMsg | C++ | ESP32-S3 validated, Arduino-ESP32 2.0.17 on PlatformIO | DePIN messaging protocol 2 client and codec: pool key pinning, signed challenges, poolsig verification, ECIES, pagination |
mldsa-esp32 | C | ESP32 | ML-DSA-44, 65 and 87 signatures (FIPS 204), hardware RNG, constant time, keys in NVS |
NeuraiHW | C++ | LilyGo T-Display S3 and other ESP32 boards | Hardware wallet firmware: PSBT signing with on-screen review, DePIN chat identity, PQ addresses. Flashed from the browser with esp32-tool-flasher |
@neuraiproject/neurai-sign-esp32 | JS/TS | Browser (Web Serial) and React Native (Android USB) | Host side of the hardware wallet: PSBT workflow, message signing, DePIN identity adapter for neurai-depin-msg |
Everything is MIT licensed. 8-bit boards such as the classic Arduino are not supported: the cryptography needs a 32-bit MCU.
Onboarding a device
A device takes part in DePIN messaging exactly like a person does: it is an address that holds a token and has revealed its public key. The steps in order:
- Provision a mnemonic in protected storage and derive the messaging identity from account
100':m/44'/1900'/100'/0/0on mainnet,m/44'/1'/100'/0/0on testnet. Keep funds on a different account. - Reveal the public key by spending once from the address. Until then the node cannot encrypt for it or verify its signatures.
- Hold the token. The owner of
&FLEETsends one unit of&FLEET/LINE1to the device address. The device can be frozen or revoked later without touching its firmware. - Pin and sync. Ship the pool's public key, its root token and the RPC service's TLS root CA with the firmware, and synchronize the clock over NTP. Challenges carry millisecond timestamps and are refused outside a 60-second window.
- Talk. Every read starts with a signed challenge and every reply is verified against the pinned key before it is decrypted. See Client guide.
Pattern 1: keys and signatures on the device
uNeurai derives keys and signs on the chip. This is the primitive under every other pattern.
#include "Neurai.h"
#include "Message.h"
HDPrivateKey hd("add good charge eagle walk culture book inherit fan nature seek repair", "");
// Funds account (BIP-44 coin type 1900) and its first receiving address
HDPrivateKey account = hd.derive("m/44'/1900'/0'/");
Serial.println(account.xpub());
Serial.println(account.derive("m/0/0").address());
// Messaging identity on account 100' (testnet coin type 1)
HDPrivateKey chatId = hd.derive("m/44'/1'/100'/0/0");
// signmessage-compatible signature: the primitive DePIN protocol 2 uses
// for challenge requests and challenge use
char sig[NEURAI_MESSAGE_SIG_B64_LEN + 1];
signMessageBase64(chatId, "DEPIN-REQ|receive|&FLEET/LINE1|tRERn8G2...|1730000000000", sig, sizeof(sig));
Signing is deterministic (RFC 6979) and the library's test suite reproduces the protocol vectors of the node byte for byte. A PSBT built by a host can be parsed, reviewed and signed on the device with PSBT::parseBase64, sign and toBase64; the asset codec in Asset.h decodes asset outputs so the device can show what is being moved instead of blind-signing.
Pattern 2: DePIN messaging from the device
NeuraiDepinMsg implements protocol 2 end to end on an ESP32-S3: it requests challenges, verifies poolsig, decrypts bound replies, discovers recipients on-chain and publishes encrypted messages. Before connecting you need, from the pool operator: the RPC URL and its TLS root CA, the pool's 66-hex public key obtained through a trusted channel, the root token and the channel token, and a WIF for an address that holds the token and has revealed its key.
#include <NeuraiDepinClient.h>
NeuraiDepinClient client;
String cursor; // persist it if processing must resume after a reboot
bool startMessaging(const char *rootCA, const char *holderWIF) {
client.setCACert(rootCA);
client.setPoolPin("YOUR_66_HEX_POOL_PUBLIC_KEY", "&FLEET");
client.setPageLimit(2);
if (!client.begin("https://YOUR_RPC_HOST", "&FLEET/LINE1", holderWIF)) return false;
return client.bootstrap();
}
void publish(float temperature) {
String hash = client.sendGroupMessage("{\"t\":" + String(temperature, 1) + "}");
if (hash.isEmpty()) Serial.println(client.lastErrorName());
}
void poll() {
if (!client.ready()) return;
for (unsigned page = 0; page < 4; ++page) {
DepinPageResult r = client.receivePage(cursor, 2);
if (!r.ok) { Serial.println(client.lastErrorName()); return; }
for (const auto &m : r.messages) {
Serial.printf("[%s] %s\n", m.type.c_str(), m.content.c_str());
// act on commands here, e.g. toggle a relay
}
cursor = r.nextCursor; // commit only after processing the page
if (!r.shouldContinue) break;
}
}
Practical rules from the library's own documentation:
- Connect Wi-Fi before encrypting anything, so the RF entropy source is available, and sync the clock before requesting challenges.
- A TLS certificate and a pool pin protect different things. Configure both. A pin mismatch is an incident, not a reason to accept a new key.
- Two messages per page is the tested starting point. Measure memory with your real payload sizes and recipient counts; the page limit bounds rows, not bytes.
- On
RateLimited, waitretryAfterSec()instead of polling again. - Use ArduinoJson 6, not 7, and the pinned uNeurai and mldsa-esp32 versions listed in the README.
What you get: readings published to the section, encrypted for its active holders and signed by the device; commands from operators decrypted on the device and verified against the sender's on-chain key. What you do not get: guaranteed delivery or real-time latency. Messages expire with the pool and an offline device misses them, so keep a cursor and design for polling.
Pattern 3: an ESP32 as a hardware wallet
NeuraiHW firmware plus neurai-sign-esp32 on the host turn a cheap board into a signer that never reveals its mnemonic. The web wallet uses it through the Web Serial API. The same device can be the DePIN identity of a wallet: after one physical approval it opens a session on a channel and signs or decrypts on the chip, with an idle timeout, a hard cap and a rate limit.
import { createDepinDeviceIdentity } from "@neuraiproject/neurai-sign-esp32";
import { requestDepinChallenge, receiveDepinMessages } from "@neuraiproject/neurai-depin-msg";
const identity = await createDepinDeviceIdentity(device, {
token: "&FLEET/LINE1",
expectedNetwork: "test",
sessionPermissions: ["receive", "publish"],
ttlMinutes: 15,
});
const challenge = await requestDepinChallenge({ rpc, identity, token: "&FLEET/LINE1", poolPublicKey });
const page = await receiveDepinMessages({
rpc, identity, token: "&FLEET/LINE1", challenge: challenge.challenge,
poolPublicKey, network: "test", limit: 25,
});
Session permissions are separate on purpose: a session approved for reading cannot publish in the owner's name or purge the pool. The device has no real-time clock, so the host supplies timestamps and the chip validates structure and scope. Firmware can be flashed and updated from the browser with the flasher, which verifies each image by SHA-256 before writing.
Pattern 4: signed telemetry without DePIN
Not every deployment needs the pool. A device that publishes over MQTT, LoRa or a serial link can still sign each reading with its Neurai key. The receiver verifies the signature with any wallet's verifymessage or with neurai-message in JavaScript, and checks on-chain that the address holds the fleet's token. That gives authenticated, attributable data with no server-side secrets, on any transport.
#include "Message.h"
String payload = "SM|41.2|3.98|1730000000";
char sig[NEURAI_MESSAGE_SIG_B64_LEN + 1];
signMessageBase64(chatId, payload.c_str(), sig, sizeof(sig));
// transmit: address | payload | sig
For devices without IP connectivity, a gateway with a network connection can forward these signed payloads and, if it is itself a token holder, republish them into a DePIN section. The gateway becomes the DePIN sender; the device's own signature travels inside the content, so the origin stays verifiable.
Pattern 5: post-quantum signatures on the device
mldsa-esp32 provides ML-DSA-44 on the ESP32 with keys persisted in NVS and deterministic key generation from a 32-byte seed. With UNEURAI_ENABLE_PQ, uNeurai derives PQ keys through the native PQ-HD tree of NIP-022 and builds AuthScript transactions, so a device can hold an nq1… or tnq1… address. Public keys are 1,312 bytes and signatures about 2,420 bytes; budget flash and RAM accordingly. Available on testnet and regtest, mainnet after POSITRONIC.
Limits to design around
| Constraint | Consequence |
|---|---|
| Validated hardware for the DePIN client is ESP32-S3 | Other ESP32 variants may work but are outside the tested scope |
| Clock skew window is ±60 s | NTP sync before every session, or challenges are refused |
| Content up to 1,024 bytes by default, 20 recipients | Keep payloads compact; large sections need the pool's limits raised |
| Messages expire, default 7 days, no delivery guarantee | Poll with a persisted cursor; add acknowledgements in your own payloads if you need them |
| Every read costs a signature and a decryption on the chip | Poll at a sensible interval and page in small batches |
| The pool operator sees metadata | Sender, token, timestamp and recipient hashes are visible to the node, content is not |
Further reading
- DePIN: the protocol, the client guide and how to run your own pool.
- Libraries: every package with versions and links.
- AI over DePIN: letting a model read what devices post.