Once your machine has a wallet and a corresponding DID Document registered on the peaq network, the next step is to associate metadata with its on-chain identity. This enables verifiable storage of machine-generated data with admin approval and adds traceability
to the machine’s activity within a DePIN ecosystem.
In this section, we’ll demonstrate how to:
Simulate data generated by a machine.
Sign that data using the machine’s private key.
Store the signed data using peaq storage.
Update the existing DID Document to include a reference to the signed data and its storage location.
While peaq provides its own storage layer, you’re free to use other decentralized or traditional storage backends. The key requirement is that any storage reference must be linked through the machine’s DID Document,
enabling others to verify the data’s origin and integrity. For more information take a look at Off-Chain Storage Solutions.
In this guide, we’ll build upon the code from the Onboard a Machine tutorial. That section covered creating a wallet, transferring tokens, and registering a DID Document with an ECDSA verification method.
Here, we’ll simulate data output from a machine (e.g., a sensor reading or device log), sign the data with the machine and admin private key to ensure authenticity, and store the result using peaq storage. Finally, we’ll update the DID
Document to include a link to the signed data, allowing it to be verified and referenced by others in the network.
To start we will simulate a machine generating data and then store it using peaq storage as a key-value pair.
We will reuse the previously created ECDSAKeyring class to access the machine’s wallet instance. This allows us to:
Generate mock data (as if produced by a sensor or onboard system).
Store the unsigned message in peaq storage using a randomly generated uuid as the key.
import{Wallet}from"ethers";import{Sdk}from"@peaq-network/sdk";// Reuse the ECDSAKeyring class from previous pageclassECDSAKeyring{constructor(privateKey){if(privateKey){this.wallet=newWallet(privateKey);}else{this.wallet=Wallet.createRandom();// generates new key}}getAddress(){returnthis.wallet.address;}getPrivateKey(){returnthis.wallet.privateKey;}asyncsignMessage(message){returnthis.wallet.signMessage(message);}}// Simulate machine-generated data and store it in peaq storageasyncfunctionstoreData(machineWallet){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";// agung testnetconst sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});const key = crypto.randomUUID();// Autogenerate a uuid that will be linked in DID Docconst message ="mock data to be stored";// Replace with dynamic data if neededconst tx =await sdk.storage.addItem({itemType: key,item: message});const receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: machineWallet.getPrivateKey()});console.log(`Stored message for ${key}: "${message}"`)return message;}asyncfunctionmain(){// Get the previously created wallet via the private keyconst machineWallet =newECDSAKeyring(MACHINE_PRIVATE);// Store 'generated' data at peaq storageconst message =awaitstoreData(machineWallet);}main();
📦 peaq storage supports a simple structure: 64-byte key : 256-byte value. For larger datasets or binary formats, please refer to alternative storage solutions or consult our extended tutorials.
After storing the message in peaq storage, the next step is to sign that message using the machine’s private key. This proves that the machine was indeed the originator of the data — a core requirement for decentralized, verifiable systems.
We use the ECDSA signature algorithm, which is native to EVM ecosystems. The generated signature can later be embedded into the DID Document so that third parties can verify the message using:
The machine’s public key (on-chain)
The unsigned message (from peaq storage)
The signature (in the DID Document)
import{Wallet}from"ethers";import{Sdk}from"@peaq-network/sdk";classECDSAKeyring{constructor(privateKey){if(privateKey){this.wallet=newWallet(privateKey);}else{this.wallet=Wallet.createRandom();// generates new key}}getAddress(){returnthis.wallet.address;}getPrivateKey(){returnthis.wallet.privateKey;}asyncsignMessage(message){returnthis.wallet.signMessage(message);}}asyncfunctionsignData(message, machineWallet){returnawait machineWallet.signMessage(message);}asyncfunctionmain(){// Get the previously created wallet via the private keyconst machineWallet =newECDSAKeyring(MACHINE_PRIVATE);// Same data value that was stored previouslyconst message ="mock data to be stored";// Sign dataconst signature =awaitsignData(message, machineWallet);}main();
After the machine has generated its data and signed it, the next step is for the administrator—acting as the trusted authority—to explicitly approve the machine-generated data. By signing a canonical representation of the stored data,
the administrator creates a verifiable link between the machine data and the authority that controls the machine. This dual-layer verification is critical for ensuring trust in decentralized systems, as verifiers can confirm
both the origin of the data and its formal endorsement by the responsible party.
The administrator will perform the following tasks:
Retrieve the stored machine data (identified by the UUID used in peaq storage).
Prepare a canonical JSON representation of the data that the admin will sign.
Sign the canonical content using the administrator’s private key.
Send the transaction to store the signed approval.
import{Wallet}from"ethers";import{Sdk}from"@peaq-network/sdk";// Reuse the ECDSAKeyring class from previous sectionsclassECDSAKeyring{constructor(privateKey){if(privateKey){this.wallet=newWallet(privateKey);}else{this.wallet=Wallet.createRandom();// generates new key}}getAddress(){returnthis.wallet.address;}getPrivateKey(){returnthis.wallet.privateKey;}asyncsignMessage(message){returnthis.wallet.signMessage(message);}}// Store machine data approval in peaq storage using the admin walletasyncfunctionstoreData(adminWallet, machineWallet, key, machineSignature){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";// agung testnet// Prepare the content to be approved. Be wary about the 256 byte limit.const storageContent ={storageKey: key,machineSignature: machineSignature};const sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});// We reuse the same uuid key to link the machine data with the admin approval.const tx =await sdk.storage.addItem({itemType: key,item: storageContent});// The admin sends the transaction to store the approval.const receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: adminWallet.getPrivateKey()});console.log(`Admin stored approval for ${key}`);// Return the content that will be signed by the admin.return storageContent;}// The admin signs the canonical representation of the stored data.asyncfunctionadminSignData(adminWallet, unsignedContent){const canonicalContent =JSON.stringify(unsignedContent);returnawait adminWallet.signMessage(canonicalContent);}asyncfunctionmain(){// Get the machine wallet instance via its private key.const machineWallet =newECDSAKeyring(MACHINE_PRIVATE);// Get the admin wallet instance (trusted authority).const adminWallet =newECDSAKeyring(ADMIN_PRIVATE);// The key used to store the machine data (generated previously).const key ="uuidValue";// Autogenerated uuid from beforeconst machineSignature ="0xSIGNATURE_VALUE";// Machine signature from previous step// Admin stores the approval data in peaq storage.const unsignedContent =awaitstoreData(adminWallet, machineWallet, key, machineSignature);// The admin signs the canonical content to approve the machine-generated data.const adminSignature =awaitadminSignData(adminWallet, unsignedContent);console.log("Admin Signature:", adminSignature);}main();
In this step, the administrator’s signature is generated over the canonical JSON representation of the stored machine data (which includes the storage key and the machine’s signature).
This signature serves as verifiable proof that the trusted authority has approved the data, and will be incorporated into the machine’s DID Document in subsequent processes.
Now that the machine & admin have generated a verifiable signatures and stored their unsigned messages in peaq storage, the final step is to update the machine’s DID Document.
This update embeds the administrators approval into the DID, linking the stored data to a trusted authority. With this update, external parties can resolve the DID, retrieve the unsigned message,
and verify both the machine’s signature and the admin’s attestation.
In this step, we will:
Reuse the machine’s original DID (same name and wallet).
Preserve existing fields such as the verification method.
Add a new signature field that includes:
The algorithm used (EcdsaSecp256k1RecoveryMethod2020).
The issuer (admin’s address).
The admin’s signature hash over the canonical data.
Update the services field to include a reference to the unsigned message stored in peaq storage, which is linked by a UUID.
import{ ethers,Wallet}from"ethers";import{Sdk}from"@peaq-network/sdk";// A simple keyring for the machine walletclassECDSAKeyring{constructor(privateKey){if(privateKey){this.wallet=newWallet(privateKey);}else{this.wallet=Wallet.createRandom();// generates new key}}getAddress(){returnthis.wallet.address;}getPrivateKey(){returnthis.wallet.privateKey;}asyncsignMessage(message){returnthis.wallet.signMessage(message);}}asyncfunctionupdateDID(machineWallet, adminWallet, key, adminSignature){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";// Agung testnet endpointconstWSS_BASE_URL="wss://wss-async.agung.peaq.network";// Used to read previously stored DIDconst sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});const name ="project_name";// Same name as beforeconst customFields ={verifications:[{type:"EcdsaSecp256k1RecoveryMethod2020"}],signature:{type:"EcdsaSecp256k1RecoveryMethod2020",issuer: adminWallet.getAddress(),hash: adminSignature},services:[{id:'#unsignedMessage',type:'peaqStorage',data: key // Reference to the stored unsigned admin & machine data in peaq storage},{id:'#admin',type:'admin',data: adminWallet.getAddress()},]};// Create a transaction object to update the DIDconst tx =await sdk.did.update({name: name,address: machineWallet.getAddress(),wssBaseUrl:WSS_BASE_URL,customDocumentFields: customFields});// Submit the transaction to the network using the machine’s private keyconst receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: machineWallet.getPrivateKey()});console.log("DID Document updated with receipt:", receipt);}asyncfunctionmain(){// Get previously created wallet via the private keyconst machineWallet =newECDSAKeyring(MACHINE_PRIVATE);const adminWallet =newECDSAKeyring(ADMIN_PRIVATE);const key ="uuidValue";// Autogenerated uuid from beforeconst adminSignature ="0xSIGNATURE_VALUE";// admin signature from previous step// Same data value that was stored previouslyawaitupdateDID(machineWallet, adminWallet, key, adminSignature);}main();
By updating the DID Document with the administrator’s signature and linking to the unsigned machine data stored in peaq storage, you create a fully verifiable data flow. External parties can now:
Retrieve the DID Document to inspect the machine’s verification method and the admin-approved signature.
Fetch the corresponding unsigned message from peaq storage using the UUID reference.
Verify the authenticity of the machine data using both the machine’s public key and the trusted admin’s attestation.
This design pattern is a critical building block for establishing decentralized trust in machine-generated data, ensuring secure and auditable DePIN systems.
In this example, we tie all the steps into one complete flow. The script performs the following tasks:
Initialize Wallets: Create instances for both the machine and the admin using the ECDSAKeyring.
Generate & Store Machine Data: The machine generates mock data and stores it in peaq storage under a randomly generated UUID.
Sign Machine Data: The machine signs the generated data with its private key to create a verifiable signature.
Admin Approval: The admin (trusted authority) retrieves the machine data reference and stores an approval object in peaq storage. Then, the admin signs a canonical JSON representation of this approval content.
Update DID Document: The machine’s DID Document is updated with the admin’s signature and a reference to the stored data. Ultimately links the unsigned machine data (with its machine signature) to the trusted admin’s attestation.
import{Wallet}from"ethers";import{Sdk}from"@peaq-network/sdk";// Reuse the ECDSAKeyring class from previous pageclassECDSAKeyring{constructor(privateKey){if(privateKey){this.wallet=newWallet(privateKey);}else{this.wallet=Wallet.createRandom();// generates new key}}getAddress(){returnthis.wallet.address;}getPrivateKey(){returnthis.wallet.privateKey;}asyncsignMessage(message){returnthis.wallet.signMessage(message);}}// Simulate machine-generated data and store it in peaq storageasyncfunctionstoreMachineData(key, machineWallet){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";// agung testnetconst sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});const message ="mock data to be stored";// Replace with dynamic data if neededconst tx =await sdk.storage.addItem({itemType: key,item: message});const receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: machineWallet.getPrivateKey()});console.log(`Stored message for ${key}: "${message}"`)return message;}asyncfunctionsignMachineData(message, machineWallet){returnawait machineWallet.signMessage(message);}// Store machine data approval in peaq storage using the admin walletasyncfunctionstoreAdminApproval(adminWallet, machineWallet, key, machineSignature){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";// agung testnet// Prepare the content to be approved. Be wary about the 256 byte limit.const storageContent ={storageKey: key,machineSignature: machineSignature};const sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});// We reuse the same key to link the machine data with the admin approval.const tx =await sdk.storage.addItem({itemType: key,item: storageContent});// The admin sends the transaction to store the approval.const receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: adminWallet.getPrivateKey()});console.log(`Admin stored approval for ${key}`);// Return the content that will be signed by the admin.return storageContent;}// The admin signs the canonical representation of the stored data.asyncfunctionadminSignData(adminWallet, unsignedContent){const canonicalContent =JSON.stringify(unsignedContent);returnawait adminWallet.signMessage(canonicalContent);}asyncfunctionupdateDID(machineWallet, adminWallet, key, adminSignature){constHTTPS_BASE_URL="https://peaq-agung.api.onfinality.io/public";constWSS_BASE_URL="wss://wss-async.agung.peaq.network";// Used to read previously stored DIDconst sdk =awaitSdk.createInstance({baseUrl:HTTPS_BASE_URL,chainType:Sdk.ChainType.EVM});const name ="project_name";// Same name as beforeconst customFields ={verifications:[{type:"EcdsaSecp256k1RecoveryMethod2020"}],signature:{type:"EcdsaSecp256k1RecoveryMethod2020",issuer: adminWallet.getAddress(),hash: adminSignature},services:[{id:'#unsignedMessage',type:'peaqStorage',data: key // Reference to the stored unsigned admin & machine data in peaq storage},{id:'#admin',type:'admin',data: adminWallet.getAddress()},]};// Create a transaction object to update the DIDconst tx =await sdk.did.update({name: name,address: machineWallet.getAddress(),wssBaseUrl:WSS_BASE_URL,customDocumentFields: customFields});// Submit the transaction to the network using the machine’s private keyconst receipt =awaitSdk.sendEvmTx({tx: tx,baseUrl:HTTPS_BASE_URL,seed: machineWallet.getPrivateKey()});console.log(`Successfully Updated DID Document`);}asyncfunctionmain(){// 1. Initialize Walletsconst machineWallet =newECDSAKeyring(MACHINE_PRIVATE);const adminWallet =newECDSAKeyring(ADMIN_PRIVATE);// 2. Generate & Store Machine Dataconst key = crypto.randomUUID();const message =awaitstoreMachineData(key, machineWallet);// 3. Sign Machine Dataconst machineSignature =awaitsignMachineData(message, machineWallet);// 4. Admin Approvalconst unsignedContent =awaitstoreAdminApproval(adminWallet, machineWallet, key, machineSignature);const adminSignature =awaitadminSignData(adminWallet, unsignedContent);// 5. Update DID DocumentawaitupdateDID(machineWallet, adminWallet, key, adminSignature);}main();
After running this script, your machine’s DID Document will be updated with:
A reference to the unsigned machine data stored in peaq storage.
The machine’s signature over the generated data.
The admin’s signature attesting to the data’s validity.
This end-to-end flow creates a fully verifiable data trail. External parties can resolve the DID Document, retrieve the stored data via its unique key,
and verify that both the machine and the trusted admin have authenticated the data. This pattern is a critical building block for secure, transparent DePIN ecosystems.