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.

Prerequisites

Before proceeding, ensure you have the following in place:

  • peaq JavaScript SDK installed.
  • A completed machine onboarding flow (see the previous page Onboard a Machine).
  • A basic understanding of blockchain transactions, DIDs, and public/private key cryptography.

Instructions

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.

1. Generating Machine Data

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 page
class ECDSAKeyring {
    constructor(privateKey) {
      if (privateKey) {
        this.wallet = new Wallet(privateKey);
      } else {
        this.wallet = Wallet.createRandom(); // generates new key
      }
    }
    getAddress() {
      return this.wallet.address;
    }
    getPrivateKey() {
      return this.wallet.privateKey;
    }
    async signMessage(message) {
      return this.wallet.signMessage(message);
    }
}

// Simulate machine-generated data and store it in peaq storage
async function storeData(machineWallet){
    const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // agung testnet

    const sdk = await Sdk.createInstance({
        baseUrl: HTTPS_BASE_URL,
        chainType: Sdk.ChainType.EVM
    });

    const key = crypto.randomUUID(); // Autogenerate a uuid that will be linked in DID Doc
    const message = "mock data to be stored"; // Replace with dynamic data if needed

    const tx = await sdk.storage.addItem({
        itemType: key,
        item: message
    });

    const receipt = await Sdk.sendEvmTx({
        tx: tx,
        baseUrl: HTTPS_BASE_URL,
        seed: machineWallet.getPrivateKey()
    });
    
    console.log(`Stored message for ${key}: "${message}"`)
    return message;
}

async function main(){	
	// Get the previously created wallet via the private key
    const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE);
		
    // Store 'generated' data at peaq storage
    const message = await storeData(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.

2. Sign Machine Data

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";

class ECDSAKeyring {
    constructor(privateKey) {
      if (privateKey) {
        this.wallet = new Wallet(privateKey);
      } else {
        this.wallet = Wallet.createRandom(); // generates new key
      }
    }
    getAddress() {
      return this.wallet.address;
    }
    getPrivateKey() {
      return this.wallet.privateKey;
    }
    async signMessage(message) {
      return this.wallet.signMessage(message);
    }
 }
 
 async function signData(message, machineWallet){
    return await machineWallet.signMessage(message);
}
  
 async function main(){	
	// Get the previously created wallet via the private key
    const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE);

    // Same data value that was stored previously
    const message = "mock data to be stored";

    // Sign data
    const signature = await signData(message, machineWallet);
}

main();

3. Admin Approve Machine Data

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 sections
class ECDSAKeyring {
  constructor(privateKey) {
    if (privateKey) {
      this.wallet = new Wallet(privateKey);
    } else {
      this.wallet = Wallet.createRandom(); // generates new key
    }
  }
  getAddress() {
    return this.wallet.address;
  }

  getPrivateKey() {
    return this.wallet.privateKey;
  }
  async signMessage(message) {
    return this.wallet.signMessage(message);
  }
}

// Store machine data approval in peaq storage using the admin wallet
async function storeData(adminWallet, machineWallet, key, machineSignature) {
  const HTTPS_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 = await Sdk.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 = await Sdk.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.
async function adminSignData(adminWallet, unsignedContent) {
  const canonicalContent = JSON.stringify(unsignedContent);
  return await adminWallet.signMessage(canonicalContent);
}

async function main() {  
  // Get the machine wallet instance via its private key.
  const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE);
  // Get the admin wallet instance (trusted authority).
  const adminWallet = new ECDSAKeyring(ADMIN_PRIVATE);

  // The key used to store the machine data (generated previously).
  const key = "uuidValue"; // Autogenerated uuid from before
  const machineSignature = "0xSIGNATURE_VALUE"; // Machine signature from previous step
    
  // Admin stores the approval data in peaq storage.
  const unsignedContent = await storeData(adminWallet, machineWallet, key, machineSignature);

  // The admin signs the canonical content to approve the machine-generated data.
  const adminSignature = await adminSignData(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.

4. Update DID Document

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 wallet
class ECDSAKeyring {
    constructor(privateKey) {
      if (privateKey) {
        this.wallet = new Wallet(privateKey);
      } else {
        this.wallet = Wallet.createRandom(); // generates new key
      }
    }
    getAddress() {
      return this.wallet.address;
    }
    getPrivateKey() {
      return this.wallet.privateKey;
    }
    async signMessage(message) {
      return this.wallet.signMessage(message);
    }
}

async function updateDID(machineWallet, adminWallet, key, adminSignature){
  const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // Agung testnet endpoint
  const WSS_BASE_URL = "wss://wss-async.agung.peaq.network";        // Used to read previously stored DID

    const sdk = await Sdk.createInstance({
        baseUrl: HTTPS_BASE_URL,
        chainType: Sdk.ChainType.EVM
    });

    const name = "project_name"; // Same name as before
    
    const 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 DID
    const 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 key
    const receipt = await Sdk.sendEvmTx({
        tx: tx,
        baseUrl: HTTPS_BASE_URL,
        seed: machineWallet.getPrivateKey()
    });

    console.log("DID Document updated with receipt:", receipt);
}

 async function main(){	
	// Get previously created wallet via the private key
    const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE);
    const adminWallet = new ECDSAKeyring(ADMIN_PRIVATE);
    const key = "uuidValue"; // Autogenerated uuid from before
    const adminSignature = "0xSIGNATURE_VALUE"; // admin  signature from previous step

    // Same data value that was stored previously
    await updateDID(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.

Putting it all Together

In this example, we tie all the steps into one complete flow. The script performs the following tasks:

  1. Initialize Wallets: Create instances for both the machine and the admin using the ECDSAKeyring.

  2. Generate & Store Machine Data: The machine generates mock data and stores it in peaq storage under a randomly generated UUID.

  3. Sign Machine Data: The machine signs the generated data with its private key to create a verifiable signature.

  4. 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.

  5. 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 page
class ECDSAKeyring {
    constructor(privateKey) {
      if (privateKey) {
        this.wallet = new Wallet(privateKey);
      } else {
        this.wallet = Wallet.createRandom(); // generates new key
      }
    }
    getAddress() {
      return this.wallet.address;
    }
    getPrivateKey() {
      return this.wallet.privateKey;
    }
    async signMessage(message) {
      return this.wallet.signMessage(message);
    }
}

// Simulate machine-generated data and store it in peaq storage
async function storeMachineData(key, machineWallet){
    const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // agung testnet

    const sdk = await Sdk.createInstance({
        baseUrl: HTTPS_BASE_URL,
        chainType: Sdk.ChainType.EVM
    });

	const message = "mock data to be stored"; // Replace with dynamic data if needed

    const tx = await sdk.storage.addItem({
        itemType: key,
        item: message
    });

    const receipt = await Sdk.sendEvmTx({
        tx: tx,
        baseUrl: HTTPS_BASE_URL,
        seed: machineWallet.getPrivateKey()
    });
    
    console.log(`Stored message for ${key}: "${message}"`)
    return message;
}

async function signMachineData(message, machineWallet){
  return await machineWallet.signMessage(message);
}


// Store machine data approval in peaq storage using the admin wallet
async function storeAdminApproval(adminWallet, machineWallet, key, machineSignature) {
  const HTTPS_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 = await Sdk.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 = await Sdk.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.
async function adminSignData(adminWallet, unsignedContent) {
  const canonicalContent = JSON.stringify(unsignedContent);
  return await adminWallet.signMessage(canonicalContent);
}

async function updateDID(machineWallet, adminWallet, key, adminSignature){
  const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public";
  const WSS_BASE_URL = "wss://wss-async.agung.peaq.network";        // Used to read previously stored DID

    const sdk = await Sdk.createInstance({
        baseUrl: HTTPS_BASE_URL,
        chainType: Sdk.ChainType.EVM
    });

    const name = "project_name"; // Same name as before
    const 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 DID
    const 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 key
    const receipt = await Sdk.sendEvmTx({
        tx: tx,
        baseUrl: HTTPS_BASE_URL,
        seed: machineWallet.getPrivateKey()
    });
    console.log(`Successfully Updated DID Document`);
}

async function main(){	
	// 1. Initialize Wallets
    const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE);
	const adminWallet = new ECDSAKeyring(ADMIN_PRIVATE);

	// 2. Generate & Store Machine Data
    const key = crypto.randomUUID();
	const message = await storeMachineData(key, machineWallet);
    
    // 3. Sign Machine Data
    const machineSignature = await signMachineData(message, machineWallet);

    // 4. Admin Approval
    const unsignedContent = await storeAdminApproval(adminWallet, machineWallet, key, machineSignature);
    const adminSignature = await adminSignData(adminWallet, unsignedContent);

    // 5. Update DID Document
    await updateDID(machineWallet, adminWallet, key, adminSignature);
}

main();

Summary

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.