Skip to main content

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.

ESP32 deviceClassic, S2, S3, C3keys kept in the board's flashsecp256k1 and ML-DSA-44AES acceleration in hardwarepolls its channel, acts, reportsusbWeb walletWeb Serial APIsigns PSBTs on the devicekeys never leave itrpcNeurai nodeDePIN message poolchecks the DePIN tokenstores ciphertext onlyBlockchainassets and DePIN tokensidentity and ownershipaccess rules for messagingOperators and devicesholders of the same tokencommands inreadings and alerts out

Building blocks

LibraryLanguageRuns onWhat it does
uNeuraiC++ESP32 family, STM32, other 32-bit MCUs. Arduino, mbed, bare metalKeys, BIP39 and BIP32, addresses, PSBT signing, asset scripts, signmessage-compatible signing. Optional ML-DSA-44 and AuthScript behind UNEURAI_ENABLE_PQ
NeuraiDepinMsgC++ESP32-S3 validated, Arduino-ESP32 2.0.17 on PlatformIODePIN messaging protocol 2 client and codec: pool key pinning, signed challenges, poolsig verification, ECIES, pagination
mldsa-esp32CESP32ML-DSA-44, 65 and 87 signatures (FIPS 204), hardware RNG, constant time, keys in NVS
NeuraiHWC++LilyGo T-Display S3 and other ESP32 boardsHardware wallet firmware: PSBT signing with on-screen review, DePIN chat identity, PQ addresses. Flashed from the browser with esp32-tool-flasher
@neuraiproject/neurai-sign-esp32JS/TSBrowser (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

1Provisionmnemonic in NVS,identity m/44'/coin'/100'2Reveal keyspend once from theaddress on chain3Hold tokenowner sends &FLEET/… to the device address4Pin and syncpool key, root token,TLS CA, NTP clock5Talkchallenge, receive,publish, every call signed

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:

  1. Provision a mnemonic in protected storage and derive the messaging identity from account 100': m/44'/1900'/100'/0/0 on mainnet, m/44'/1'/100'/0/0 on testnet. Keep funds on a different account.
  2. Reveal the public key by spending once from the address. Until then the node cannot encrypt for it or verify its signatures.
  3. Hold the token. The owner of &FLEET sends one unit of &FLEET/LINE1 to the device address. The device can be frozen or revoked later without touching its firmware.
  4. 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.
  5. 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, wait retryAfterSec() 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

ConstraintConsequence
Validated hardware for the DePIN client is ESP32-S3Other ESP32 variants may work but are outside the tested scope
Clock skew window is ±60 sNTP sync before every session, or challenges are refused
Content up to 1,024 bytes by default, 20 recipientsKeep payloads compact; large sections need the pool's limits raised
Messages expire, default 7 days, no delivery guaranteePoll with a persisted cursor; add acknowledgements in your own payloads if you need them
Every read costs a signature and a decryption on the chipPoll at a sensible interval and page in small batches
The pool operator sees metadataSender, 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.