# Glossary Source: https://docs.peaq.xyz/glossary Plain-English definitions of the web3 terms used across the peaqOS docs. No crypto background assumed. You don't need a web3 background to build on peaqOS. This page defines every blockchain term the docs use, in plain English. Throughout the docs, terms with a dotted underline show this definition on hover or tap, and link back here. **On a robot, half these words mean something else.** A `node` here is a blockchain server, not a ROS 2 node. An `event` is an on-chain record that feeds credit scoring, not a topic message. Where a term collides with robotics, the definition says so. ## Chains & networks ## Wallets, keys & signing ## Identity & registration ## Tokens, NFTs, fees & credit ## Smart contracts ## Cross-chain & bridging ## Agents, markets & tooling ## Data streams (Stream) ## Machine earnings (Monetize) # peaq Source: https://docs.peaq.xyz/home The economic infrastructure for machines. peaq turns robots, vehicles, sensors, AI agents, and other machines into economic participants: machines that earn, transact, build reputation, access capital, and become investable assets. ## What peaq provides Give machines a verifiable identity that works across chains. Track performance, reliability, and activity as a portable credit rating. Enable autonomous transactions, including agent-delegated spending. Let machines sell their spare capacity and earn to their own wallet. Tokenize and invest in machines and machine networks. Connect machines to data, AI, compute, storage, and other services. Under the hood, peaq combines these machine economy services with a trust infrastructure that records ownership, reputation, transactions, and economic activity in a verifiable way. ## Architecture The machine economy operating system that developers and machines interact with. Identity, credit, payments, and tokenization through one SDK. **Most teams build here and never go deeper.** The decentralized infrastructure layer that secures ownership, reputation, payments, and coordination across the machine economy. peaq chain is the home chain; state mirrors out to satellite chains so machines can transact wherever their counterparties are. peaqOS runs on the Trust Layer and abstracts it. Identity and credit records live in peaqOS contracts on the home chain, mirror out to satellite chains, and any chain can read them through the MCR API. You drop down to the Trust Layer directly only when you need something peaqOS does not already cover. ## Start building The five-minute peaqOS flow. Install the SDK, activate a machine in one transaction, manage it. Solidity, node operations, staking, and the peaq chain SDK primitives. ## When to use which Most of the time the answer is peaqOS. Drop to the Trust Layer — peaq chain — when you need protocol-level control. | I want to... | Use | | :----------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | | Give a machine a cross-chain identity | [peaqOS Activate](/peaqos/functions/activate) | | Query a machine's credit rating from any chain | [peaqOS MCR API](/peaqos/api-reference/overview) | | Submit revenue events for a fleet | [peaqOS proxy operator](/peaqos/guides/proxy-operator-fleet) | | Delegate spending to an AI agent | [peaqOS Scale](/peaqos/functions/scale) | | Fractionalize a machine as an RWA | [peaqOS Tokenize](/peaqos/functions/tokenize) | | Deploy a Solidity contract | [peaq chain · Smart contracts](/peaqchain/build/basic-operations/smart-contracts/deploy-smart-contract) | | Run a validator | [peaq chain · Node operations](/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/introduction) | | Use on-chain DID, RBAC, or storage primitives directly | [peaq chain SDK](/peaqchain/sdk-reference/javascript/did-operations) | **Two SDKs, one stack.** `@peaqos/peaq-os-sdk` is the opinionated path through the machine flow. `@peaq-network/sdk` gives raw access to peaq chain primitives (DID, RBAC, storage, UMT, chain transactions). Most teams use peaqOS and never touch the chain SDK directly. ## Who it's for Give your machines identity, credit, and financial infrastructure. Activate a whole fleet. Every machine owns its NFT; one operator controls them all. Query any machine's credit rating. Any chain. One API call. ## Keep going What ships when. Plain-English web3 terms. Source and SDKs. Builders, operators, OEMs. # Arcana Social Login Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/account-abstraction/arcana-social-login Web3 developers building on the **peaq** chain can onboard app users quickly via the [social login](https://docs.arcana.network/concepts/social-login) feature of the [Arcana Auth SDK](https://arcana.network/auth). Apps can integrate with this SDK to enable the in-app, non-custodial [Arcana wallet](https://docs.arcana.network/concepts/anwallet/) for authenticated users to sign blockchain transactions. To get started, see the [Auth Quickstart Guide](https://docs.arcana.network/quick-start/). Use the appropriate SDK flavor as per your [app type](https://docs.arcana.network/sdk-installation). ## In-app Arcana wallet The in-app, non-custodial [Arcana wallet](https://docs.arcana.network/concepts/anwallet/) is automatically available via the [Arcana Auth SDK](https://docs.arcana.network/concepts/authsdk). The SDK can be integrated with any app type, including [web, mobile, and gaming apps](https://docs.arcana.network/sdk-installation). The users of apps integrated with the Arcana Auth SDK can access the **peaq** chain via the in-app Arcana wallet, right out of the box. The peaq chain is preconfigured in the blockchain network list. Developers can choose to configure it as the [default active chain](https://docs.arcana.network/setup/config-dapp-with-db/) by using the [Arcana Developer Dashboard](https://dashboard.arcana.network/). arcana-1 App users don't need to install a browser extension to use the Arcana wallet or switch to the peaq chain. When a user logs into the app, they can instantly access the Arcana wallet and sign blockchain transactions on the peaq chain which is automatically selected as the active chain. This simplifies UX and speeds up app onboarding. arcana-2 Developers can tailor the user experience of signing blockchain transactions on the peaq chain. Besides the look and feel of the wallet, developers can also manage when the wallet UI is displayed within the app context to sign blockchain transactions. [Learn more](https://docs.arcana.network/user-guides/wallet-ui/). ## Enable Arcana wallet Users cannot directly access the Arcana wallet as a standalone application. App developers must integrate the app with the Arcana Auth SDK and enable users to access the Arcana Wallet within the app. Developers need to follow these steps for integration: 1. **Register the app** with the [Arcana Developer Dashboard](https://dashboard.arcana.network/), and copy the unique client identifier for the app. 2. **Configure Auth SDK usage** via the dashboard, specify social login options, wallet user experience settings, etc. 3. **Download and [install](https://docs.arcana.network/sdk-installation)** the Arcana Auth SDK, integrate the app, and add a single line of code to onboard users by using the plug-and-play login UI via the `connect()` method. After a successful user login, *Arcana wallet will automatically display within the app context*, enabling the user to sign blockchain transactions instantly. The in-app Arcana Wallet supports [JSON/RPC calls and web3 wallet operations](https://docs.arcana.network/auth/web3-ops/evm/). ## References * [Social login providers](https://docs.arcana.network/web3-stack/auth/) * [Blockchain networks](https://docs.arcana.network/web3-stack/chains/) * [Browsers](https://docs.arcana.network/web3-stack/browsers/) * [Supported app types](https://docs.arcana.network/web3-stack/apps/) * [Arcana Developer Dashboard](https://dashboard.arcana.network/) * [API](https://docs.arcana.network/auth/auth-usage-guide/) # Particle Network Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/account-abstraction/particle-network # What is it? [Particle Network](https://particle.network) is an L1 unifying all chains through Universal Accounts. They have deployed their complete Wallet Abstraction stack on peaq and agung, natively bringing social logins and account abstraction to the ecosystem. Using Particle Network's SDKs, applications built on peaq can onboard users into ERC-4337 smart contract wallets through Web2-adjacent mechanisms such as Google, Twitter, email, phone, and so on. ## Overview Developers can integrate and leverage Particle Network's Wallet Abstraction stack through a variety of SDKs spanning over nine different frameworks and platforms. This document will focus on the basic flow of building a React-based web application on peaq using Particle Network to onboard users into ERC-4337 smart accounts through social logins. To try Particle Network for yourself, head over to their [web demo](https://core-demo.particle.network). Additionally, a complete demo repository containing the code covered throughout this document can be found [here](https://github.com/TABASCOatw/particle-peaq-demo). By the end of this page, you'll understand the process of implementing an onboarding flow similar to the preview above. # Using Particle Network: Guide Particle Network's Wallet Abstraction stack can be leveraged through a variety of mechanisms, each varying in complexity and integration flow. These options are: * [Particle Connect](https://developers.particle.network/docs/particle-connect), a custom connection kit (similar to RainbowKit) that facilitates both social logins and Web3 wallet connections. Available for Web, Unity, Android, iOS, Flutter, and React Native. * [Particle Auth](https://developers.particle.network/docs/building-with-particle-auth), the primary library facilitating social logins through either a standard modal provided by Particle or lower-level shortcuts within your own interface. * External connection kits, such as [Web3Modal](https://developers.particle.network/docs/web3modal), [Web3-Onboard](https://developers.particle.network/docs/web3-onboard), and [RainbowKit](https://developers.particle.network/docs/rainbowkit). Any of the above libraries are capable of introducing social logins to your application with Particle Network. Tying account abstraction (ERC-4337) into these approaches requires the usage of Particle Network's standalone [AA SDK](https://developers.particle.network/reference/aa-web). This SDK leverages Particle's native Bundler and Paymaster deployed on peaq and agung. For this guide, we'll be using Particle Auth alongside Particle's AA SDK to facilitate a standard implementation of social logins. Particle Auth can be integrated through a variety of platforms and frameworks. This guide will focus on Particle Auth Core, a React-based SDK for web applications. To explore the integration process for alternative platforms, head to [Particle Network's documentation](https://developers.particle.network/reference/introduction-to-particle-auth). ## Part 1: Installation Working with Particle Auth Core alongside Particle's AA SDK involves the installation of a few core libraries, including: * `@particle-network/auth-core-modal`, the primary mechanism for facilitating social logins. * `@particle-network/aa`, for generating and assigning ERC-4337 smart accounts to traditional accounts (EOAs) created and linked to the user’s identity through social login. * `@particle-network/chains`, for using peaq. To install these libraries, run one of the two following commands: ```shell theme={"theme":{"light":"github-light-default","dark":"github-dark"}} yarn add @particle-network/auth-core-modal @particle-network/aa @particle-network/chains # OR npm install @particle-network/auth-core-modal @particle-network/aa @particle-network/chains ``` ## Part 2: Configuration Both `@particle-network/auth-core-modal` and `@particle-network/aa` need to be configured independently, although a common denominator between the two is the need for three key values from the [Particle dashboard](https://dashboard.particle.network): * **projectId** * **clientKey** * **appId** Collectively, these values authenticate each SDK. The retrieval process is as follows: 1. Log in or sign up to the [Particle dashboard](https://dashboard.particle.network). 2. Create a new project. 3. Within this project, create an application. 4. Copy the **Project ID**, **Client Key**, and **App ID** from the dashboard. If applicable, save these to environment variables within your application. *** Configuring Particle Auth involves the initialization of its core React component, **AuthCoreContextProvider**. This component will wrap the application in which we intend to use Particle Auth and, beyond the aforementioned values, contain parameters for customizing the embedded wallet modal, specifying the smart account implementation you intend to use, and so on. After importing **AuthCoreContextProvider** from `@particle-network/auth-core-modal`, open it within your JSX. You'll need to use the **options** property for configuration. This takes the following values: * `projectId`, `clientKey`, and `appId`. These are the values you found on the [Particle dashboard](https://dashboard.particle.network). * `wallet`, a collection of properties for configuring the embedded wallet modal that optionally shows after a user logs in with their social account. `wallet` contains: * `visible`, a Boolean dictating whether the embedded wallet interface is shown post-login. If `true`, this materializes by default through a button placed near the bottom right of the application. * `customStyle`, which, in this example, takes `supportChains`, an array of chain objects that dictate the blockchains supported within the embedded wallet modal. To lock this to peaq, import `PeaqKrest` or `PeaqAgungTestnet` from `@particle-network/chains`. * `erc4337`, used for specifying a Smart Account implementation to be shown within the embedded wallet modal rather than the EOA. If `visible` is `false` on `wallet`, ignore this property. `erc4337` contains: * `name`, the name of the Smart Account implementation used within your application. For both peaq and agung, this should be `'SIMPLE'`. * `version`, the version of the Smart Account implementation referenced in `name`. Currently, only `'1.0.0'` is supported with `'SIMPLE'`. After defining these various parameters, **AuthCoreContextProvider** should look like the following example (which is the **index.tsx** file of the aforementioned **create-react-app** example): ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // index.tsx import React from 'react' import ReactDOM from 'react-dom/client' import { PeaqKrest, PeaqAgungTestnet } from '@particle-network/chains'; import { AuthCoreContextProvider } from '@particle-network/auth-core-modal'; import App from './App' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( // Where Particle Auth will be used ) ``` *** Additionally, Particle Network's AA SDK needs to be configured in a very similar fashion. However, rather than being initialized through a React component, `@particle-network/aa` will need to be configured within the same component where you intend to leverage Particle Auth. Specifically, `@particle-network/aa` has a key "master" object, **SmartAccount**, which, once initialized, enables end-to-end management of the user's smart account. After configuring Particle Auth Core, you'll need to define **provider** from the **useEthereum** hook (imported through `@particle-network/auth-core-modal`) within your application. **provider** represents the EIP-1193 provider object we'll need to configure an attached smart account. Below is an example of this. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { provider } = useEthereum(); // EIP-1193 provider ... } ```
Using **provider**, you'll need to define a new instance of **SmartAccount**. In addition to **provider**, **SmartAccount** takes an object which contains: * `projectId`, `clientKey`, and `appId`, as was previously defined in `AuthCoreContextProvider`. * `aaOptions`, containing: * `accountContracts`, a collection of the Smart Account implementations you intend to support. For peaq, this should just be: * `SIMPLE`, an array of objects which takes: * `chainIds`, an array of chain IDs (integers) for the blockchain(s) you'll be using the Smart Account on. * `version`, the version of the Smart Account you'll use; in the case of `SIMPLE`, this should be `'1.0.0'`. Therefore, defining an instance of **SmartAccount** should look similar to the snippet below: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { provider } = useEthereum(); const smartAccount = new SmartAccount(provider, { projectId: process.env.REACT_APP_PROJECT_ID, clientKey: process.env.REACT_APP_CLIENT_KEY, appId: process.env.REACT_APP_APP_ID, aaOptions: { accountContracts: { SIMPLE: [{ chainIds: [PeaqAgungTestnet.id], version: '1.0.0' }] } } }); } ```
In this example, **smartAccount** is the central source for controlling and reading data from the smart account attached to the user's social login. ## Part 3: Social Login Using Particle Auth, users are onboarded through traditional Web2 social accounts such as Google, Twitter, email, and so on. To initiate social logins programmatically, you'll need to use the **useConnect** hook from `@particle-network/auth-core-modal`. **useConnect** exposes the **connect** function, which directly handles social logins. This takes the following parameters: * `socialType`, the specific social login mechanism to be opened. If this is left as a blank string (`''`), a generalized interface will open, allowing a user to enter their email, choose an external social account, etc. Otherwise, if a string such as `'google'` or `'twitter'` is used, these will be opened directly. * `chain`, the blockchain to be connected to. This should be an object from `@particle-network/chains`, either `PeaqKrest` or `PeaqAgungTestnet` in this case. Below is a snippet showcasing an example implementation of `connect`: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { connect, disconnect } = useConnect(); const handleLogin = async (authType) => { if (!userInfo) { await connect({ socialType: authType, // 'google', 'twitter', etc. - can also be '' chain: PeaqAgungTestnet, // or PeaqKrest }); } }; ... disconnect(); // Often mapped to an element within your JSX } ```
After logging in, **provider** will be populated and, by proxy, **SmartAccount** will be initialized. At this point, you'll be ready to execute transactions. ## Part 4: Transaction Execution Application interaction, or transaction execution, can be done through one of two ways with `@particle-network/aa`, either: 1. Through the instance of `SmartAccount` directly. 2. Using an external Web3 library such as Ethers or Web3.js. ### Option 1: Using `SmartAccount` Instances of **SmartAccount** have various methods capable of constructing and executing transactions, otherwise known as UserOperations (within the ERC-4337 standard). These methods are as follows: * `sendTransaction` * `sendUserOperation` * `sendSignedUserOperation` * `buildUserOperation` * `getFeeQuotes` * `signUserOperation` These vary in granularity and operational significance; although for this example we'll focus on the most straightforward method, `sendTransaction`. For information about the others listed above, head over to [Particle Network's documentation](https://developers.particle.network/reference/aa-web). `sendTransaction` can be used to construct and execute any standard transaction; just as you would with Ethers, Web3.js, or any related library. Transactions should be constructed using typical parameters such as `to`, `value`, and `data`. Upon calling `{your SmartAccount instance}.sendTransaction`, the user will be asked to confirm the transaction through an in-app popup. Upon doing so, it'll be executed on-chain. The snippet below is an example of this approach: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { provider } = useEthereum(); // Smart Account configuration const smartAccount = new SmartAccount(provider, { projectId: process.env.REACT_APP_PROJECT_ID, clientKey: process.env.REACT_APP_CLIENT_KEY, appId: process.env.REACT_APP_APP_ID, aaOptions: { accountContracts: { SIMPLE: [{ chainIds: [PeaqAgungTestnet.id], version: '1.0.0' }] } } }); ... // Executing a burn of 0.001 AGUNG const executeUserOp = async () => { const tx = { to: "0x000000000000000000000000000000000000dEaD", value: ethers.utils.parseEther("0.001"), }; const txResponse = smartAccount.sendTransaction(tx) return txResponse; }; } ``` ### Option 2: Using Ethers More commonly, an instance of **SmartAccount** can be used in the construction of an instance of Ethers (or Web3.js, viem, and so on), allowing for a more standardized mechanism of programmatic interaction. This is done by building an intermediary EIP-1193 provider object using **AAWrapProvider** from `@particle-network/aa` within your instance of **SmartAccount**. After plugging this into an object such as **new ethers.providers.Web3Provider**, you’ll be able to interact with the smart account directly through Ethers. Below is an example of this configuration process: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { connect, disconnect } = useConnect(); const { provider } = useEthereum(); // Smart Account configuration const smartAccount = new SmartAccount(provider, { projectId: process.env.REACT_APP_PROJECT_ID, clientKey: process.env.REACT_APP_CLIENT_KEY, appId: process.env.REACT_APP_APP_ID, aaOptions: { accountContracts: { SIMPLE: [{ chainIds: [PeaqAgungTestnet.id], version: '1.0.0' }] } } }); // Ethers provider construction const customProvider = new ethers.providers.Web3Provider(new AAWrapProvider(smartAccount, SendTransactionMode.Gasless), "any"); } ```
From this point, your Ethers instance can be used to construct and execute transactions as normal, automatically routing signatures to the embedded wallet generated through social login. ## Conclusion Below is an example of an application component implementing all of the previously covered code snippets: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // App.tsx import { useEthereum, useConnect, useAuthCore } from '@particle-network/auth-core-modal'; import { PeaqAgungTestnet } from '@particle-network/chains'; import { AAWrapProvider, SmartAccount } from '@particle-network/aa'; import { ethers } from 'ethers'; const App = () => { const { connect, disconnect } = useConnect(); const { provider } = useEthereum(); // Smart Account configuration const smartAccount = new SmartAccount(provider, { projectId: process.env.REACT_APP_PROJECT_ID, clientKey: process.env.REACT_APP_CLIENT_KEY, appId: process.env.REACT_APP_APP_ID, aaOptions: { accountContracts: { SIMPLE: [{ chainIds: [PeaqAgungTestnet.id], version: '1.0.0' }] } } }); // Ethers provider construction const customProvider = new ethers.providers.Web3Provider(new AAWrapProvider(smartAccount, SendTransactionMode.Gasless), "any"); // Facilitating social login const handleLogin = async (authType) => { if (!userInfo) { await connect({ socialType: authType, chain: PeaqAgungTestnet, }); } }; // Executing a burn of 0.001 ETH const executeUserOp = async () => { const signer = customProvider.getSigner(); const tx = { to: "0x000000000000000000000000000000000000dEaD", value: ethers.utils.parseEther("0.001"), }; const txResponse = await signer.sendTransaction(tx); const txReceipt = await txResponse.wait(); return txReceipt; }; } ```
*Extending this tutorial, a demo application containing this same code can be found [here](https://github.com/TABASCOatw/particle-peaq-demo). You can try it within your own browser [here](https://particle-peaq-demo.replit.app).*
Using Particle Network's Wallet Abstraction stack, you can now implement Web2-like user onboarding flows and account abstraction capabilities on peaq with only a few lines of code, as demonstrated here. To learn more about Particle Network and its various SDKs, take a look at the following links: * [https://developers.particle.network](https://developers.particle.network) * [https://particle.network](https://particle.network) * [https://blog.particle.network](https://blog.particle.network)


**Documentation provided by [Particle](https://particle.network/), 2024.** # Introduction Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/erc-8004/intro # Overview peaq enables autonomous AI agents to operate in open machine economies. For agents to safely interact with each other (discovering services, evaluating trust, and validating capabilities) they need a standardized identity and reputation layer. **ERC-8004 on peaq provides this foundation**. It allows AI agents to: * Establish a verifiable on-chain identity * Publish service endpoints and metadata * Build and query reputation * Validate claims and capabilities * Make trust-based decisions autonomously Together, these components form the trust infrastructure for AI agents on peaq. The following documentation explains how to integrate ERC-8004 into your agent architecture and begin building trusted agent systems on peaq. ## Network Contract Addresses ### peaq (Mainnet) ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} IdentityRegistry: 0x2154317da929098A033ac1ef869d6A8bB771A0e3 ReputationRegistry: 0x3f68D8b74208023Bcb6617C305e22080eb2fF6C0 ValidationRegistry: 0x9E9463a65c7B74623b3b6Cdc39F71be7274e5971 ``` ### agung (Testnet) ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} IdentityRegistry: 0x567b6953f29Ef2F2f6a592fFeccCE4A7AbE35928 ReputationRegistry: 0xC4421B43917aF1eF2b352AE7dfcFef0Ff181409e ValidationRegistry: 0x6e4E6D77E6a3c704249db0f550C19E80821cbD7d ``` Developers are encouraged to integrate and test on agung before deploying production agents on peaq mainnet. For more information on how to establish a peaq/agung connection please refer to the [connecting to peaq page](/peaqchain/build/getting-started/connecting-to-peaq). ## Source Code & ABIs The ERC-8004 implementation used on peaq is available in the official peaq [github repository](https://github.com/peaqnetwork/erc-8004-contracts/tree/peaq). This repository contains: * Smart contract source code * Deployment configuration * ABI files * Upgradeable implementation details Developers may reference the repository directly when integrating, verifying contracts, or building advanced tooling on top of ERC-8004. ## What ERC-8004 Enables ERC-8004 introduces three core registries that work together: | Component | Purpose | | ----------------------- | --------------------------------------- | | **Identity Registry** | Agent identity and discoverability | | **Reputation Registry** | Trust and feedback between agents | | **Validation Registry** | Verifiable claims and capability checks | These registries allow agents to operate in a decentralized environment where they can: * Discover other agents * Evaluate trustworthiness * Record interaction outcomes * Validate capabilities before transacting Ultimately, creating a trust layer for autonomous machine and agent economies. ## Core Architecture ### Identity Layer: Agent Discovery The Identity Registry enables an AI agent to publish: * Its unique identity * Metadata describing capabilities * Service endpoints * Public keys or verification data Once registered, an agent becomes discoverable by other agents and applications. ### Reputation Layer: Trust Between Agents The Reputation Registry enables agents to leave feedback about interactions. This creates: * verifiable trust history * performance scoring * reliability metrics * interaction history Agents can use this data to determine: *Should I interact or transact with this agent?* Reputation becomes a programmable trust signal. ### Validation Layer: Verifiable Claims The Validation Registry enables structured validation requests and responses between agents. This allows agents to: * Verify capabilities * Confirm service delivery * Validate credentials * Record proof of execution This forms the basis of *agent-to-agent verification and trust automation.* ## How AI Agents Use ERC-8004 An AI agent operating on peaq typically follows this lifecycle: 1. **Register identity**: The agent publishes metadata and endpoints via the Identity Registry. 2. **Become discoverable**: Other agents can query and find the agent. 3. **Interact with other agents**: Agents exchange services, data, or value. 4. **Record reputation**: Agents leave feedback on interactions. 5. **Validate capabilities**: Agents can request or respond to validation checks. This loop creates a continuously improving trust graph across the agent ecosystem. ### Real-life example: peaq ERC-8004 Escrow To see ERC-8004 in action end-to-end—identity registration, claim creation and acceptance, staking, and cross-chain payment—see the **[peaq ERC-8004 Escrow](/peaqchain/build/advanced-operations/erc-8004/omnichain-escrow)** flow. It walks through a buyer machine (Unitree) and seller machine (Drone) registering on peaq, creating and accepting a service claim, funding escrow on Base (USDT via LayerZero), and releasing payment while peaq stays in sync. That page is the concrete implementation of the lifecycle above. We will present the quick start guide below to get you started with ERC-8004 on our testnet (agung). You must modify the code to use your own predefined business logic to be interoperable with systems you interact with. ## Quick Start The following is a quick start guide to get you started with ERC-8004 on our testnet (agung). ### 1. Install ethers & .env ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm init -y npm pkg set type=module npm install ethers dotenv ``` ### 2. Create a .env file ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PRIVATE_KEY=0xYOUR_PRIVATE_KEY FEEDBACK_PRIVATE_KEY=FEEDBACK_PRIVATE_KEY RPC_URL=https://peaq-agung.api.onfinality.io/public IDENTITY_REGISTRY_ADDRESS= REPUTATION_REGISTRY_ADDRESS= VALIDATION_REGISTRY_ADDRESS= ``` ### 3. Set abis files The following cmd copies the abis files from [peaq's github fork](https://github.com/peaqnetwork/erc-8004-contracts/tree/peaq/abis) to the local abis directory. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} mkdir abis curl -o abis/IdentityRegistry.json https://raw.githubusercontent.com/peaqnetwork/erc-8004-contracts/peaq/abis/IdentityRegistry.json curl -o abis/ReputationRegistry.json https://raw.githubusercontent.com/peaqnetwork/erc-8004-contracts/peaq/abis/ReputationRegistry.json curl -o abis/ValidationRegistry.json https://raw.githubusercontent.com/peaqnetwork/erc-8004-contracts/peaq/abis/ValidationRegistry.json ``` ## Create AI Agent ### 1. Register Your AI Agent Create a file called `register-agent.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import IdentityABI from "./abis/IdentityRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const identity = new ethers.Contract( process.env.IDENTITY_REGISTRY_ADDRESS, IdentityABI, wallet ); const agentURI = "ipfs://QmYourMetadata"; // or https://... const metadata = [ ["name", ethers.toUtf8Bytes("AgentName")], ["api", ethers.toUtf8Bytes("https://api.agentname.ai")], ["capabilities", ethers.toUtf8Bytes(JSON.stringify(["capability1", "capability2"]))], ]; async function main() { const tx = await identity["register(string,(string,bytes)[])"](agentURI, metadata); console.log("Transaction sent:", tx.hash); const receipt = await tx.wait(); console.log("Mined in block:", receipt.blockNumber); const reg = receipt.logs .map((l) => { try { return identity.interface.parseLog(l); } catch { return null; } }) .find((e) => e?.name === "Registered"); if (!reg) throw new Error("Registered event not found in receipt logs"); const agentId = reg.args.agentId; console.log("agentId:", agentId.toString()); console.log("tokenURI:", await identity.tokenURI(agentId)); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node register-agent.js` ### 2. Query Your Agent Create a file called `query-agent.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import IdentityABI from "./abis/IdentityRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const identity = new ethers.Contract( process.env.IDENTITY_REGISTRY_ADDRESS, IdentityABI, provider ); // replace with the agentId printed from register-agent.js const agentId = BigInt("1"); async function main() { const uri = await identity.tokenURI(agentId); console.log("tokenURI:", uri); const nameBytes = await identity.getMetadata(agentId, "name"); console.log("name:", ethers.toUtf8String(nameBytes)); const capsBytes = await identity.getMetadata(agentId, "capabilities"); console.log("capabilities:", JSON.parse(ethers.toUtf8String(capsBytes))); const agentWallet = await identity.getAgentWallet(agentId); console.log("agentWallet:", agentWallet); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node query-agent.js`. Make sure you get the agentId from the register-agent.js script. ### 3. Update Agent Metadata Create a file called `update-agent-metadata.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import IdentityABI from "./abis/IdentityRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const identity = new ethers.Contract( process.env.IDENTITY_REGISTRY_ADDRESS, IdentityABI, wallet ); const agentId = BigInt("1"); async function main() { const tx1 = await identity.setAgentURI(agentId, "ipfs://QmNewMetadata"); await tx1.wait(); console.log("Updated tokenURI:", await identity.tokenURI(agentId)); const tx2 = await identity.setMetadata( agentId, "api", ethers.toUtf8Bytes("https://api.agentname.ai/v2") ); await tx2.wait(); const apiBytes = await identity.getMetadata(agentId, "api"); console.log("api:", ethers.toUtf8String(apiBytes)); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node update-agent-metadata.js`. Make sure you get the agentId from the register-agent.js script. ## Reputation Interactions ### 1. Leave One Reputation Feedback Record The ReputationRegistry writes feedback about an agentId, from the caller's wallet (the clientAddress in events). You cannot leave feedback for your own agentId, therefore another wallet must be used. Create a file called `give-feedback.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import ReputationABI from "./abis/ReputationRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.FEEDBACK_PRIVATE_KEY, provider); const reputation = new ethers.Contract( process.env.REPUTATION_REGISTRY_ADDRESS, ReputationABI, wallet ); const agentId = BigInt("1"); // agent being reviewed // Example: a 1–5 rating stored as value=5 with valueDecimals=0 const value = 5; const valueDecimals = 0; // Tags are for grouping/aggregation in getSummary() // Keep tag1 consistent across your app (e.g. "quality", "latency", "reliability") const tag1 = "quality"; const tag2 = "v1"; // endpoint: the specific interface used const endpoint = "https://api.agentname.ai/v2"; // feedbackURI: off-chain JSON describing the interaction (ipfs or https) const feedbackURI = "ipfs://QmYourFeedbackJson"; // feedbackHash: hash of the feedback JSON bytes for integrity // In a real app: hash the canonical JSON bytes you publish at feedbackURI. const feedbackHash = ethers.keccak256(ethers.toUtf8Bytes("example-feedback-payload")); async function main() { const tx = await reputation.giveFeedback( agentId, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash ); console.log("TX:", tx.hash); const receipt = await tx.wait(); console.log("Mined in block:", receipt.blockNumber); console.log("Feedback submitted for agentId:", agentId.toString()); console.log("Client (reviewer):", wallet.address); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node give-feedback.js`. ### 2. Read Reputation Back Create a file called `read-reputation.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import ReputationABI from "./abis/ReputationRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const reputation = new ethers.Contract( process.env.REPUTATION_REGISTRY_ADDRESS, ReputationABI, provider ); const agentId = BigInt("1"); const tag1 = "quality"; const tag2 = "v1"; async function main() { const clientsResult = await reputation.getClients(agentId); console.log("Clients who reviewed agent:", clientsResult); const clients = Array.from(clientsResult); // or [...clientsResult] // Summary for these clients and tags const [count, summaryValue, summaryDecimals] = await reputation.getSummary( agentId, clients, tag1, tag2 ); const scaled = Number(summaryValue) / 10 ** Number(summaryDecimals); console.log(`Summary (${tag1}/${tag2}) count=${count} value=${scaled}`); // Raw feedback rows const includeRevoked = false; const [ rowClients, feedbackIndexes, values, valueDecimals, tag1s, tag2s, revokedStatuses ] = await reputation.readAllFeedback(agentId, clients, tag1, tag2, includeRevoked); for (let i = 0; i < values.length; i++) { const v = Number(values[i]) / 10 ** Number(valueDecimals[i]); console.log( `#${feedbackIndexes[i]} client=${rowClients[i]} value=${v} revoked=${revokedStatuses[i]}` ); } } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node read-reputation.js`. ## Validation Request + Response Your ValidationRegistry flow is: 1. **Someone creates a request:** `validationRequest(validatorAddress, agentId, requestURI, requestHash)` 2. **The validator responds:** `validationResponse(requestHash, response, responseURI, responseHash, tag)` 3. **Anyone can query:** `getValidationStatus(requestHash) or getSummary(agentId, validators, tag)` **Who is the validator?** In this quickstart, simplest is: you validate yourself (validatorAddress = your wallet). In real apps: validator is a third party, a DAO, or a known attester. ### 1. Validation Request Create a file called `validation-request.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import ValidationABI from "./abis/ValidationRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const validation = new ethers.Contract( process.env.VALIDATION_REGISTRY_ADDRESS, ValidationABI, wallet ); const agentId = BigInt("1"); // In quickstart, we self-validate. Replace with a 3rd-party validator in production. const validatorAddress = wallet.address; // Off-chain request payload (describe what’s being validated) const requestURI = "ipfs://QmYourNewValidationRequestJson"; const requestHash = ethers.keccak256(ethers.toUtf8Bytes("example-new-validation-request")); async function main() { const tx = await validation.validationRequest( validatorAddress, agentId, requestURI, requestHash ); console.log("TX:", tx.hash); await tx.wait(); console.log("Validation request created:"); console.log("requestHash:", requestHash); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node validation-request.js`. Make sure to record the `requestHash` which will be used in the validation response. ### 2. Validation Response Create a file called `validation-response.js` and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import ValidationABI from "./abis/ValidationRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const validation = new ethers.Contract( process.env.VALIDATION_REGISTRY_ADDRESS, ValidationABI, wallet ); // must match requestHash used in validation-request.js const requestHash = "request_hash_from_request_js"; // e.g. 0xabc123... if (!requestHash) throw new Error("Set REQUEST_HASH=0x... before running."); const response = 1; // uint8: project-defined (e.g. 0=unknown, 1=pass, 2=fail) const responseURI = "ipfs://QmYourNewValidationRequestJson"; const responseHash = ethers.keccak256(ethers.toUtf8Bytes("example-new-validation-response")); const tag = "capability:capability1"; async function main() { const tx = await validation.validationResponse( requestHash, response, responseURI, responseHash, tag ); console.log("TX:", tx.hash); await tx.wait(); console.log("Validation response submitted for requestHash:", requestHash); const status = await validation.getValidationStatus(requestHash); console.log("Status:", status); } main().catch((e) => { console.error(e); process.exit(1); }); ``` Run the script: `node validation-response.js`. ### 3. Read Validation Status + Summary Create a file called `read-validation-status.js`. Add the following code, and make sure the validator address is set to the one set in the validation request. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; import "dotenv/config"; import ValidationABI from "./abis/ValidationRegistry.json" with { type: "json" }; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const validation = new ethers.Contract(process.env.VALIDATION_REGISTRY_ADDRESS, ValidationABI, provider); const agentId = BigInt("1"); async function main() { const validatorAddresses = ["validatorAddress"]; const tag = "capability:capability1"; const [count, avgResponse] = await validation.getSummary(agentId, validatorAddresses, tag); console.log(`Validation summary tag=${tag} count=${count} avgResponse=${avgResponse}`); } main().catch(console.error); ``` Run the script: `node read-validation-status.js`. ## Final Notes ERC-8004 provides the foundational infrastructure for agent identity, reputation, and validation on peaq. However, it is intentionally flexible and unopinionated about how these signals are interpreted or enforced. Each application, agent framework, or marketplace must define its own business logic around: * how reputation scores are calculated and interpreted * what validation responses mean * which validators or reviewers are trusted * how feedback impacts agent permissions or interactions * what thresholds determine trust or access The contracts provide the **shared trust layer**, but your application must define how that trust is used. For example, your system may choose to: * Only interact with agents above a reputation threshold * Require validation from specific validators * Weight feedback differently depending on tags or source * Implement staking or slashing around reputation * Gate services based on validation outcomes These decisions live at the application layer and should reflect your specific use case, risk tolerance, and economic model. ERC-8004 is most powerful when combined with clear, intentional business logic that governs how agents interact within your ecosystem. Build thoughtfully, define your trust model carefully, and use these primitives to enable secure and autonomous agent interactions on peaq. # peaq ERC-8004 Escrow Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/erc-8004/omnichain-escrow This page is the **real-life example** of the [ERC-8004 agent lifecycle](/peaqchain/build/advanced-operations/erc-8004/intro) described in the introduction. This flow lets a **buyer machine** (Unitree) and a **seller machine** (Drone) register identities on peaq, create and accept a service claim on peaq, fund the claim from Base (USDT locked in a Base escrow), and then release payment on Base while peaq is kept in sync via LayerZero messages. Omnichain Escrow Purchase Flow ## Actors & Contracts ### Actors | Actor | Role | | ------------------- | --------------------------------- | | **Unitree (Buyer)** | Purchases a service | | **Drone (Seller)** | Provides the service | | **peaq Chain** | Identity + claim + state tracking | | **Base Chain** | Holds USDT escrow and pays seller | | **LayerZero** | Transports cross-chain messages | ### Key Components **peaq** * **Identity / registration** — `register(agent-card.json)` → creates machine/agent identity (`agent_id`) * **ClaimRegistry** — creates/accepts claims, locks stakes, tracks status * **PaymentReceiver** — receives LayerZero messages (`lzReceive`) and updates peaq state * **TransactionRegistry** — records funding/release events (auditability) **Base** * **BaseEscrow** — locks USDT, releases USDT to seller, emits cross-chain messages *** ## Phase 1 — Identity registration on peaq **Goal:** Both parties become on-chain actors on peaq. 1. **Unitree → peaq:** `register("unitree-agent-card.json")`\ **peaq → Unitree:** `agent_id` 2. **Drone → peaq:** `register("drone-agent-card.json")`\ **peaq → Drone:** `agent_id` **Result:** Both buyer and seller have peaq identities and can participate in claims. *** ## Phase 2 — Create & accept claim on peaq **Goal:** Create a service purchase intent on peaq and have the seller accept it with stake. ### Buyer (Unitree) 1. Ensure buyer has USDC on peaq for stake (mint if needed). 2. Approve staking: `approve(ClaimRegistry, stake)`. 3. Ensure buyer has USDT on Base (mint if needed). 4. Create the claim on peaq:\ `create_purchase_claim(seller=Drone, service_id, amount, deadline)`\ **peaq → Unitree:** `claim_id` 5. Share `claim_id` to Drone (off-chain / manual in this diagram). ### Seller (Drone) 6. Fetch details: `get_claim(claim_id)`. 7. Verify claim + seller address (local checks). 8. Ensure seller has USDC on peaq for stake (mint if needed). 9. Approve staking: `approve(ClaimRegistry, stake)`. 10. Accept claim: `accept_claim(claim_id)`\ **peaq:** locks seller stake\ **peaq → Drone:** status `Accepted`. **Result:** Claim exists and is accepted; both sides can poll status on peaq. *** ## Phase 3 — Cross-chain funding (Base → peaq) **Goal:** Lock funds in Base escrow and mark the claim as **Funded** on peaq via LayerZero. 1. **Unitree → peaq:** `get_claim(claim_id)` (to confirm deadline). 2. **Unitree → Base:** `approve(BaseEscrow, USDT amount)`. 3. **Unitree → Base:** `BaseEscrow.deposit(claim_id, amount, seller, deadline)`\ **BaseEscrow:** locks USDT. 4. **Base → LayerZero:** sends cross-chain “funded” message. 5. **LayerZero → peaq:** delivers to PaymentReceiver. 6. **peaq.PaymentReceiver:** `lzReceive()` * updates TransactionRegistry * updates claim status → **Funded**. **Result:** Funds are escrowed on Base; peaq reflects “Funded”. *** ## Phase 4 — Release payment on Base + finalize on peaq **Goal:** Seller performs the service off-chain; buyer releases escrowed payment; peaq finalizes status and returns stakes. 1. **Drone:** Performs the real-world service (off-chain). 2. **Unitree → Base:** `BaseEscrow.release(claim_id)`\ **BaseEscrow:** transfers USDT to Drone\ **Base → Drone:** USDT received. 3. **Base → LayerZero:** sends “completed” message. 4. **LayerZero → peaq:** delivers completion to PaymentReceiver. 5. **peaq.PaymentReceiver:** * updates TransactionRegistry * updates claim status → **Completed** * returns stakes to buyer & seller. **Result:** Payment settled on Base; peaq records completion and settles stakes. *** ## State machine (peaq claim status) **Created → Accepted → Funded → Completed** *** ## What to poll / verify * **Buyer & Seller** poll on peaq: `get_claim(claim_id)` to read status + deadline. * **Funding truth** lives on Base: USDT is locked/released by BaseEscrow. * **peaq** is the coordination ledger: identity, claim lifecycle, audit trail, stake settlement. * **LayerZero** is the sync bridge: delivers funding + completion events to peaq. *** ## Outcome A single claim ties together: * **Identity + staking + lifecycle** on peaq * **USDT custody + payout** on Base * **Cross-chain state synchronization** via LayerZero The transaction is complete when Base pays out and peaq marks **Completed** and returns stakes. *** ## Contract addresses ### peaq | Contract | Address | | -------------------- | -------------------------------------------- | | Identity Registry | `0x2154317da929098A033ac1ef869d6A8bB771A0e3` | | Claim Registry | `0x48E33cF40D427004313760F1E514A7488e8DF0Cc` | | Transaction Registry | `0xe2C58c1A0d18E87F0cD0f658E2f2806c3458C374` | | Mock USDC | `0x0Ad7E419358a462649d2D53b5C2B25cDf85Ca009` | ### LayerZero (peaq) | Contract | Address | | ---------------- | -------------------------------------------- | | Payment Receiver | `0x49850c56cf8AAF27Ee20f78753D6A4f7C73EBB81` | | peaq LZ Endpoint | `0x6F475642a6e85809B1c36Fa62763669b1b48DD5B` | ### Base | Contract | Address | | ---------------- | -------------------------------------------- | | Base Escrow | `0x76C73B6a1A145b9D578c474efeBA83191dF5DCC3` | | Base USDT | `0x280BEeeD2EAb5F661d664C26B2A808cEdf70be2d` | | Base LZ Endpoint | `0x1a44076050125825900e736c501f859c50fE728c` | # Sim by Dune Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/indexers/sim-by-dune ## Overview The following guide introduces how to work with peaq's developer tooling to access live onchain data, interact with machine accounts, and build production-ready applications without managing your own indexing or infrastructure. It is achieved through Sim by Dune. To understand more please refer to the [Sim By Dune Documentation](https://docs.sim.dune.com/). ## What you'll learn This guide walks through the core building blocks required to develop on peaq: * Getting Started Set up your development environment, configure API access, and connect to the peaq network. * Core data interfaces Learn how to query balances, transactions, and asset metadata. ## Quick Start ### 1. Get API Key You will need an API key from the Sim dashboard to get started. 1. Go to the [Sim dashboard](https://sim.dune.com/) 2. Click on your username in the top right corner (or create an account) 3. Click on `Keys` 4. Click on `New` 5. Enter a name for your API Key 6. Click on `Create Key` 7. Copy the API Key Save the API Key in a secure location and never share it in public repositories or in your code. You will need it to access the Sim API. ### 2. Set up your development environment You will need to set up your development environment to get started. We will be using JavaScript to interact with the Sim API. Open a new terminal and run the following command to create a new project: ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm init -y npm pkg set type=module npm install axios dotenv ``` Create `.env` file: ``` API_KEY=your_sim_api_key_here WALLET_ADDRESS=0x...your_test_address CHAIN_ID=3338 ``` ### 3. API Call Create a new file called `sim.js` and add the following code: ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import axios from 'axios'; const SIM_API_BASE = 'https://api.sim.dune.com/v1'; const headers = { 'X-Sim-Api-Key': process.env.API_KEY, 'Content-Type': 'application/json' }; async function getPeaqBalances() { try { console.log('Fetching peaq balances...'); console.log(`Using wallet address: ${process.env.WALLET_ADDRESS}`); const response = await axios.get(`${SIM_API_BASE}/evm/balances/${process.env.WALLET_ADDRESS}?chain_ids=${process.env.CHAIN_ID}`, { headers }); const peaqBalances = response.data.balances.filter((balance) => balance.chain === 'peaq'); console.log('peaq Balances:'); peaqBalances.forEach((balance) => { console.log(` ${balance.symbol}: ${balance.amount / 10 ** balance.decimals} (${balance.value_usd} USD)`); }); } catch (error) { console.error('❌ Error fetching balances:', error.response?.data || error.message); } } getPeaqBalances(); ``` Execute with: ``` node sim.js ``` ## Additional API Examples ### Get Token Balances ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getPeaqBalances() { try { console.log('Fetching peaq balances...'); console.log(`Using wallet address: ${process.env.WALLET_ADDRESS}`); const response = await axios.get(`${SIM_API_BASE}/evm/balances/${process.env.WALLET_ADDRESS}?chain_ids=${process.env.CHAIN_ID}`, { headers }); const peaqBalances = response.data.balances.filter((balance) => balance.chain === 'peaq'); console.log('peaq Balances:'); peaqBalances.forEach((balance) => { console.log(` ${balance.symbol}: ${balance.amount / 10 ** balance.decimals} (${balance.value_usd} USD)`); }); } catch (error) { console.error('❌ Error fetching balances:', error.response?.data || error.message); } } ``` ### Monitor Wallet Transactions ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function monitorPeaqTransactions() { try { console.log(`Monitoring transactions for ${process.env.WALLET_ADDRESS}...`); const response = await axios.get(`${SIM_API_BASE}/evm/transactions/${process.env.WALLET_ADDRESS}`, { headers, params: { chain: 'peaq', limit: 10 } }); const transactions = response.data.transactions; console.log('🔄 Recent Transactions:'); transactions.forEach((tx) => { console.log(` Hash: ${tx.hash}`); console.log(` From: ${tx.from} → To: ${tx.to}`); console.log(` Value: ${tx.value} PEAQ`); console.log(` Gas Used: ${tx.gas_used}`); console.log(` Status: ${tx.success}`); console.log(' ---'); }); } catch (error) { console.error('❌ Error monitoring transactions:', error.message); } } ``` ### Get Token Metadata ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getTokenInfo(tokenAddress) { try { console.log('Fetching token information...'); const response = await axios.get(`${SIM_API_BASE}/evm/token-info/${tokenAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers }); const tokenInfo = response.data.tokens[0]; console.log('Token Information:'); console.log(` Name: ${tokenInfo.name}`); console.log(` Symbol: ${tokenInfo.symbol}`); console.log(` Decimals: ${tokenInfo.decimals}`); console.log(` Total Supply: ${tokenInfo.total_supply}`); console.log(` Current Price: ${tokenInfo.price_usd}`); console.log(` Logo: ${tokenInfo.logo}`); return tokenInfo; } catch (error) { console.error('❌ Error fetching token info:', error.message); } } // Example usage with USDC address getTokenInfo('0xbba60da06c2c5424f03f7434542280fcad453d10'); ``` You can see an exhaustive list of token addresses on [peaq's token list](https://tokenlist.peaq.xyz/). ### Find Top Holders ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getTokenHolders(tokenAddress, limit = 10) { try { console.log('Fetching token holders...'); const response = await axios.get(`${SIM_API_BASE}/evm/token-holders/${process.env.CHAIN_ID}/${tokenAddress}`, { headers, params: { limit } }); const holders = response.data.holders; console.log(`Top ${holders.length} holders:`); holders.forEach((holder, index) => { console.log(`${index + 1}. ${holder.wallet_address}`); console.log(` Balance: ${parseFloat(holder.balance)}`); // Make sure to parse if you want human readable format console.log(` First Acquired: ${holder.first_acquired}`); console.log(' ---'); }); return holders; } catch (error) { console.error('❌ Error fetching token holders:', error.message); } } ``` ### Track Swaps and Transfers ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getWalletActivity(walletAddress, limit = 10) { try { console.log('Fetching wallet activity on peaq...'); const response = await axios.get(`${SIM_API_BASE}/evm/activity/${walletAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers, params: { limit } }); const activities = response.data.activity; console.log(`Recent activities (${activities.length}):`); activities.forEach((activity, index) => { console.log(`${index + 1}. Type: ${activity.type.toUpperCase()}`); console.log(` Hash: ${activity.tx_hash}`); console.log(` Time: ${new Date(activity.block_time).toLocaleString()}`); if (activity.type === 'receive' && activity.asset_type === 'erc20') { console.log(` Received: ${activity.value} ${activity.token_metadata?.symbol}`); console.log(` From: ${activity.from}`); } console.log(' ---'); }); return activities; } catch (error) { console.error('❌ Error fetching activity:', error.message); } } ``` ### Get NFT Collections ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getNFTCollections(walletAddress) { try { console.log('Fetching NFT collections...'); const response = await axios.get(`${SIM_API_BASE}/evm/collectibles/${walletAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers }); const collectibles = response.data.entries; console.log(`NFT Collections (${collectibles.length}):`); // Group by collection const collections = {}; collectibles.forEach((nft) => { if (!collections[nft.contract_address]) { collections[nft.contract_address] = { name: nft.collection_name, items: [] }; } collections[nft.contract_address].items.push(nft); }); Object.entries(collections).forEach(([address, collection]) => { console.log(`📚 ${collection.name} (${collection.items.length} items)`); console.log(` Contract: ${address}`); collection.items.slice(0, 3).forEach((nft) => { console.log(` • Token ID: ${nft.token_id}`); console.log(` Name: ${nft.name || 'Unnamed'}`); console.log(` Image: ${nft.image_url || 'No image'}`); }); if (collection.items.length > 3) { console.log(` ... and ${collection.items.length - 3} more`); } console.log(' ---'); }); return collectibles; } catch (error) { console.error('❌ Error fetching NFTs:', error.message); } } ``` ## Advanced Operations ### Portfolio Analytics ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import axios from 'axios'; const SIM_API_BASE = 'https://api.sim.dune.com/v1'; const headers = { 'X-Sim-Api-Key': process.env.API_KEY, 'Content-Type': 'application/json' }; async function buildAdvancedPortfolio(walletAddress) { try { console.log('📊 Building advanced portfolio analytics...'); // Get all data in parallel for better performance const [balancesRes, transactionsRes, activityRes, nftsRes] = await Promise.all([ axios.get(`${SIM_API_BASE}/evm/balances/${walletAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers }), axios.get(`${SIM_API_BASE}/evm/transactions/${walletAddress}`, { headers, params: { limit: 50, chain: 'peaq' } }), axios.get(`${SIM_API_BASE}/evm/activity/${walletAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers, params: { limit: 50 } }), axios.get(`${SIM_API_BASE}/evm/collectibles/${walletAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers }) ]); const balances = balancesRes.data.balances; const transactions = transactionsRes.data.transactions; const activities = activityRes.data.activity; const nfts = nftsRes.data.entries; // Calculate comprehensive metrics const totalValue = balances.reduce((sum, asset) => sum + parseFloat(asset.value_usd || 0), 0); const peaqAssets = balances.filter((b) => b.chain === 'peaq'); const peaqValue = peaqAssets.reduce((sum, asset) => sum + parseFloat(asset.value_usd || 0), 0); console.log('💼 Advanced Portfolio Analytics:'); console.log(` Total Portfolio Value: $${totalValue.toFixed(2)}`); console.log(` PEAQ Network Value: $${peaqValue.toFixed(2)} (${((peaqValue / totalValue) * 100).toFixed(1)}%)`); console.log(` Total Assets: ${balances.length} tokens`); console.log(` NFT Collections: ${new Set(nfts.map((n) => n.contract_address)).size}`); console.log(` Total NFTs: ${nfts.length}`); return { totalValue, peaqValue, balances, transactions, activities, nfts }; } catch (error) { console.error('❌ Portfolio analytics error:', error.message); } } buildAdvancedPortfolio(process.env.WALLET_ADDRESS); ``` ### Token Analysis Tool: ```JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import axios from 'axios'; const SIM_API_BASE = 'https://api.sim.dune.com/v1'; const headers = { 'X-Sim-Api-Key': process.env.API_KEY, 'Content-Type': 'application/json' }; async function analyzeToken(tokenAddress) { try { console.log(`Analyzing token: ${tokenAddress}`); // Get comprehensive token data const [infoRes, holdersRes] = await Promise.all([ axios.get(`${SIM_API_BASE}/evm/token-info/${tokenAddress}?chain_ids=${process.env.CHAIN_ID}`, { headers }), axios.get(`${SIM_API_BASE}/evm/token-holders/${process.env.CHAIN_ID}/${tokenAddress}`, { headers, params: { limit: 100 } }) ]); const tokenInfo = infoRes.data.tokens[0]; const holders = holdersRes.data.holders; // Calculate holder distribution const totalSupply = parseFloat(tokenInfo.total_supply); const holderAnalysis = { whales: holders.filter((h) => parseFloat(h.balance) / totalSupply > 0.01).length, // >1% large: holders.filter((h) => { const pct = parseFloat(h.balance) / totalSupply; return pct > 0.001 && pct <= 0.01; // 0.1% - 1% }).length, medium: holders.filter((h) => { const pct = parseFloat(h.balance) / totalSupply; return pct > 0.0001 && pct <= 0.001; // 0.01% - 0.1% }).length }; // Top holder concentration const top10Concentration = holders.slice(0, 10).reduce((sum, holder) => sum + parseFloat(holder.balance) / totalSupply, 0) * 100; console.log('📊 Token Analysis Results:'); console.log(` Name: ${tokenInfo.name} (${tokenInfo.symbol})`); console.log(` Price: ${tokenInfo.price_usd}`); console.log(` Total Supply: ${parseFloat(tokenInfo.total_supply).toLocaleString()}`); console.log(` Total Holders: ${holders.length}`); console.log(` Whales (>1%): ${holderAnalysis.whales}`); console.log(` Large Holders (0.1-1%): ${holderAnalysis.large}`); console.log(` Medium Holders (0.01-0.1%): ${holderAnalysis.medium}`); console.log(` Top 10 Concentration: ${top10Concentration.toFixed(2)}%`); return { tokenInfo, holders, holderAnalysis }; } catch (error) { console.error('❌ Token analysis failed:', error.message); } } analyzeToken('0xbba60da06c2c5424f03f7434542280fcad453d10'); // USDC address ``` You can see an exhaustive list of token addresses on [peaq's token list](https://tokenlist.peaq.xyz/). ## Conclusion You now have a complete, working foundation for accessing peaq onchain data using **Sim by Dune**. This guide demonstrated how to: * Connect to the peaq network without running your own indexers * Query balances, transactions, token metadata, and NFT holdings * Track wallet and token activity at scale * Build higher-level analytics such as portfolio view and token distribution insights These building blocks are sufficient for most production use cases, including dashboards, analytics services, DePIN backends, and automated agents. ### Where to go next From here, you can extend this setup in several directions: * **Integrate machine identities** - Combine onchain data with machine DIDs to build identity-aware applications. * **Build persistent analytics** - Store queried data in a database to support historical analysis, alerting, or reporting. * **Add automation** - Trigger workflows based on onchain activity, such as rewards distribution or machine state changes. * **Harden for production** - Implement caching, request batching, rate limiting, and monitoring to support higher throughput. * **Explore the broader peaq stack** - Pair Sim by Dune with peaq-native smart contracts, Machine Accounts, and DePIN primitives to build end-to-end systems. For deeper architectural concepts and advanced integrations, refer to the official peaq developer documentation and the Sim by Dune docs. # Subsquid Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/indexers/subsquid **Indexers** provide an easy solution to query on-chain data. They process smart contract events in the background and store them in a database, which you can query using GraphQL. This enables efficient access to all smart contract events and allows for filtering the data as needed. ## Prerequisites * **Node.js 20.x or later** is installed. * **Docker** is installed, and you have basic knowledge of using it. ## Instructions ### Install Subsquid **Create a Squid:** Follow the instructions for [creating a Squid](https://docs.sqd.dev/squid-cli/installation/). * During setup, create a new Squid with a name of your choice. This will be used later. ### Create the Indexer #### Initialize the JavaScript environment ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm init ``` #### Install required packages ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm i dotenv typeorm @subsquid/evm-processor @subsquid/typeorm-store @subsquid/typeorm-migration @subsquid/graphql-server @subsquid/evm-abi npm i typescript @subsquid/typeorm-codegen @subsquid/evm-typegen --save-dev ``` #### Add `tsconfig.json` Create a `tsconfig.json` file in your project root with the following configuration: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "compilerOptions": { "rootDir": "src", "outDir": "lib", "module": "commonjs", "target": "es2020", "esModuleInterop": true, "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true } } ``` #### Define the schema Create a `schema.graphql` file and define the database schema. For example: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type Transfer @entity { id: ID! from: String! @index to: String! @index value: BigInt! } ``` This schema represents the structure of the events you want to store in your database. #### Generate TypeORM entities Run the following command to generate TypeORM entities based on the schema: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx squid-typeorm-codegen ``` #### Create a `.env` file Define the database credentials in a `.env` file: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} DB_NAME=peaq DB_PORT=23798 ``` #### Create `docker-compose.yaml` Create a `docker-compose.yaml` file to set up a PostgreSQL database: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} version: "3" services: db: image: postgres:15 environment: POSTGRES_DB: "${DB_NAME}" POSTGRES_PASSWORD: postgres ports: - "${DB_PORT}:5432" ``` Start the database container: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} docker compose up -d ``` #### Compile TypeORM classes Compile the generated TypeORM classes: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx tsc ``` #### Generate and apply migrations * Generate the migration file: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx squid-typeorm-migration generate ``` * Apply the migration: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx squid-typeorm-migration apply ``` ### Tie the code together Create a `src/main.ts` file and add the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { EvmBatchProcessor } from '@subsquid/evm-processor'; import { TypeormDatabase } from '@subsquid/typeorm-store'; import { DataStoredEvent } from './model'; const DataStoredEventTopic = '0x9455957c3b77d1d4ed071e2b469dd77e37fc5dfd3b4d44dc8a997cc97c7b3d49'; const CONTRACT_ADDRESS = '0xdbdac2ef52681230f996624a5fa2624b06972671'; const processor = new EvmBatchProcessor() .setRpcEndpoint({ url: 'https://rpcpc1-qa.agung.peaq.network', }) .setFinalityConfirmation(5) .setBlockRange({ from: 3564900 }) .addLog({ address: [CONTRACT_ADDRESS] }) .setFields({ log: { transactionHash: true, }, }); const db = new TypeormDatabase(); processor.run(db, async (ctx) => { const events: DataStoredEvent[] = []; for (let block of ctx.blocks) { for (let log of block.logs) { if (log.topics[0] === DataStoredEventTopic) { const data = BigInt(log.data); console.log("ID: ", log.id); console.log("DATA: ", data); console.log("BLOCK_NUMBER: ", block.header.height); console.log("TX_HASH: ", log.block.hash); events.push(new DataStoredEvent({ id: log.id, data: Number(data), blockNumber: block.header.height, transactionHash: log.block.hash, })); } } } await ctx.store.insert(events); }); ``` ### Compile and start the processor * Compile the code: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx tsc ``` * Start the processor: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} node -r dotenv/config lib/main.js ``` ### Start the GraphQL server In a separate terminal, start the GraphQL server: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx squid-graphql-server ``` # The Graph Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/indexers/the-graph Getting **historical** data on a smart contract can be frustrating when building a dapp. [The Graph](https://thegraph.com/) provides an easy way to query smart contract data through APIs known as **subgraphs**. The Graph's infrastructure relies on a decentralized network of indexers, enabling your dapp to become truly decentralized. Follow this [quick-start](https://thegraph.com/docs/en/quick-start/) guide to create, deploy, and query a subgraph within 5 minutes. ## Why The Graph? The Graph provides a robust solution for indexing and querying blockchain data. It effectively tackles the challenge of reading blockchain data without creating a centralized bottleneck. With its network of indexers, The Graph offers increased redundancy and **quicker query responses**. Using GraphQL queries, your dApp can pinpoint exactly the fields it requires. ## Key Features * **Decentralized Indexing**: Enables indexing blockchain data through multiple indexers, thus eliminating any single point of failure * **GraphQL Queries**: Provides a powerful GraphQL interface for querying indexed data, making data retrieval super simple. * **Customizable & Reusable**: Define your own logic for transforming & storing blockchain data. Reuse subgraphs published by other developers. * **Pay per Use**: No monthly plans. Pay only for [the queries you use](https://thegraph.com/studio-pricing/). # Building with AI Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/integrating-using-ai Use AI coding assistants to build with peaq faster # Building with AI peaq documentation is optimized for AI coding assistants. Whether you're using Claude, Cursor, GitHub Copilot, or other AI tools, you can reference our docs directly for accurate information about the peaq SDK and APIs. ## LLM-Optimized Documentation ### llms.txt Files peaq docs support the [llms.txt standard](https://llmstxt.org/), making our documentation easily accessible to AI tools: | File | URL | Description | | ----------------- | ------------------------------------- | ---------------------------------------- | | **llms.txt** | `https://docs.peaq.xyz/llms.txt` | Navigation structure with page summaries | | **llms-full.txt** | `https://docs.peaq.xyz/llms-full.txt` | Complete documentation in plain text | These files are automatically kept in sync with our documentation. ### Copy a Single Page Click the **Copy page** button at the top of any docs page to copy its contents in a clean, AI-friendly format, then paste it directly into your AI assistant's context window. ## Using Cursor ### MCP Server Install the peaq docs MCP server for queryable access to our documentation directly from Cursor, Claude Desktop, or any MCP-compatible client. **One-click install for Cursor:** ``` npx @mintlify/mcp add docs.peaq.xyz ``` ### Cursor @Docs Add peaq docs as a context source in Cursor: 1. Open Cursor Settings → Features → Docs 2. Add `https://docs.peaq.xyz` as a documentation source 3. Reference it in any Cursor chat by typing `@Docs` and selecting peaq ## Using Claude ### Claude Projects Create a dedicated Claude Project for peaq development: 1. Create a new project in [Claude](https://claude.ai) 2. Add `https://docs.peaq.xyz/llms-full.txt` as a knowledge source 3. Set custom instructions like *"Use the peaq documentation to answer questions about the peaq SDK and APIs"* ### Claude Code Add the following to your `.mcp.json` for context-aware assistance in your terminal: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "mcpServers": { "peaq-docs": { "url": "https://docs.peaq.xyz/mcp" } } } ``` ## Prompting Tips When prompting any AI assistant about peaq: * **Specify the SDK** — Clarify if you're using the JavaScript SDK or interacting directly with the API so the model generates the right patterns * **Mention the function** — Be explicit about whether you're working with peaq ID, peaq access, peaq store, peaq verify, or UMT * **Include chain context** — Mention if you're targeting mainnet or testnet to get accurate endpoints and config values * **Paste the relevant page** — Use the **Copy page** button on specific docs pages and paste into your conversation for the most accurate results # Choosing Validator Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/choosing-validator Delegating your peaq tokens to a validator is a vital step in supporting the **security** and **stability** of the peaq network, whether you're interacting through the EVM side or Substrate side of peaq. By delegating, you **trust** a validator to participate in the network consensus on your behalf, and in return, you may earn a share of the rewards. However, knowing how to choose a validator is crucial to **maximizing** your rewards and **minimizing** the risk of downtime, slashing, or other penalties. This guide provides a high-level strategy for deciding who to delegate to. ## Prerequisites * **Basic Staking Knowledge**: You understand the fundamental concepts of staking and how it supports the blockchain network by securing and validating transactions. * **Wallet Setup**: You have a compatible wallet for the peaq network, with some peaq tokens available to stake. This could be a Substrate-compatible wallet (e.g., Polkadot.js) or an EVM-compatible wallet (e.g., MetaMask) depending on which chain you're interacting with. * **Network Familiarity**: You know how to access relevant blockchain explorers (e.g., [Subscan](https://peaq.subscan.io/validator)) to view validator statistics or use the [peaq app staking dashboard](https://portal.peaq.xyz/portal/staking). * **Risk Tolerance**: You understand that there is risk in choosing a validator, especially if they have poor performance or if they are penalized for misbehavior. ## Instructions ### 1. Research Validators * **Official Sources**: Start by looking for an official validator list from the peaq app or by querying the `CandidatePool` under `Chain State` in Polkadot.js. The peaq app (at this time) only shows validators in the active set, whereas the Polkadot.js `CandidatePool` will return the entire list of potential validators considered for candidacy in the active set of validators. * **Community Reputation**: Visit peaq's community forums or social channels ( [Discord](https://discord.gg/UKTFkPWsyH) ) to get a sense of which validators are well-regarded. Word-of-mouth recommendations can be valuable, especially if these validators have a history of reliable performance. * **Block Explorer Metrics**: Use a block explorer (e.g., [Subscan's Staking page](https://peaq.subscan.io/validator)) to check each validator's uptime, total stake, commission rate, performance, and slashing history. You may also view their performance statistics by viewing peaq's [Telemetry](https://telemetry.polkadot.io/#list/0xd2a5d385932d1f650dae03ef8e2748983779ee342c614f80854d32b8cd8fa48c). ### 2. Evaluate Key Factors * **Commission Rate**: Validators charge a commission rate (a percentage of your rewards that they keep). A lower commission can mean higher rewards for delegators, but extremely low commissions aren't always optimal if they come with higher risk or less reliable infrastructure. * **Uptime and Performance**: Prioritize validators with near 100% uptime and consistent performance. Frequent downtime can reduce rewards and even trigger slashing events. * **Slashing History**: Avoid validators with a history of slashing events or repeated misbehavior, as they put your stake at greater risk. * **Stake Distribution**: Check if the validator already has a large share of the network stake. While popular validators may appear safer, it's good for the network to spread stake across multiple validators to maintain decentralization. * **Validator's Infrastructure**: Look for validators with robust infrastructure (e.g., cloud or dedicated servers, backups, monitoring tools). Although the details might not be publicly visible, many validators share information about their setup in community forums or on their websites. ### 3. Create a Shortlist * **Cross-Reference Data**: After you gather performance metrics and read community feedback, narrow down your list to a few validators that best align with your preferences (low commission, good reputation, reliable performance). * **Balance Your Stake**: Consider delegating across multiple wallets / validators to spread your risk. If one validator has issues, your stake on the other wallet(s) / validator(s) can continue earning rewards. ### 4. Delegate Your Tokens * **Access the Staking UI**: * **Substrate**: Use the native Substrate staking UI (e.g., Polkadot.js) to bond your peaq tokens and select your chosen validator(s). * **EVM**: Use an EVM-compatible wallet (e.g., MetaMask or another Web3 interface) to delegate your tokens through the peaq app. * **Confirm Bonding & Delegation**: Follow the on-screen instructions to specify the amount of peaq tokens you want to delegate. Double-check you're delegating to the correct validator addresses. * **Monitor Your Delegation**: Track your delegation on the block explorer or staking dashboard. Ensure that your chosen validator remains active and continues providing good performance. ### 5. Monitor and Adjust * **Ongoing Monitoring**: Keep an eye on the validator's health, performance, and commission changes. If you notice a decline in performance or a spike in commission rate, consider re-delegating to another validator. * **Stay Informed**: Be active in the community channels for any announcements regarding network upgrades, validator misbehavior, or new validator opportunities. ## Summary Choosing the right validator for your peaq token delegation is both an art and a science. By **researching** validator performance, commission rates, uptime, and history, you significantly **improve** your chances of earning consistent rewards while supporting a healthy, decentralized network. Always remember that validator reliability can shift over time, so it's wise to **monitor** and **adjust** your delegations as needed. Following these steps on both **Substrate-based** and **EVM-based** peaq networks will help you strike a balance between maximizing rewards and minimizing risk, ultimately fostering a more secure and robust ecosystem. # Adjust Delegator Stake Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/peaq-portal/adjust-delegator-stake ## Change your stake amount: ### 1. Find the validator to which you've staked your tokens and click **Manage** peaq-portal-8 ### 2. Then click **Stake more** peaq-portal-9 ### 3. Enter the additional amount of PEAQ you would like to stake in the pop-up window peaq-portal-10 ### 4. This is a success message you'll see once additional tokens are staked peaq-portal-11 # Join Delegator Set Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/peaq-portal/join-delegator-set ## Stake your PEAQ ### 1. Go to the [peaq app](https://portal.peaq.xyz/portal/staking) peaq-portal-1 ### 2. Connect your wallet peaq-portal-2 ### 3. This is the home page you'll see once connected peaq-portal-3 ### 4. Click on **Staking** in the left sidebar peaq-portal-4 ### 5. Scroll down to see the list of available validators and click **Stake** on the validator of your choice peaq-portal-5 ### 6. Enter the number of PEAQ tokens you would like to stake and click **Stake (Minimum 100 peaq)** peaq-portal-6 ### 7. This is a success message you'll see once staked peaq-portal-7 # Leave Delegator Set Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/peaq-portal/leave-delegator-set ## Unstake your PEAQ: 1. Go to the [**Staking**](https://portal.peaq.xyz/portal/staking) tab in the peaq app and find the validator to which you've delegated your tokens 2. Click **Unstake** peaq-portal-12 3. Confirm that you want to unstake your tokens by clicking **Unstake** peaq-portal-13 4. This is how your **Staking** tab will look, while your tokens are being unstaked peaq-portal-14 # Adjust Delegator Stake Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/polkadot-js-org/adjust-delegator-stake ## Adjust Delegator Stake Open the **Developer** tab, and click **Extrinsics**. There, you'll need to submit the extrinsic with the amount you would like to add/subtract from your stake. ### To Increase Your Stake `Developer → Extrinsics → Submission → parachainStaking → delegatorStakeMore(more)` polkadot-js-delegator-2 Keep in mind that peaq has 18 decimals, so if you want to stake 300 tokens you'll need to enter `300000000000000000000`. 1. Select `Id` in the dropdown list in the field `collator: MultiAddress (LookupSource)`. 2. Paste the same `Id` (which you used during the `joinDelegators` step) to the `Id:AccountId` field. 3. Enter the additional staking amount to the `more: u128 (BalanceOf)` field. 4. Click `Submit Transaction`. ### To Decrease Your Stake `Developer → Extrinsics → Submission → parachainStaking → delegatorStakeLess(less)` polkadot-js-delegator-3 Keep in mind that peaq has 18 decimals, so if you want to stake 100 tokens you'll need to enter `100000000000000000000`. 1. Select `Id` in the dropdown list in the field `collator: MultiAddress (LookupSource)`. 2. Paste the same `Id` (which you used during the `joinDelegators` step) to the `Id:AccountId` field. 3. Enter the staking amount to be subtracted in the `less: u128 (BalanceOf)` field. 4. Click `Submit Transaction`. ## Unlock Your Unstaked Tokens `Developer → Extrinsics → Submission → parachainStaking → unlockUnstaked(target)` polkadot-js-delegator-4 * You will be able to unlock your tokens in 14 days after sending `delegatorStakeLess`. * To check when you'll be able to unstake, use: `Developer → Chain state → parachainStaking → unstaking` polkadot-js-delegator-5 1. Select `Id` in the dropdown list in the field `target: MultiAddress (LookupSource)`. 2. Use the address of your delegator account in the `Id:AccountId` field. 3. Click `Submit Transaction`. # Join Delegator Set Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/polkadot-js-org/join-delegator-set ## Join the Delegator Set Open the **Developer** tab, and click **Extrinsics**. There, you'll need to submit the extrinsic with the collator of choice and your stake: `Developer → Extrinsics → Submission → parachainStaking → joinDelegators(collator, amount)` polkadot-js-delegator-1 1. Select `Id` in the dropdown list in the field `collator: MultiAddress (LookupSource)`. 2. Paste the `Id` (which you copied during the previous step) to the `Id:AccountId` field. 3. Enter the staking amount to the `amount: u128 (BalanceOf)` field. * Keep in mind that peaq has 18 decimals, so if you want to stake 250 tokens you'll need to enter `250000000000000000000`. * The min stake amount for the delegator is `100 $PEAQ`. * 1 collator can be backed up by `100` delegators max. 4. Click `Submit Transaction`. # Leave Delegator Set Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-delegator/polkadot-js-org/leave-delegator-set ## Leave Delegator Set Open the **Developer** tab, and click **Extrinsics**. There, you'll need to send 2 extrinsics to leave the set of delegators and unlock your unstaked tokens. ### Request to Leave `Developer → Extrinsics → Submission → parachainStaking → leaveDelegators` It takes 14 days to unlock your stake after submitting the `leaveDelegators` transaction. polkadot-js-delegator-6 Click `Submit Transaction`. ## Unlock Your Unstaked Tokens `Developer → Extrinsics → Submission → parachainStaking → unlockUnstaked(target)` polkadot-js-delegator-7 You will be able to unlock your tokens in 14 days (`201600` blocks) after sending `leaveDelegators`. To check when you'll be able to unstake, use: `Developer → Chain state → parachainStaking → unstaking` polkadot-js-delegator-8 1. Select `Id` in the dropdown list in the field `target: MultiAddress (LookupSource)`. 2. Use the address of your delegator account in the `Id:AccountId` field. 3. Click `Submit Transaction`. # Adjust Commission Rate Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/adjust-commission-rate This document provides guidance on setting up and adjusting the commission rate for collators on the peaq network. It highlights key aspects of the new token economy introduced in runtime upgrade v0.0.104. ## Setting Up Commission Rate To set up your commission rate, follow these steps: 1. **Open polkadot.js** * Navigate to the [polkadot.js](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fpeaq.api.onfinality.io%2Fpublic#/explorer) interface. validator-14 2. **Navigate to Extrinsics** * Go to `Developer → Extrinsics → parachainStaking → setCommission`. validator-15 3. **Submit Transaction** * Enter your desired commission rate and submit the transaction. To set a 1% commission, enter `10000` in the commission field. For 25%, enter `250000`. ## Reward Calculation More information about the reward calculation can be found [here](/peaqchain/build/advanced-operations/node-operations/block-reward-distribution#reward-calculation). # Adjust Validator Stake Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/adjust-validator-stake You can change your validator stake at any moment in time. ## To Increase Your Stake `Developer → Extrinsics → Submission → parachainStaking → candidateStakeMore(more)` validator-7 Keep in mind that peaq has `18` decimals, so if you want to add `1,234` Tokens, you'll need to enter `1234000000000000000000`. 1. Enter the amount you would like to add to your stake. 2. Click `Submit Transaction`. ## To Decrease Your Stake `Developer → Extrinsics → Submission → parachainStaking → candidateStakeLess(less)` validator-8 Keep in mind that peaq has `18` decimals, so if you want to subtract `1,234` Tokens, you'll need to enter `1234000000000000000000`. 1. Enter the amount you would like to subtract from your stake. 2. Click `Submit Transaction`. ### Unlock Your Unstaked Tokens `Developer → Extrinsics → Submission → parachainStaking → unlockUnstaked(target)` You will be able to unlock your tokens in 14 days after sending `candidateStakeLess`. To check when you'll be able to unstake, use: `Developer → Chain state → parachainStaking → unlockUnstaked(target)` validator-9 1. Select `Id` in the dropdown list in the field `target: MultiAddress (LookupSource)`. 2. Use the address of your validator account in the `Id:AccountId` field. 3. Click `Submit Transaction`. # Choosing Node Provider Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/choosing-node-provider Running a peaq validator requires a robust and reliable **Virtual Private Server** (VPS) or dedicated server to ensure optimal performance and uptime. You can also opt for a **managed node provider** like **OnFinality** to reduce operational overhead. This guide will help you choose a suitable provider, with examples from **AWS**, **DigitalOcean**, **Linode** (Akamai), and **OnFinality**. The recommended hardware specifications provided by the peaq engineering team serve as a baseline for reliable results. ## Prerequisites * Basic familiarity with setting up **virtual** servers. * Aim to run a peaq validator node using the **recommended** hardware. * Your goal is to balance **performance**, **cost-efficiency**, and **reliability** when selecting a VPS provider. * You are running **Ubuntu 20.04** as your operating system. ## Recommended Hardware Requirements * **OS**: Ubuntu 20.04 * **CPU**: 3.3 GHz AMD EPYC 7002 * **Storage**: 1TB SSD * **Memory**: 8GB RAM The above specifications have been **tested** and **proven** by peaq's engineering team to produce reliable results over time. While these are recommended, you can experiment with other configurations to **optimize** for cost and performance. ## Managed Node Provider (OnFinality) If you prefer a managed experience, **OnFinality** offers dedicated collator (validator) nodes for peaq. This approach removes most server administration while providing fast sync and production-grade monitoring. ### Why OnFinality * Managed provisioning, monitoring, and scaling for peaq nodes * Dedicated nodes with "Lightning Restore" for fast sync * Built-in secure API endpoints (HTTP/WebSocket) * Clear operational guidance for collator setup ### Quick start with OnFinality 1. Create an account and deploy a Dedicated Node for peaq (Collator Node Type). 2. Use the recommended image version and region; ensure resources meet peaq’s baseline specs. 3. During on-chain configuration, set `--rpc-methods=unsafe` (temporarily) and enable external RPC if needed. After setup, switch back to `--rpc-methods=safe` and disable unsafe external RPC. 4. Generate session keys via `author_rotateKeys` (UI or RPC), then set them on-chain (`session.setKeys`). 5. Stake the minimum required amount and join the validator candidate pool. Refer to the step-by-step guide for details: [How to Set Up a Validator for peaq on OnFinality](https://blog.onfinality.io/how-to-set-up-a-validator-for-peaq-on-onfinality/). ## Instructions for Choosing a VPS Provider ### 1. Evaluate the Recommended Hardware Ensure the chosen VPS provider offers a machine with: * **High-performance CPU** (preferably AMD EPYC 7002 or equivalent) * **Ample SSD storage** (1TB or more) * **Adequate RAM** (8GB or more) ### 2. Compare VPS Providers If you’re managing your own infrastructure, the following VPS providers are commonly used. Otherwise, see the managed option above for OnFinality. #### Amazon Web Services (AWS) * **Instance Recommendation**: `c5ad.xlarge` * **Specs**: * 4 vCPUs (AMD EPYC) * 8GB RAM * Up to 10 Gbps network bandwidth * Elastic Block Storage (EBS) for SSD storage * **Why AWS**: * High availability and reliability * Flexible scaling options * Extensive documentation and support * **Considerations**: * Pricing can be higher compared to other providers. #### DigitalOcean * **Instance Recommendation**: Premium AMD CPU Droplet (e.g., `CPU-Optimized 8GB`) * **Specs**: * 4 vCPUs * 8GB RAM * 160GB SSD local storage (option to scale to 1TB via storage volumes) * **Why DigitalOcean**: * Simplicity in UX for setup and management * Competitive pricing * Integrated monitoring tools * **Considerations**: * Limited to specific regions for Premium CPUs. #### Linode (Akamai) * **Instance Recommendation**: Dedicated CPU Plan (e.g., `Dedicated 8GB`) * **Specs**: * 4 Dedicated vCPUs * 8GB RAM * 160GB SSD (scale up with storage volumes) * **Why Linode**: * Affordable pricing * Strong performance for dedicated resources * User-friendly interface / UX * **Considerations**: * Storage upgrades may require additional configurations. #### OnFinality (Managed Provider) * **Offering**: Dedicated collator (validator) nodes for peaq * **Why OnFinality**: * Managed node lifecycle (deploy, monitor, scale) with rapid sync * Secure RPC endpoints and operational best practices * Reduces complexity vs self-managing VPS * **Considerations**: * Managed service pricing; fewer low-level customization options * Follow security guidance to revert RPC methods to safe after setup * Full guide: [How to Set Up a Validator for peaq on OnFinality](https://blog.onfinality.io/how-to-set-up-a-validator-for-peaq-on-onfinality/) ### 3. Cost and Regional Availability * Compare the monthly **pricing** for the recommended instances from AWS, DigitalOcean, and Linode. * Check for **regional** availability to ensure low latency, especially if you are running a collator in a specific geographic location. ### 4. Additional Factors to Consider * **Network Performance**: Ensure the VPS offers sufficient bandwidth and low latency. * **Support and SLAs**: Check if the provider offers technical support and Service Level Agreements (SLAs) for uptime. * **Scalability**: Choose a provider that allows easy scaling of resources in case your node requires higher specs. * **Backup and Recovery**: Opt for providers that offer automated backups and disaster recovery solutions. ## Summary | Provider | Recommended Plan | Specs | Pricing Notes | | ---------------- | ---------------------- | ------------------------------------------- | ------------------------------------------ | | **AWS** | `c5ad.xlarge` | 4 vCPUs, 8GB RAM, 1TB SSD | Higher cost, high reliability | | **DigitalOcean** | `CPU-Optimized 8GB` | 4 vCPUs, 8GB RAM, scalable SSD | Moderate cost, easy to use | | **Linode** | `Dedicated 8GB` | 4 vCPUs, 8GB RAM, scalable SSD | Affordable, solid performance | | **OnFinality** | Managed Dedicated Node | Managed collator with fast sync, secure RPC | Managed service (usage-based), minimal ops | By following this guide, you can choose a VPS provider that aligns with your technical requirements and budget to reliably run your peaq validator node. While AWS, DigitalOcean, and Linode are excellent options, the final choice depends on your specific needs and preferences. If you want to minimize operational overhead, consider the managed route with OnFinality and follow their peaq validator guide: [How to Set Up a Validator for peaq on OnFinality](https://blog.onfinality.io/how-to-set-up-a-validator-for-peaq-on-onfinality/). # Generate Session Key Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/generate-session-key ## Generate the key 1. Make the request from your **VM** (where your validator node container is running) using the following command: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -X 'POST' \ -H "Content-Type: application/json" \ -d '{"id":1, "jsonrpc":"2.0", "method": "author_rotateKeys", "params":[]}' http://localhost:9944 ``` 2. Copy the **key** from the response: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {"jsonrpc":"2.0","result":"0xf22d82cfcf4402990a0bef3abefd1e58217ff3a86c548d30f1495fd529460d76","id":1} ``` 3. In this case, the **key** is: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "0xf22d82cfcf4402990a0bef3abefd1e58217ff3a86c548d30f1495fd529460d76" ``` ## Associate your session key with your validator account Open the **Developer** tab, and click **Extrinsics**. There, you'll need to submit the following extrinsic: `Developer → Extrinsics → session → setKeys(keys, proof)` You can use any peaq endpoint to submit the `setKeys` extrinsic, including the endpoint from your VM with validator node. validator-3 1. Paste your session key into the `aura: SpConsensusAuraSr25519AppSr25519Public` field 2. Paste 0x00 into the field `proof: Bytes` 3. Click `Submit Transaction` After using setKeys extrinsic, you can connect back to the public endpoint. ### Double-check that your session key is associated with your wallet address Open the **Developer** tab, and click **Chain state**. There, you'll need to submit the following state query: `Developer → Chain state → Session → nextKeys[AccountId32]` validator-4 1. Toggle the switch `include option` and select your validator address in the `AccountId32` field. 2. Click the `+` button. 3. Confirm that your key is displayed. ## Remove unsafe rpc methods from your node Remove `--unsafe-rpc-external` and `--rpc-methods=unsafe` methods, which we needed in the beginning, and rerun the container. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} sudo docker run -d -v peaq-storage:/chain-data -p 9944:9944 peaq/parachain:peaq-v0.0.105 \ --collator \ --parachain-id 3338 \ --chain ./node/src/chain-specs/peaq-raw.json \ --base-path chain-data \ --port 30333 \ --rpc-port 9944 \ --rpc-cors=all \ --execution=wasm \ --out-peers 50 \ --in-peers 50 \ -- \ --execution wasm \ --port 30343 \ --sync fast \ --rpc-port 9977 ``` # Node Operator Introduction Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/introduction ## Discussion peaq’s block production relies on **validators** and **delegators** working together to ensure that transactions are propagated into blocks in a fast, reliable, censorship-resistant manner. The **main role** of a validator is to **create blocks** and keep the **state** of the network up to date. After creation, blocks are provided to validators on the **relay chain** for final approval. The role of validators can be compared to miners in networks that use **Proof of Work**. However, unlike miners, validators don't improve network safety, but they're essential for **maintaining** network activity, ensuring it's decentralized and censorship-resistant. In essence, validators generate proofs of state changes for validators, based on the **Polkadot** and **Kusama** relay chains. They also run a complete node on both the relay chain and parachain in their respective operations. peaq is using [NPoS](https://learn.bybit.com/glossary/definition-nominated-proof-of-stake-npos/) (Nominated Proof of Stake) for block production and relies on the Relay Chain for block **validation** and **finalization**. It's important to mention that due to the **security** design of the Relay Chain, dishonest validators can **never finalize** invalid blocks. Therefore, the maximum damage that dishonest validators can inflict is to **slow down** or **temporarily stop** the network. Provided there is a single honest validator, the parachain remains **secure** and **fully operational**. However, the speed at which blocks are created would be slower than when there's a complete group of honest and working validator nodes. Delegators play another crucial role in the block production process by **filtering** the pool of validator candidates for **honest, reliable validators**, who show steady performance over time. Also, delegator requirements are much **lower** than validator's, thus decreasing the entry barrier and allowing the wider community to participate in the block creation process. Besides becoming a validator or delegator to participate in block production on peaq, users are **encouraged** to run a full node, in order to keep the network **decentralized** and **censorship-resistant**. ## What You Need * On-premises or Virtual Machine in the Cloud matching the requirements ([Hardware Requirements](/peaqchain/build/getting-started/connecting-to-peaq#node-hardware-requirements)) * A full node (synced with parachain and relay chain block history) * **Substrate** (SS58 format) account to stake funds and get rewards * Minimum staking balance to be included in the validator set `50,000 $PEAQ` * Session key validator-1 1. **Ensure** that you have a machine matching hardware requirements and an SS58 Substrate wallet with 50,000+ \$PEAQ tokens. 2. **Set up** a node. 3. **Generate** session key. 4. **Link** session key to validator account. 5. **Stake** tokens and join the validator candidates pool. 6. **Check** whether you were included in the active set. # Join Candidate Pool Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/join-candidate-pool ## Request to Join a Validator Pool Go to the [polkadot.js portal](https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fpeaq.api.onfinality.io%2Fpublic#/explorer), open the **Developer** tab, and click **Extrinsics**. There, you'll need to submit the extrinsic: `Developer → Extrinsic → parachainStaking → joinCandidates(stake)` The min stake amount for the validator is 50,000 \$PEAQ. validator-5 Keep in mind that peaq has `18` decimals, so if you want to stake `50,000` tokens, you'll need to enter `50000000000000000000000`. 1. Enter your stake into the `stake: u128 (BalanceOf)` field. 2. Click `Submit Transaction`. ### Check if You Are in the Active Set of Validators Open the **Developer** tab, and click **Chain state**. There, you will be able to check whether your stake was big enough to get a seat in the active set of validators (top `42` validators by total stake). `Developer → Chain state → Storage → parachainStaking → topCandidates()` validator-6 Confirm that your validator account is in the result. ### Confirm that Your Validator Node is Authoring Blocks Wait for **two sessions** (2400 blocks or \~4hrs) to see whether your node starts authoring blocks. You can **verify** it by going to the peaq block explorer and checking that your address started getting validator rewards. # Leave Validator Pool Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/leave-validator-pool ## Initiate Leave Request `Developer → Extrinsics → Submission → parachainStaking → initLeaveCandidates` validator-10 1. Click `Submit Transaction`. ## Check Whether You Can Exit You need to wait for 5 min after sending `initLeaveCandidates`. `Developer → Chain state → parachainStaking → candidatePool()` validator-11 1. Toggle the switch to `include option` and select your validator address in the `AccountId32` field. 2. Click the `+` button. 3. Confirm that your status is `Leaving`. ## Execute Leave Request `Developer → Extrinsics → Submission → parachainStaking → executeLeaveCandidates` validator-12 1. Select `Id` in the dropdown list in the field `collator: MultiAddress (LookupSource)`. 2. Use the address of your validator account in the `Id:AccountId` field. 3. Click `Submit Transaction`. ## Unlock Your Unstaked Tokens `Developer → Extrinsics → Submission → parachainStaking → unlockUnstaked(target)` * You will be able to unlock your tokens in 14 days after sending `executeLeaveCandidates`. * To check when you'll be able to unstake, use: `Developer → Chain state → parachainStaking → unstaking` validator-13 1. Select `Id` in the dropdown list in the field `target: MultiAddress (LookupSource)`. 2. Use the address of your validator account in the `Id:AccountId` field. 3. Click `Submit Transaction`. # Setup a Node Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/setup-a-node ## Recommended Hardware Requirements * **OS**: Ubuntu 20.04 * **CPU**: 3.3 GHz AMD EPYC 7002 * **Storage**: 1TB SSD * **Memory**: 8GB RAM For example, the `c5ad.xlarge` machine from AWS satisfies these requirements. The above specifications have been **tested** and **proven** by peaq's engineering team to produce reliable results over time. While these are recommended, you can experiment with other configurations to **optimize** for cost and performance. ## Set up a Node 1. Stop your docker container if running an older version (remember to keep the docker volume). 2. Copy the docker image from below. 3. Remove `--unsafe-rpc-external` and `--rpc-methods=unsafe` from your node (if you already generated session keys previously). 4. Run your updated image again. 5. Install Docker on your VM ([how to install Docker on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)). 6. Create and run the container from **the validator node** image using the following command. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} sudo docker run -d -v peaq-storage:/chain-data -p 9944:9944 peaq/parachain:peaq-v0.0.113 \ --collator \ --parachain-id 3338 \ --chain ./node/src/chain-specs/peaq-raw.json \ --base-path chain-data \ --port 30333 \ --rpc-port 9944 \ --rpc-methods=unsafe \ --unsafe-rpc-external \ --runtime-cache-size 64 \ --rpc-cors=all \ --execution=wasm \ --out-peers 50 \ --in-peers 50 \ -- \ --execution wasm \ --port 30343 \ --sync fast \ --rpc-port 9977 ``` 7. Wait until your validator node **syncs** with the blockchain (both Polkadot Relay Chain and peaq parachain block history). It may take a **few hours** and will depend on your connection speed. A fully synced node will look like this: validator-2 # Block Reward Distribution Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/node-operations/block-reward-distribution ## Validator & Delegator Rewards peaq's tokenomics are designed in a way to **incentivize** different network participants. **Validators** play an important role in the network as they not only maintain the state by running a full node, but also propose blocks - which are later validated by the Relay Chain validators. * Validators together with delegators receive `40%` of the total rewards, consisting of new blocks being minted and transaction fees paid by the network users. * The active validator set consists of `42` validators, who are proposing new blocks in a round-robin manner. Any new block is being proposed by a single validator. * The active validator set renews every round (`2400` blocks or approximately every 4 hours). The mechanism of **selecting validators** in the active set is based on the **total stake** of the validator itself, **plus** the stake of all of its delegations. If the active set is full and the new candidate has the exact same stake as the last member of the set (by total stake), the system favors the validator that has been in the pool the **longest.** This way we can ensure that only the validators with the highest total stake are periodically selected to be eligible block authors. Delegators stake tokens on the validator of their **choice**, trying to select honest and well-performing validators. One validator can be supported by `100` delegators. If there is a 101st delegator who would like to delegate to the same validator, then `100` delegators are **selected** based on the **highest stake**, and the delegator with the lowest stake is **removed** from the delegator list. ### New Token Economy (v2) The new token economy introduces significant changes, including the ability for collators to set their commission rates, which can range from `0%` to `100%`. ### Reward Calculation The new token economy introduces a reward system based on total network stake rather than individual validator stakes. The following factors determine block rewards: 1. Validator commission rate 2. Number of blocks produced by the validator 3. Total stake of all collators and their delegators 4. Individual stake amount The reward calculation for **collators** and their **delegators** is detailed below. ``` PotBalance = (Block Reward Per Block * Blocks in Session) + TX Fees TotalStake = SUM(# of authored blocks * (author's stake + SUM(author's delegator's stake))) Delegator's Reward = PotBalance * (# of authored blocks * (1 - author's commission) * (Delegator's Stake / TotalStake)) Collator's Reward = PotBalance * (# of authored blocks * (Author's Stake / TotalStake)) + (PotBalance * (# of authored blocks * Author's Commission) * (total delegator's staking number in this validator / Total staking number)) ``` In other words, the calculations are carried out as follows: ``` Total session reward = Block reward * Collator_Delegator percentage * session length Total staking number = SUM(validator's generated blocks * (validator's stake + SUM(validator's delegators' stakes))) Delegator's reward = Total session reward * validator's generated blocks * (1 - validator's commission) * (delegator's stake / Total staking number) Validator's reward = (Total session reward * validator's generated blocks * validator's stake / Total staking number) + (Total session reward * validator's generated blocks * validator's commission * total delegators' stake / Total staking number) ``` ### Reward Snapshot In our design, no matter the commission rate, the change of the staking number, and the delegator joining or leaving, all of them will affect the next session - not this session. For example: 1. The validator changes the commission rate from 5% to 10% in session `X`. The reward calculation will follow 5% in session X, and the 10% will be affected in session `X+1`. 2. The delegator stakes 100 tokens more on session `Y`. The increment of 100 tokens takes into account the session `Y+1` only. 3. The delegator joins session `Z`; his staking number is involved in the session `Z+1`. ### Reward Distribution To enhance scalability, rewards are now distributed in **batches per session** rather than per block. Rewards are accumulated and calculated based on the session, then distributed in the following session. For instance, blocks created by collators `A`, `B`, and `C` during session `X` will have their rewards distributed in session `X+1`. # IPFS Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/off-chain-storage/ipfs ## What is it? IPFS is a **decentralized** protocol and peer-to-peer network that stores and shares hypermedia using content-addressing. It ensures **global**, **permanent** access to files by assigning unique cryptographic hashes as addresses, employs a distributed network for resilience, tracks file versions, organizes data with a Merkle DAG, supports offline sharing, and utilizes caching for efficient content retrieval. **IPFS** is widely used for decentralized file storage, web hosting, DApps, and ensuring data integrity in various domains, including **blockchain** and **cryptocurrency.** ## Integrating IPFS with a Substrate-based Blockchain Integrating IPFS with a Substrate-based blockchain allows for **decentralized storage** solutions in blockchain applications. This guide outlines the process to **connect** to IPFS, **store** data, **generate** a unique storage key, and **interact** with the blockchain to store and retrieve data CIDs. ### Connect to IPFS We will be using **Helia** to connect to a node. Helia gives developers the ability to run IPFS-compatible functionality without requiring a full IPFS daemon. It is a newer, modular implementation for building IPFS-like networks. A main benefit of Helia is that it can operate in browser environments, enabling IPFS-based applications to run entirely in the browser without requiring an HTTP API connection to a remote IPFS node. First we will need to install the libraries that Helia depends on. You can do so using npm with the following commands: * `npm install helia` - Used to import the Helia package which is used in creating a new instance of a Helia node. * `npm install @helia/unixfs` - Installs the unixfs package from Helia that is used to create a filesystem. Import the two packages previously downloaded, connect to the node, and create a filesystem for that particular node: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { createHelia } from 'helia'; import { unixfs } from '@helia/unixfs'; // create node const helia = await createHelia(); // create filesystem const fs = unixfs(helia); ``` Next we provide a code snippet to show how to add data to the newly created Helia node: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // convert string to Uint8Array const encoder = new TextEncoder(); const bytes = encoder.encode('Hello peaq data'); // adds bytes to node and receives a CID back (content identifier) const cid = await fs.addBytes(bytes); console.log('CID of the data added:', cid.toString()); ``` The code snippet above encodes the string data into bytes and adds it to the filesystem (fs) initialized in the previous example. Take special note of the **CID** that is returned back after adding your data. That value will be needed when reading back from the node. ### Retrieve data from Node The next lines of code provide an example on how to **read** data back from the node using the cid that was returned previously: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // decoder converts Uint8Array to strings const decoder = new TextDecoder() let stored_data = '' for await (const data of fs.cat(cid)) { stored_data += decoder.decode(data, { stream: true }) } console.log('Read file contents from data store:', stored_data); helia.stop(); ``` ### Initialize Blockchain API Connection Next we will need to **install** the libraries that the Substrate-based blockchain depends on. You can do so using npm with the following commands: * `npm install @polkadot/api` - Used to import the **websocket provider** and **api promise** to communicate to the Substrate-based blockchain. Imports Keyring object to sign the write transaction to peaq storage. * `npm install @peaq-network/types` - Used to see the options that peaq offers in the types package. In this example we will use the testnet url, agung, to connect to the Substrate blockchain using Polkadot.js API: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; import { defaultOptions } from '@peaq-network/types'; const provider = new WsProvider(WSS_URL); const api = await ApiPromise.create({ provider, ...defaultOptions }); ``` ### Add CID to peaqStorage Store the previously created **CID** with an item\_type name for the data to be added in peaq Storage: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const item_type = "user-data"; const keyring = new Keyring({ type: 'sr25519' }); // Add Alice to our keyring with a hard-derivation path. // Can add agung funded wallet here with your substrate wallet's mnemonic phrase (recommended). const UserPair = keyring.addFromUri('//Alice'); await api.tx.peaqStorage.addItem(item_type, cid).signAndSend(UserPair); ``` ### Create a Unique Storage Key and Retrieve Data Import the following libraries that the Substrate-based blockchain uses to decode and generate a key for storage. * `npm install @polkadot/util-crypto` - Used to import the **decodeAddress** and **blake2AsHex** packages to successfully generate a storage key used in retrieving data from peaq Storage. * `npm install @polkadot/util` - Used to import polkadot-provided **conversion** mechanisms to read data from peaq Storage. The following code utilizes the user's address and data type to generate a unique key for identifying stored data on the blockchain. Then it fetches the corresponding data from IPFS: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { decodeAddress, blake2AsHex } from '@polkadot/util-crypto'; import { u8aToU8a, u8aToHex, u8aConcat, hexToString } from "@polkadot/util"; // generate storage key const storageKeyByteArray = []; const decodedAddress = decodeAddress(UserPair.address, false, 42); storageKeyByteArray.push(decodedAddress); const hashItemType = u8aToU8a(cid); storageKeyByteArray.push(hashItemType); const key = u8aConcat(...storageKeyByteArray); const storageKey = blake2AsHex(key, 256); const val = await api.query.peaqStorage.itemStore(storageKey); // convert u8a to hex to obtain data from peaq storage var retrieved_cid = hexToString(u8aToHex(val)); const decoder = new TextDecoder() let stored_data = '' for await (const data of fs.cat(retrieved_cid)) { stored_data += decoder.decode(data, { stream: true }) } console.log('Read file contents from data store:', stored_data); ``` ### Putting it all together The code snippet provides an overview of how to use this functionality with everything put together: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { createHelia } from 'helia'; import { unixfs } from '@helia/unixfs'; import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; import { defaultOptions } from '@peaq-network/types'; import { decodeAddress, blake2AsHex } from '@polkadot/util-crypto'; import { u8aToU8a, u8aToHex, u8aConcat, hexToString} from "@polkadot/util"; const provider = new WsProvider(WSS_URL); const api = await ApiPromise.create({ provider, ...defaultOptions }); // create node const helia = await createHelia(); // create filesystem const fs = unixfs(helia); async function createAddHelia() { // convert string to Uint8Array const encoder = new TextEncoder(); const bytes = encoder.encode('Hello peaq data'); // adds bytes to node and receives a CID back (content identifier) const cid = await fs.addBytes(bytes); console.log('CID of the data added:', cid.toString()); helia.stop(); return cid; } async function peaqStorageAdd(item_type, cid){ const keyring = new Keyring({ type: 'sr25519' }); // Add Alice to our keyring with a hard-derivation path. // Can add agung funded wallet here with your substrate wallet's mnemonic phrase (recommended). const UserPair = keyring.addFromUri('//Alice'); await api.tx.peaqStorage.addItem(item_type, cid).signAndSend(UserPair); return UserPair; } async function peaqStorageRetrieve(UserPair, item_type){ // generate storage key const storageKeyByteArray = []; const decodedAddress = decodeAddress(UserPair.address, false, 42); storageKeyByteArray.push(decodedAddress); const hashItemType = u8aToU8a(item_type); storageKeyByteArray.push(hashItemType); const key = u8aConcat(...storageKeyByteArray); const storageKey = blake2AsHex(key, 256); const val = await api.query.peaqStorage.itemStore(storageKey); // convert u8a to hex to obtain data from peaq storage var retrieved_cid = hexToString(u8aToHex(val)); return retrieved_cid } async function readHelia(returned_cid){ // decoder converts Uint8Array to strings const decoder = new TextDecoder() let stored_data = '' for await (const data of fs.cat(returned_cid)) { stored_data += decoder.decode(data, { stream: true }) } console.log('Read file contents from data IPFS store:\n', stored_data); } async function main() { const cid = await createAddHelia(); const item_type = 'user-data-1'; const UserPair = await peaqStorageAdd(item_type, cid); await sleep(25000); // 25 second delay to guarantee it has been added const returned_cid = await peaqStorageRetrieve(UserPair, item_type); // may have to wait until block has been appended before reading await readHelia(returned_cid); // disconnect to terminate the process await api.disconnect(); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } main(); ``` ## Summary This guide provides a **foundational** approach for leveraging **IPFS** with Substrate-based blockchains, emphasizing decentralized storage solutions and data integrity within blockchain applications. For any additional questions please see the [Helia Documentation](https://github.com/ipfs-examples/helia-examples/tree/main/examples/helia-101). # MongoDB Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/off-chain-storage/mongoDB ## What is it? MongoDB is a popular open-source NoSQL (non-relational) **database management system**. It is designed to handle large volumes of unstructured or semi-structured data, making it well-suited for applications with rapidly evolving schemas or complex data models. MongoDB stores data in **flexible,** JSON-like BSON (Binary JSON) documents, and its architecture allows for **horizontal scaling**, enabling efficient handling of growing datasets. It supports **dynamic queries, indexing**, and provides features like **replication** and **sharding** for high availability and scalability. MongoDB is commonly used in modern web development and other scenarios where flexible and scalable data storage is essential. The purpose of integration is that peaq storage does not store big data. It should be handled by your off-chain storage. On-chain storage data should be small so it doesn't delay execution. ## On-Chain Storage **Smart Contracts:** On-chain storage primarily involves the use of smart contracts. Smart contracts are self-executing contracts with the terms of the agreement directly written into code. They reside on the blockchain and can store small amounts of data. **State Variables:** State variables are used to store persistent data. These variables hold information that needs to be preserved across transactions and blocks. **Immutable Ledger:** The blockchain itself serves as an immutable ledger that records the history of all transactions. Each block contains a reference to the previous block, creating a chain of blocks that cannot be altered without changing all subsequent blocks. ## Off-chain Storage: **Databases:** Off-chain storage involves the use of traditional databases or distributed databases to store large amounts of data that do not need to be recorded on the blockchain itself. **Decentralized Storage Networks:** Some blockchain projects leverage decentralized storage networks like IPFS (InterPlanetary File System) or Swarm. These networks store data across multiple nodes, providing a decentralized and distributed storage solution. **Oracles:** Off-chain data can also be brought on-chain through oracles. Oracles are entities or services that provide external information to smart contracts or programs running on-chain, enabling them to interact with off-chain data. ## Architectural Flow: **Data Generation:** Data is generated either on-chain through transactions or off-chain through external sources. **On-chain Storage:** Relevant data is stored on-chain using smart contracts and state variables. This data is typically small and critical for the execution of on-chain logic and external services business logic that require data to be immutable. **Off-chain Storage:** Large or less critical data is stored off-chain in databases or decentralized storage networks. This data may include files, images, or other information not necessary for the consensus mechanism. **Interaction:** Smart contracts may interact with off-chain data using oracles, fetching external information and incorporating it into their logic. Or external services may interact with on-chain data to execute essential business logic. **Immutable Record:** All on-chain data is recorded in blocks on the blockchain, creating an immutable and transparent record of transactions and changes to on-chain state. off-chain-storage-1 ## Integration tutorial For data verification and immutability checks, you can store the **hash** of the data being stored off-chain on our chain (in the DID Document/peaq storage). By doing this, you can fetch the hash from the chain and use it to **verify** the data being fetched from your off-chain storage. You can also **map** the keys of the on-chain key/value pair with the off-chain storage key. Below is an example code of using our peaqStorage with MongoDB off-chain storage: ### 1. Install the required packages: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install mongodb @polkadot/api ``` ### 2. Create a tsconfig.json file in your project directory with the following configuration: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "compilerOptions": { "target": "es2018", "module": "commonjs", "strict": true, "esModuleInterop": true } } ``` ### 3. Create a file named app.ts with the following code: ```jsx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { MongoClient, Db, Collection, ObjectId } from 'mongodb'; class MongoDBStorage { private client: MongoClient; private db!: Db; private collection!: Collection; private collectionName: string; private dbName: string; constructor() { const connectionString = 'mongodb://localhost:27017'; this.client = new MongoClient(connectionString); this.dbName = 'yourDataBaseName'; this.collectionName = 'yourCollectionName'; } async connect() { try { await this.client.connect(); console.log('Connected successfully to MongoDB server'); this.db = this.client.db(this.dbName); this.collection = this.db.collection(this.collectionName); } catch (error) { console.error('Error connecting to MongoDB:', error); throw error; } } async disconnect() { try { await this.client.close(); console.log('Disconnected successfully from MongoDB server'); } catch (error) { console.error('Error disconnecting from MongoDB:', error); throw error; } } async insertDocument(document: any) { try { const result = await this.collection.insertOne(document); console.log('Document inserted:', result); return result; } catch (error) { console.error('Error inserting document:', error); throw error; } } async findDocumentById(id: string) { const result = await this.collection.findOne({ _id: new ObjectId(id) }); console.log('Found document by ID:', result); return result; } async updateDocument(id: string, update: any) { const result = await this.collection.updateOne({ _id: new ObjectId(id) }, { $set: update }); console.log('Updated document:', result.modifiedCount); return result.modifiedCount > 0; } async deleteDocument(id: string) { const result = await this.collection.deleteOne({ _id: new ObjectId(id) }); console.log('Deleted document:', result.deletedCount); return result.deletedCount > 0; } } export {MongoDBStorage} ``` ### 4. MongoDB Setup You either have the option to deploy your MongoDB database **locally** (self-managed), to the **cloud** (using MongoDB Atlas), **serverless** (pay-as-you-go), or you can use the **community edition**. The next step in the process is to download MongoDB to **set** your connection string and configure your database. * Follow the steps to download [MongoDB](https://www.mongodb.com/try/download/community) * Run the cmd `atlas setup` to create and configure an atlas account * After executing the cmd your terminal will output your cluster name, cloud provider location, and display the admin username and password. * Using the code from step 3 above, change the connection string to include your username and password. The [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/connection-string) gives an explanation on how to construct this string. * Run the cmd `tsc` to compile your typescript code. Change the generated `app.js` file to have the new name `app.cjs`. This will be needed for the next step when we import the MongoDB class into the script that interacts with peaq. ### 5. Connect with peaq network: * Create a new file in the same repository and call it `peaq.js` * In package.json add `"type": "module"` * Copy and paste the below code to initialize peaq network connection ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ApiPromise, WsProvider } from '@polkadot/api'; import { MongoDBStorage } from './app.cjs'; import { u8aToHex, stringToU8a, u8aToU8a, u8aConcat } from "@polkadot/util"; import Keyring from "@polkadot/keyring"; import { decodeAddress, blake2AsHex } from '@polkadot/util-crypto'; // using agung url to connect to provider. Change to peaq when necessary const provider = new WsProvider(WSS_URL); const api = await ApiPromise.create({ provider }); // creates a storage key to read data from chain function createStorageKey(address, itemType) { const storageKeyByteArray = []; const decodedAddress = decodeAddress(address, false, 42); storageKeyByteArray.push(decodedAddress); const hashItemType = u8aToU8a(itemType); storageKeyByteArray.push(hashItemType); const key = u8aConcat(...storageKeyByteArray); const storageKey = blake2AsHex(key, 256); return storageKey } // Store Big data in MongoDB, hash data on peaq Network, read to confirm data is the same const main = async () => { // Data to store in MongoDB const data = "Hello World"; // Store data in MongoDB const mongoDBStorage = new MongoDBStorage(); await mongoDBStorage.connect(); const insertedDocument = await mongoDBStorage.insertDocument({text: data}); await mongoDBStorage.disconnect(); // Hash data to store on chain const mongoID = insertedDocument.insertedId.toString(); const storeHashData = u8aToHex(stringToU8a(data)); // Create wallet from seed of a user funded wallet const keyring = new Keyring({ type: 'sr25519' }); const owner = keyring.addFromUri(owner_seed); // Add to MongoDB id and hashed data to peaq storage await api.tx.peaqStorage.addItem(mongoID, storeHashData).signAndSend(owner); await sleep(30000); // wait 30 seconds to guarantee it has been added on chain. // Retrieve data from off-chain storage based on the id await mongoDBStorage.connect(); const foundDocument = await mongoDBStorage.findDocumentById(mongoID); await mongoDBStorage.disconnect(); const readId = foundDocument._id.toString(); // Create a storage key and read hashed on-chain data storage const storageKey = createStorageKey(owner.address, readId); var onChainHashData = await api.query.peaqStorage.itemStore(storageKey); // Confirm data storage hashed value and text in mongoDB are the same onChainHashData = u8aToHex(onChainHashData); const hashData = u8aToHex(stringToU8a(foundDocument.text)); console.log(onChainHashData == hashData); await api.disconnect(); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } main(); ``` The code above creates an instance of the MongoDBStorage() Class that was created and generated in step 3. It stores the 'big data' in MongoDB. The hash of that data is sent to peaq storage as a pair of `MongoDB_Id: Hashed_Data`. The data is then retrieved from the MongoDB based on the ID and the hashed data is read, then compared, to show the immutability. ## Summary In this documentation, MongoDB is presented as the **off-chain storage solution** to complement the peaq network's on-chain storage, which is limited to small, critical data maintained via smart contracts and state variables on an immutable ledger. # P-256 Signature Verification Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/p256-signature-verification Verify hardware-backed P-256 signatures from robots and machines on peaq. P-256, also called `secp256r1`, is widely supported by secure elements, TPMs, passkeys, mobile secure enclaves, and industrial hardware. On peaq, you can use it to verify that a machine action was signed by the private key associated with an enrolled P-256 public key. P-256 verification is available on **peaq mainnet (chain ID 3338)** today. The audited Daimo verifier is deployed at `0xc2b78104907F722DABAc4C69f826a522B2754De4`. P-256 signatures move from machine hardware through application policy to onchain verification and authorization. ## How verification works 1. A machine hashes an action and signs the hash with a P-256 private key held by its secure hardware. 2. The application checks its policy, including the active public key, chain ID, nonce, expiry, and revocation status. 3. A smart contract verifies the signature onchain. 4. The application authorizes the requested payment, command, access, or data operation. P-256 proves possession of the private key. It does not prove that the key came from genuine hardware; use trusted enrollment or hardware attestation when provenance matters. ## Choose an integration path | Path | Availability | Approximate gas | Use when | | ------------------------------ | -------------------- | ---------------------: | --------------------------------------------------------------------------------------------------------- | | OpenZeppelin `P256.verify()` | Available now | 330,000-400,000 | You want a portable integration that can use the future precompile automatically. | | Daimo verifier singleton | Available now | About 330,000 | You want to call the deployed verifier directly or preserve the same address across supported EVM chains. | | RIP-7212 precompile at `0x100` | Not live on peaq yet | 3,450 after activation | Wait until a peaq runtime upgrade activates it. | Gas usage varies with compiler settings and input data. Do not call `0x100` on peaq until the precompile is announced as live. ## Use OpenZeppelin P256 [OpenZeppelin Contracts 5.1 or later](https://docs.openzeppelin.com/contracts/5.x/api/utils/cryptography#P256) provides a portable `P256.verify()` function. It checks for the RIP-7212 precompile and falls back to its Solidity implementation when the precompile is unavailable. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @openzeppelin/contracts ``` ```solidity P256Example.sol theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol"; contract P256Example { function verifyMachineSignature( bytes32 messageHash, bytes32 r, bytes32 s, bytes32 publicKeyX, bytes32 publicKeyY ) external view returns (bool) { return P256.verify(messageHash, r, s, publicKeyX, publicKeyY); } } ``` The same deployed contract will use the lower-cost native path after the RIP-7212 precompile becomes available. ## Call the mainnet verifier directly The Daimo singleton accepts the RIP-7212-shaped 160-byte payload directly. There is no function selector or ABI wrapper. ```solidity PeaqP256Verifier.sol theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; library PeaqP256Verifier { address internal constant P256_VERIFIER = 0xc2b78104907F722DABAc4C69f826a522B2754De4; function verify( bytes32 messageHash, bytes32 r, bytes32 s, bytes32 publicKeyX, bytes32 publicKeyY ) internal view returns (bool) { (bool ok, bytes memory output) = P256_VERIFIER.staticcall( abi.encodePacked(messageHash, r, s, publicKeyX, publicKeyY) ); return ok && output.length == 32 && abi.decode(output, (uint256)) == 1; } } ``` The result check handles both verifier outputs safely: the singleton returns a 32-byte `0` for an invalid signature, while a conforming RIP-7212 precompile returns empty output. ## Build the 160-byte input Concatenate five 32-byte, big-endian fields in this exact order: | Byte range | Field | Description | | ---------- | ------------- | ----------------------------------------- | | `0-31` | `messageHash` | The 32-byte hash that the machine signed. | | `32-63` | `r` | Signature `r`, padded to 32 bytes. | | `64-95` | `s` | Signature `s`, padded to 32 bytes. | | `96-127` | `publicKeyX` | Affine public-key X coordinate. | | `128-159` | `publicKeyY` | Affine public-key Y coordinate. | The verifier does not hash the message for you. For a plain ECDSA-SHA256 flow, pass `sha256(message)`. For WebAuthn, construct the signed hash according to the WebAuthn assertion format. Before submitting the payload: * Decode DER signatures into raw `r` and `s` values. * Remove the `0x04` prefix from an uncompressed SEC1 public key. Decompress compressed keys offchain. * Confirm the stored public key is active for the machine and the requested action. ## Verify through an RPC call Use `eth_call` when verification is needed offchain. It executes the verifier without submitting a transaction or spending gas. ```typescript ethers.ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { JsonRpcProvider, concat } from "ethers"; const provider = new JsonRpcProvider("https://peaq.api.onfinality.io/public"); const verifier = "0xc2b78104907F722DABAc4C69f826a522B2754De4"; const payload = concat([ messageHash, signatureR, signatureS, publicKeyX, publicKeyY, ]); const output = await provider.call({ to: verifier, data: payload }); const valid = output.length === 66 && BigInt(output) === 1n; ``` Each input value in the example must be a 32-byte hex string. ## Production checklist * Bind each public key to the intended machine identity through an authenticated enrollment flow. * Include the application domain, peaq chain ID, action, nonce, and expiry in the signed data. * Store or invalidate nonces to prevent replay attacks. * Define key rotation and revocation before accepting signatures in production. * Do not use signature bytes as a unique identifier; P-256 signatures can be malleable. * Require hardware attestation when you need proof of device provenance, not only proof of key possession. ## References * [OpenZeppelin P256](https://docs.openzeppelin.com/contracts/5.x/api/utils/cryptography#P256) * [Daimo P256 verifier and audit](https://github.com/daimo-eth/p256-verifier) * [RIP-7212 specification](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) # DID Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/did The following provides **simplified**, step-by-step instructions for interacting with the EVM-compatible Decentralized Identifier (DID) **smart contract** on the peaq network. It covers how to execute the primary functions of the DID smart contract. This documentation applies to **both** peaq and agung networks. The precompile is integrated into our JavaScript SDK, serving as the underlying mechanism that triggers the execution of the **corresponding** extrinsic on the substrate side. This seamless integration ensures **interoperability** between the EVM environment and our substrate-based framework. ## Prerequisites * Basic understanding of blockchain, EVM, and DID concepts. * Using the Remix IDE with MetaMask configured to interact with a peaq network. * You have token funds available in the wallet connected to a peaq network. ## Instructions ### 1. Setup Remix IDE * Open the [Remix website](https://remix.ethereum.org/). * Create a new blank workspace for the precompiles. ### 2. Create a New File * In your Remix workspace, create a new file named `DID.sol`. * Copy the content of the DID precompile file from the peaq network node precompile page and paste it into the `DID.sol` file. The precompile is found at the [peaq node source code](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/peaq-did/did.sol). ### 3. Compile the Contract * Compile the `DID.sol` file by clicking on the **Solidity Compiler** tab and selecting the appropriate compiler version. * Ensure the compilation is successful and generates the ABI file needed for interacting with the smart contract. ### 4. Deploy and Interact with the Contract 1. Under the **Deploy and Run Transactions** tab, select **Injected Provider - MetaMask** as the environment. 2. Ensure your chain ID matches the selected network (peaq/agung). 3. Choose the account with sufficient token funds. 4. Set the contract address to `0x0000000000000000000000000000000000000800` (found at the top of the copied file). 5. Click **At Address** to load the deployed contract into the interface. On the bottom left of the Remix interface you will get the following functions you can interact with: did-precompile-1 ### 5. Interact with the Functions #### addAttribute To add a DID Document as a value please generate the value via the Serialized DID section in the [JavaScript SDK](/peaqchain/sdk-reference/javascript/serialized-did) Reference. Otherwise you can set your own custom data to set with. * **Parameters:** * `did_account`: Public hex key (e.g., `0x…`). * `name`: Hex representation of the DID name data. * `value`: Hex representation of the DID value data. * `validity_for`: Set to 0. * **Behavior:** Adds a new attribute to the DID. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm EVM logs in the Remix terminal. * Check for the attribute on the Substrate side via recent events in polkadot.js. #### removeAttribute * **Parameters:** * `did_account`: Public hex key (e.g., `0x…`). * `name`: Hex representation of the DID name data to be removed. * **Behavior:** Removes an existing attribute from the DID. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm EVM logs in the Remix terminal. * Ensure the attribute is removed on the Substrate side. #### updateAttribute * **Parameters:** * `did_account`: Public hex key (e.g., `0x…`). * `name`: Hex representation of the DID name data to be updated. * `value`: Hex representation of the new DID value data. * `validity_for`: Set to 0. * **Behavior:** Updates an existing attribute of the DID. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm EVM logs in the Remix terminal. * Ensure the attribute is updated on the Substrate side. #### readAttribute * **Parameters:** * `did_account`: Public hex key (e.g., `0x…`). * `name`: Hex representation of the DID name data to be read. * **Behavior:** Reads the value of an existing DID attribute. * **Result:** * Confirm the attribute information in the `readAttribute` field. * Data is displayed as a tuple (e.g., `tuple: xxxx`).
For more exhaustive definitions please look at the [DID Operations](/peaqchain/sdk-reference/javascript/did-operations) in the SDK. # ERC-20 Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/erc-20 peaq inherits the [ERC-20 Native Token precompile](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/balances-erc20/ERC20.sol) to interact with the peaq token as if it were a native ERC-20. This prevents the need to have wrapped representations of the same token and allows for easy interaction with the token without the need to deploy another smart contract. This guide will show you how to interact with peaq's testnet token, agung, through this precompile using Remix. The same can be done on peaq mainnet. ## Prerequisites * Basic understanding of blockchain, EVM, and ERC-20 concepts. * Using the Remix IDE with MetaMask configured to interact with a peaq network. * You have token funds available in the wallet connected to a peaq network. ## Instructions ### 1. Setup Remix IDE * Open the [Remix website](https://remix.ethereum.org/). * Create a new blank workspace for the precompiles. ### 2. Create a New File * In your Remix workspace, create a new file named `IERC-20.sol`. * Copy the content of the ERC-20 precompile file from the peaq network node precompile page and paste it into the `IERC-20.sol` file. The precompile is found at the [peaq node source code](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/balances-erc20/ERC20.sol). ### 3. Compile the Contract * Compile the `IERC-20.sol` file by clicking on the **Solidity Compiler** tab and selecting the appropriate compiler version. * Ensure the compilation is successful and generates the ABI file needed for interacting with the smart contract. ### 4. Deploy and Interact with the Contract 1. Under the **Deploy and Run Transactions** tab, select **Injected Provider - MetaMask** as the environment. 2. Ensure your chain ID matches the selected network (peaq/agung). 3. Choose the account with sufficient token funds. 4. Set the contract address to `0x0000000000000000000000000000000000000809` (found at the top of the copied file). 5. Click **At Address** to load the deployed contract into the interface. ### 5. Interact with the Functions erc-20-precompile-1 The contract that you are able to interact with will show up on the bottom of the sidebar. Here lie all the functions that are present in the interface. For detailed information about the functions it provides please see the [EIP-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20). # Introduction to Precompiles Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/introduction ## What is a Precompile? Precompiles are specialized **EVM smart contracts** with **fixed** addresses and **predefined** functionality. Unlike regular contracts, their code is embedded into the blockchain runtime, enabling highly efficient execution of specific tasks. ### Context in Our Ecosystem Our blockchain is built on a **Substrate-based** framework that utilizes pallets—modular components responsible for executing various aspects of the blockchain's logic. While our core functionality runs within these pallets, we also **support** an EVM environment. Precompiles serve as a **bridge** between our native extrinsics (triggered by pallets) and the EVM, allowing smart contracts to access underlying blockchain functions seamlessly. ## How Precompiles Work Each precompile encapsulates a set of operations (such as managing decentralized identities, storage, role-based access control, token standards like ERC-20, and vesting mechanisms) as **EVM-compatible smart contracts**. They are deployed with preset values and built-in functions that **convert** EVM calls into substrate extrinsics. This allows developers to call these functionalities using familiar EVM interfaces, while the heavy lifting is done by the **underlying** substrate pallets. ### Benefits 1. **Efficiency:** Direct integration into the runtime means that precompiles execute faster and more efficiently than equivalent logic implemented entirely in EVM bytecode. 2. **Security & Stability:** Because their functionality is defined at the protocol level, precompiles are rigorously tested and maintained, ensuring consistent performance and security. 3. **Enhanced Interoperability:** They allow for seamless interaction between native substrate features and EVM-based smart contracts, broadening the scope of decentralized applications built on our blockchain. ## Overview of Precompile Modules The subsequent pages in this documentation provide detailed information on some available precompiles: * **DID Precompile:** Manages decentralized identities (called directly in sdk). * **Storage Precompile:** Handles specialized storage operations (called directly in sdk). * **RBAC Precompile:** Implements role-based access control features (called directly in sdk). * **ERC-20 Precompile:** Facilitates standard ERC-20 token operations for native peaq. * **Vesting Precompile:** Manages token vesting schedules and related functionalities. For an exhaustive list of all of our precompiles please view the [peaq network node](https://github.com/peaqnetwork/peaq-network-node/tree/dev/precompiles). ## Summary This introductory page sets the stage for understanding how precompiles integrate deeply with our Substrate-based blockchain. The following sections will dive into each precompile's specifics, offering insights, usage examples, and best practices for harnessing their capabilities in your applications. # RBAC Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/rbac This guide provides a walkthrough for interacting with the peaq **RBAC** (role-based access control) precompile. Applicable to all peaq networks. The RBAC precompile is integrated into our JavaScript SDK, serving as the underlying mechanism that triggers the corresponding substrate extrinsic. ## Prerequisites * Basic understanding of blockchain, EVM, and RBAC concepts. * Using the Remix IDE with MetaMask configured to interact with a peaq network. * You have token funds available in the wallet connected to a peaq network. ## Instructions ### 1. Setup Remix IDE * Open the [Remix website](https://remix.ethereum.org/). * Create a new blank workspace for the precompiles. ### 2. Create a New File * In your Remix workspace, create a new file named `rbac.sol`. * Copy the content of the rbac precompile file from the peaq network node precompile page and paste it into the `rbac.sol` file. The precompile is found at the [peaq node source code](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/peaq-rbac/rbac.sol). ### 3. Compile the Contract * Compile the `rbac.sol` file by clicking on the **Solidity Compiler** tab and selecting the appropriate compiler version. * Ensure the compilation is successful and generates the ABI file needed for interacting with the smart contract. ### 4. Deploy and Interact with the Contract 1. Under the **Deploy and Run Transactions** tab, select **Injected Provider - MetaMask** as the environment. 2. Ensure your chain ID matches the selected network (peaq/agung). 3. Choose the account with sufficient token funds. 4. Set the contract address to `0x0000000000000000000000000000000000000802`. 5. Click **At Address** to load the deployed contract into the interface. On the bottom left of the Remix interface you will get the following functions you can interact with: rbac-precompile-1 ### 5. Interact with the Functions The RBAC smart contract provides extensive role-based access management capabilities. Below are some key functions categorized by their purpose: #### Role Management 1. **addRole** * **Parameters:** * `role_id`: A unique identifier for the role. * `name`: Bytes representing the name of the role. * **Behavior:** Adds a new role to the system. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **RoleAdded** event in the Remix terminal. 2. **updateRole** * **Parameters:** * `role_id`: Identifier of the role to update. * `name`: Updated name in bytes. * **Behavior:** Updates an existing role. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **RoleUpdated** event in the Remix terminal. 3. **disableRole** * **Parameters:** * `role_id`: Identifier of the role to disable. * **Behavior:** Disables an existing role. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **RoleRemoved** event in the Remix terminal. #### Permission Management 1. **addPermission** * **Parameters:** * `permission_id`: A unique identifier for the permission. * `name`: Bytes representing the name of the permission. * **Behavior:** Adds a new permission. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **PermissionAdded** event in the Remix terminal. 2. **assignPermissionToRole** * **Parameters:** * `permission_id`: Identifier of the permission. * `role_id`: Identifier of the role. * **Behavior:** Assigns a permission to a role. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **PermissionAssigned** event in the Remix terminal. #### Group Management 1. **addGroup** * **Parameters:** * `group_id`: A unique identifier for the group. * `name`: Bytes representing the group name. * **Behavior:** Adds a new group to the system. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **GroupAdded** event in the Remix terminal. 2. **assignUserToGroup** * **Parameters:** * `user_id`: Identifier of the user. * `group_id`: Identifier of the group. * **Behavior:** Assigns a user to a group. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **UserAssignedToGroup** event in the Remix terminal. After getting an understanding of these key functions, feel free to interact with the RBAC controls that were not mentioned in this document.
For more exhaustive definitions please look at the [RBAC Operations](/peaqchain/sdk-reference/javascript/rbac-operations/role) in the SDK. # Storage Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/storage This guide provides a walkthrough for interacting with the peaq storage precompile. It includes detailed instructions on setting up the Remix IDE and utilizing the key functions of the **storage smart contract**. This guide is applicable to all peaq networks. Additionally, the storage precompile is **integrated** into our JavaScript SDK, serving as the underlying mechanism that triggers the corresponding substrate extrinsic. ## Prerequisites * Basic understanding of blockchain, EVM, and storage concepts. * Using the Remix IDE with MetaMask configured to interact with a peaq network. * You have token funds available in the wallet connected to a peaq network. ## Instructions ### 1. Setup Remix IDE * Open the [Remix website](https://remix.ethereum.org/). * Create a new blank workspace for the precompiles. ### 2. Create a New File * In your Remix workspace, create a new file named `Storage.sol`. * Copy the content of the storage precompile file from the peaq network node precompile page and paste it into the `Storage.sol` file. The precompile is found at the [peaq node source code](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/peaq-storage/storage.sol). ### 3. Compile the Contract * Compile the `Storage.sol` file by clicking on the **Solidity Compiler** tab and selecting the appropriate compiler version. * Ensure the compilation is successful and generates the ABI file needed for interacting with the smart contract. ### 4. Deploy and Interact with the Contract 1. Under the **Deploy and Run Transactions** tab, select **Injected Provider - MetaMask** as the environment. 2. Ensure your chain ID matches the selected network (peaq/agung). 3. Choose the account with sufficient token funds. 4. Set the contract address to `0x0000000000000000000000000000000000000801`. 5. Click **At Address** to load the deployed contract into the interface. On the bottom left of the Remix interface you will get the following functions you can interact with: storage-precompile-1 ### 5. Interact with the Functions #### addItem * **Parameters:** * `item_type`: Bytes representing the type of item to add. * `item`: Bytes representing the item data to be added. * **Behavior:** Adds a new item to the storage. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **ItemAdded** event appears in the Remix terminal. #### updateItem * **Parameters:** * `item_type`: Bytes representing the type of item to update. * `item`: Bytes representing the new item data. * **Behavior:** Updates an existing item in the storage. * **Result:** * Verify the transaction in MetaMask or Remix. * Confirm the **ItemUpdated** event appears in the Remix terminal. #### getItem * **Parameters:** * `account`: Address of the account whose item you want to retrieve. * `item_type`: Bytes representing the type of item to retrieve. * **Behavior:** Reads an item stored in the contract. * **Result:** * The retrieved data is returned as bytes. * Verify the data in the Remix terminal. #### deleteItem Coming in next runtime upgrade.
For more exhaustive definitions please look at the [Storage Operations](/peaqchain/sdk-reference/javascript/storage-operations) in the SDK. # Vesting Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/precompiles/vesting The vesting interface is designed to manage the vesting of tokens. **Vesting** is a mechanism used to **lock** tokens for a certain period of time, or until certain conditions are met. This allows for the gradual **release** of tokens to beneficiaries. This page will show how to use the precompile to reference the deployed contract so users can directly interact with it. Works with the peaq or agung networks. ## Prerequisites * Basic understanding of blockchain, EVM, and vesting concepts. * Using the Remix IDE with MetaMask configured to interact with a peaq network. * You have token funds available in the wallet connected to a peaq network. ## Instructions ### 1. Setup Remix IDE * Open the [Remix website](https://remix.ethereum.org/). * Create a new blank workspace for the precompiles. ### 2. Create a New File * In your Remix workspace, create a new file named `vesting.sol`. * Copy the content of the vesting precompile file from the peaq network node precompile page and paste it into the `vesting.sol` file. The precompile is found at the [peaq node source code](https://github.com/peaqnetwork/peaq-network-node/blob/dev/precompiles/vesting/vesting.sol). ### 3. Compile the Contract * Compile the `vesting.sol` file by clicking on the **Solidity Compiler** tab and selecting the appropriate compiler version. * Ensure the compilation is successful and generates the ABI file needed for interacting with the smart contract. ### 4. Deploy and Interact with the Contract 1. Under the **Deploy and Run Transactions** tab, select **Injected Provider - MetaMask** as the environment. 2. Ensure your chain ID matches the selected network (peaq/agung). 3. Choose the account with sufficient token funds. 4. Set the contract address to `0x0000000000000000000000000000000000000808` (found at the top of the copied file). 5. Click **At Address** to load the deployed contract into the interface. ### 5. Interact with the Functions vesting-precompile-1 #### vest * Allows the caller to vest their own vested funds. * The caller can trigger the release of their locked tokens according to the vesting schedule. #### vestedTransfer() * This function creates a new vesting schedule for a transfer. * It locks a specified amount of tokens (locked) for a target address (target), specifying the number of tokens to be released per block (perBlock) starting from a specified block (startingBlock). #### vestOther() * Allows someone to vest the funds of another account. * It enables a user to trigger the release of tokens for a specified beneficiary (target). # Introduction to the peaq RWA SDK Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/rwa/introduction The **peaq Real World Asset (RWA) Framework** is a comprehensive blockchain infrastructure that enables the tokenization of physical assets (primarily machines) on the peaq network. It provides a compliant, regulated pathway for transforming real-world assets into tradeable digital securities. Framework Overview At its core, the framework combines three key blockchain standards: * [**ONCHAINID**](https://github.com/onchain-id/solidity) - A decentralized identity protocol that links wallet addresses to verified identities, enabling Know-Your-Customer (KYC) compliance on-chain. * [**T-REX (ERC-3643)**](https://github.com/ERC-3643/ERC-3643) - The Token for Regulated EXchanges standard, which provides a security token framework with built-in compliance controls. * [**ERC-721 NFTs**](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) - Non-fungible tokens that represent unique real-world assets (MachineNFT) and contractual agreements (ContractNFT). These standards were combined into the peaq-rwa-evm repository, where custom business logic is integrated into smart contracts. The framework enables asset owners to: 1. Register physical machines as on-chain NFTs with embedded DID documents 2. Package those assets into vaults 3. Fractionalize ownership into security tokens that can be sold to investors 4. Distribute yield from machine operations to token holders automatically An SDK has been deployed to the peaq npm registry which can be used to interact with the framework in an easy manner. You can check out the [SDK Reference](/peaqchain/sdk-reference/rwa/initialize) for more details. ## Contract Addresses ### peaq ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} proxyAdmin: '0xaF4d82F70B29d58e87C69f0404d171E2e60a5913', infoDesk: '0x246eC0443D3017e319330e2B70F554B997a2Ec50', peaqRwaNft: '0x10a6734bfB7123fb0a43AaE870f116A3Af91539A', peaqVaultFactory: '0xF3e61Fa74FfA62a7de2791361536e4b1bccbFE34', onchainidAuthority: '0xE51F50bA6713CA71Ee987C3C305BE77EC502fdBC', onchainidFactory: '0x2f91c6db691D4b412976aDaB518D83723DC3A069', verifierKyc: '0xF1b050b943065002489796BFDF54E37B19101b3F', verifierMnftIssuer: '0x9C056EdCC77447c576d2b5808871d5305C58Bb03', verifierMnftRegulator: '0x823db6D7621857a6279B26102577D55EEE59A406', trexAuthority: '0x207d63D341fA6834E93606e3e66B9fe8f86A4966', trexFactory: '0x51c17AC87f44F492271B7A243D0f21855C593003', trexGateway: '0x9D671d1e2e621300312213CF24c767753c15ba9c', initialContractNft: '0xdA340e0EB025014107cd87035A6DAadD4c80989C' ``` ### Agung ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} infoDesk: '0x9f2bF4e338cCC48D1b7021494377907ea4a593F2', peaqRwaNft: '0x968b4520261C9c602a25448b75f3d4483D0a083B', peaqVaultFactory: '0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6', onchainidAuthority: '0x84Df77FEa8398bcBCB18CE1165D89aEa02f8b67F', onchainidFactory: '0x6c2Cf1CD533C5c36C2d9918Ca00EAB800AE32d0A', verifierKyc: '0xb1b53EAD9E8aBa8eb061cC50a563FFd392C260d9', verifierMnftIssuer: '0xfbbb480a67A5CB746F9468d00D9b0F4106D04a32', verifierMnftRegulator: '0xEE3c15a286575620852d252F0BE1e1De0308C29f', trexAuthority: '0xA2b6B1d25b8072E6560bFDB0D09b1217368CBa4a', trexFactory: '0x2207DB2a167E31fF2d8E82E953ec0Bda942a8B71', trexGateway: '0x6c24606B129cD4e426c54628E0A0896985aEbca3', initialContractNft: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a' ``` ## Next Steps * **[Learn more about the modules](/peaqchain/build/advanced-operations/rwa/modules)** - Detailed documentation for each module the RWA Framework is composed of. * **[Roles & Responsibilities](./roles/)** - Detailed documentation for each participant role * **[SDK Reference](/peaqchain/sdk-reference/rwa/initialize)** - Complete API documentation with code examples # Core Modules Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/rwa/modules ### OnChainID Module (`sdk.onchainid`) The identity module manages ONCHAINID identities and claims—the foundation of all compliance in the framework. **Purpose:** Create and manage on-chain identities, issue KYC claims, and handle claim lifecycle operations. **Key capabilities:** * **Create identities** - Deploy an `Identity` contract linked to a user's wallet address * **Retrieve identities** - Look up existing identity contracts by wallet address * **Issue KYC claims** - Generate and sign claims that verify a user's identity * **Issue Role claims** - Generate and sign claims that verify a particular role * **Add claims to identities** - Attach signed claims to identity contracts * **Remove claims** - Revoke claims when they are no longer valid Every participant in the framework—whether an investor, machine issuer, or regulator—must have a verified identity with appropriate claims before they can interact with the system. → See [Identity SDK Reference](/peaqchain/sdk-reference/rwa/identity/createIdentity) for implementation details. ### Machine NFT Module (`sdk.mnft`) The Machine NFT module handles the registration and management of tokenized physical assets. Machine NFT Flow **Purpose:** Register real-world machines as NFTs with embedded DID documents, and manage their lifecycle. **Key capabilities:** * **Ensure allowances** - Verify and approve ERC-20 allowances for registration fees * **Register machines** - Mint MachineNFTs with associated DID documents that describe the physical asset * **Read DID documents** - Retrieve the machine metadata stored on-chain Each MachineNFT contains a Decentralized Identifier (DID) document that uniquely identifies the physical machine, including details like manufacturer, model, serial number, and other relevant metadata. This creates an immutable link between the on-chain token and the real-world asset. → See [Machine NFT SDK Reference](/peaqchain/sdk-reference/rwa/mnft/ensureMachineNftAllowance) for implementation details. ### Contract NFT Module (`sdk.cnft`) The Contract NFT module manages digital agreements between multiple parties. **Purpose:** Create, sign, and manage multi-party contracts that are represented as NFTs on-chain. **Key capabilities:** * **Create contracts** - Initialize a new contract with specified counterparties and document hash * **Sign contracts** - Allow counterparties to sign and finalize agreements * **Cancel drafts** - Allow initiators to cancel contracts before all parties have signed * **Verify contracts** - Validate that document content matches the on-chain hash ContractNFTs store a hash of the actual document (which may be stored off-chain, e.g., on IPFS) and a URL to retrieve it. This ensures document integrity while keeping large files off the blockchain. Contracts can also be deposited into vaults alongside MachineNFTs. → See [Contract NFT SDK Reference](/peaqchain/sdk-reference/rwa/cnft/createContract) for implementation details. ### RWA NFT Factory Module (`sdk.rwanft`) The RWA NFT factory module manages the top-level factory contract that coordinates machine issuers and regulators. **Purpose:** Administrative operations for the PeaqRwaNft factory contract. **Key capabilities:** * **Manage machine regulators** - Add or remove addresses authorized to approve machine issuers * **Manage machine issuers** - Register new issuers who can mint MachineNFTs * **Emergency controls** - Block or unblock machine issuers and NFT operations This module is primarily used by framework administrators rather than end users. → See [RWA NFT SDK Reference](/peaqchain/sdk-reference/rwa/rwanft/addMachineIssuer) for implementation details. ### Vault Module (`sdk.vault`) The Vault module enables the fractionalization of assets into security tokens and manages yield distribution. Vault Operations **Purpose:** Create vaults that hold MachineNFTs and ContractNFTs, fractionalize them into security tokens, and distribute yield to token holders. **Key capabilities:** * **Create vaults** - Deploy new vault instances with associated security tokens and reward distributors * **Pause/unpause tokens** - Control token transferability for regulatory or emergency purposes * **Register identities** - Add verified identities to the vault's identity registry (required for token holders) * **Deposit and mint** - Lock NFTs in a vault and mint security tokens representing fractional ownership * **Deposit yield** - Add revenue to the vault for distribution to token holders * **Claim yield** - Allow token holders to withdraw their share of accumulated yield * **Transfer tokens** - Move security tokens between verified holders Security tokens issued by vaults comply with the T-REX (ERC-3643) standard, meaning they have built-in compliance checks that ensure only verified (KYC'd) addresses can hold and transfer them. → See [Vault SDK Reference](/peaqchain/sdk-reference/rwa/vault/approveVaultAsOperator) for implementation details. ## Roles in the Ecosystem The RWA Framework defines several key roles, each with specific responsibilities and required claims: | Role | Description | Required Claim | | --------------------- | ----------------------------------------------------------------------------------- | -------------------------- | | **Framework Owner** | Administers the entire framework, manages trusted claim issuers, and creates vaults | Admin access | | **Claim Issuer** | Issues KYC and role claims to users after verification | Trusted by Framework Owner | | **Machine Regulator** | Approves machine issuers and oversees their operations | `CT_MNFT_REGULATOR` | | **Machine Issuer** | Registers physical machines as MachineNFTs | `CT_MNFT_ISSUER` | | **User / Investor** | Owns MachineNFTs, holds security tokens, and receives yield | `CT_KYC_APPROVED` | # Roles & Responsibilities Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/rwa/roles The peaq RWA Framework defines a structured hierarchy of roles, each with specific responsibilities, requirements, and capabilities. This document provides a comprehensive overview of every role in the ecosystem. ## Quick Reference | Role | Required Claim | Appointed By | Primary Responsibility | | ----------------- | -------------------------- | ----------------- | ------------------------- | | Framework Owner | Admin access | Deployment | Ecosystem administration | | Claim Issuer | Trusted by Framework Owner | Framework Owner | Issue KYC and role claims | | Machine Regulator | `CT_MNFT_REGULATOR` | Framework Owner | Approve machine issuers | | Machine Issuer | `CT_MNFT_ISSUER` | Machine Regulator | Mint MachineNFTs | | Vault Owner | Created via factory | Framework Owner | Manage vault operations | | User / Investor | `CT_KYC_APPROVED` | Claim Issuer | Hold assets and tokens | *** ## Framework Owner The **Framework Owner** is the top-level administrator of the entire RWA ecosystem. This role is typically held by the organization that deployed the framework (e.g., EoTLabs for the peaq network). ### Responsibilities 1. **Manage Trusted Claim Issuers** - Determine which entities are authorized to issue claims (KYC, roles) that the framework will recognize 2. **Appoint Machine Regulators** - Add or remove addresses authorized to approve machine issuers 3. **Create Vaults** - Deploy new vault instances with associated security tokens and reward distributors 4. **Administer the InfoDesk** - Update contract addresses, implementation contracts, and configuration values 5. **Emergency Controls** - Block or unblock machine issuers and pause/unpause security tokens when necessary ### Trust and Claim Issuers Claim issuers are crucial for the KYC process. While anyone can technically deploy a `ClaimIssuer` contract, only claims from issuers trusted by the Framework Owner are recognized by the framework's verifiers. The Framework Owner determines which claim topics each issuer can issue: * `CT_KYC_APPROVED` - For user identity verification * `CT_MNFT_ISSUER` - For machine issuer authorization * `CT_MNFT_REGULATOR` - For machine regulator authorization ### Vault Factory Management The Framework Owner controls the `PeaqVaultFactory`, which orchestrates the deployment of complete vault setups: * **Vault** - Holds MachineNFTs and ContractNFTs * **Security Token** - T-REX compliant token representing fractional ownership * **Reward Distributor** - Handles yield distribution to token holders When creating a vault, the Framework Owner specifies: * The vault taker (who will own the vault) * Token name and symbol * Payout asset for yield distribution * KYC requirements and compliance modules ### InfoDesk Administration The InfoDesk is the central configuration hub for the framework. The SDK does not provide access. The Framework Owner can update: * **Contract addresses** - Locations of core framework contracts * **Implementation addresses** - For upgradeable proxy contracts * **Fee configurations** - Registration fees, transfer fees, and fee accounts * **Precompile addresses** - For peaq-specific functionality *** ## Claim Issuers A **Claim Issuer** is an entity that issues verifiable claims about users. These claims enable the framework to enforce compliance requirements without centralized identity storage. ### What Are Claims? Claims contain structured information that verifiers use to grant access to framework functionality: * **Topic** - The type of claim (e.g., KYC approved, machine issuer role) * **Scheme** - The claim's data format * **Signature** - Cryptographic proof from the issuer * **Data** - The actual claim payload * **URI** - Optional reference to additional information ### Types of Claims The RWA Framework uses two primary claim types: | Claim Type | Topic | Purpose | | ----------- | ------------------------------------- | --------------------------------------- | | KYC Claims | `CT_KYC_APPROVED` | Verify a user's identity for compliance | | Role Claims | `CT_MNFT_ISSUER`, `CT_MNFT_REGULATOR` | Authorize specific framework roles | ### Becoming a Claim Issuer To become a trusted claim issuer: 1. **Deploy a ClaimIssuer contract** - This contract will hold the keys authorized to sign claims 2. **Request trust from the Framework Owner** - The Framework Owner must add your contract to the list of trusted issuers for specific claim topics 3. **Configure signing keys** - Add wallet addresses that can sign claims on behalf of your ClaimIssuer contract ### Issuing Claims The claim issuance process: 1. **Receive identity information** from the user (wallet address, `Identity` contract address) 2. **Verify the information** through your KYC process (off-chain) 3. **Generate the claim** with the appropriate topic and data 4. **Sign the claim** with an authorized signing key 5. **Return the signature** to the user so they can add it to their Identity contract For role claims, the process is similar but verifies organizational authorization rather than personal identity. *** ## Machine Regulators A **Machine Regulator** oversees and authorizes machine issuers within the framework. They act as a quality control layer, ensuring that only legitimate entities can mint MachineNFTs. ### Prerequisites Before becoming a Machine Regulator, you must: 1. **Have an Identity contract** - Deploy an ONCHAINID `Identity` contract linked to your wallet 2. **Be KYC approved** - Have a `CT_KYC_APPROVED` claim added to your Identity 3. **Have the regulator claim** - Obtain a `CT_MNFT_REGULATOR` claim from a trusted Claim Issuer ### Becoming a Machine Regulator The process involves two steps: **Step 1: Obtain the Role Claim** Provide the following to a trusted Claim Issuer: * Your `Identity` contract address * The name or description of your organization The Claim Issuer will generate and sign a claim with topic `CT_MNFT_REGULATOR`. Add this signed claim to your Identity contract. **Step 2: Get Appointed by the Framework Owner** The Framework Owner must add your wallet address to the list of authorized machine regulators in the `PeaqRwaNft` contract. This requires: * Your public wallet address Once added, you can begin authorizing machine issuers. ### Responsibilities Machine Regulators are responsible for: 1. **Vetting Machine Issuers** - Verify that potential issuers are legitimate organizations with proper authority to tokenize machines 2. **Adding Machine Issuers** - Call `addMachineIssuer` on the `PeaqRwaNft` contract, which deploys a new `MachineNft` contract for the issuer 3. **Monitoring Issuers** - Oversee the activities of authorized issuers within their jurisdiction 4. **Reporting Issues** - Alert the Framework Owner if an issuer needs to be blocked ### Adding a Machine Issuer When you authorize a new machine issuer: 1. Verify they have a valid Identity with `CT_MNFT_ISSUER` claim 2. Call `addMachineIssuer` with their wallet address 3. A new `MachineNft` contract is deployed and assigned to them 4. The issuer can now mint MachineNFTs through their contract *** ## Machine Issuers A **Machine Issuer** is authorized to mint MachineNFTs representing real-world physical assets. Each issuer manages their own `MachineNft` contract instance. ### Prerequisites Before becoming a Machine Issuer, you must: 1. **Have an Identity contract** - Deploy an ONCHAINID `Identity` contract linked to your wallet 2. **Be KYC approved** - Have a `CT_KYC_APPROVED` claim added to your Identity 3. **Have the issuer claim** - Obtain a `CT_MNFT_ISSUER` claim from a trusted Claim Issuer ### Becoming a Machine Issuer The process involves two steps: **Step 1: Obtain the Role Claim** Provide the following to a trusted Claim Issuer: * Your `Identity` contract address * The name or description of your organization The Claim Issuer will generate and sign a claim with topic `CT_MNFT_ISSUER`. Add this signed claim to your Identity contract. **Step 2: Get Authorized by a Machine Regulator** A Machine Regulator must add you to the list of authorized issuers: * Provide your public wallet address to the regulator * The regulator calls `addMachineIssuer`, which deploys your `MachineNft` contract * The contract address is emitted in the transaction event ### Your MachineNft Contract Once authorized, you own a dedicated `MachineNft` contract. Key points: * **You are the only issuer** - Only you can mint NFTs from this contract * **Multiple owners** - NFTs you mint can be owned by any KYC'd user * **Unique per issuer** - Each issuer has their own contract instance * **Retrievable** - The contract address can be looked up from `PeaqRwaNft` using your wallet address ### Registering Machines To register a new machine as a MachineNFT: 1. **Collect machine information** from the owner (or yourself) 2. **Create a DID document** describing the machine (type, manufacturer, serial number, etc.) 3. **Ensure fee approval** - The machine owner must approve the registration fee 4. **Call registerMachine** with: * Machine owner's wallet address * Machine value (in PEAQ tokens) * Machine DID document (serialized) 5. **Return the token ID** - The minted NFT is transferred to the owner's wallet *** ## Vault Owners A **Vault Owner** manages a `PeaqVault` that holds MachineNFTs and ContractNFTs, fractionalizing them into security tokens. ### How Vaults Are Created Vaults are created by the Framework Owner through the `PeaqVaultFactory`. When created: * A new `PeaqVault` contract is deployed * An associated T-REX security token is deployed * A reward distributor is deployed for yield management * Ownership is transferred to the specified vault taker ### Vault Owner Responsibilities 1. **Configure accepted NFT collections** - Determine which MachineNft and ContractNft contracts can deposit into the vault 2. **Accept deposits** - Receive NFTs from authorized depositors 3. **Mint security tokens** - Issue fractional ownership tokens based on deposits 4. **Manage yield distribution** - Configure and trigger yield distribution to token holders 5. **Handle compliance** - Ensure only verified identities can hold tokens ### Depositing and Minting The vault supports a single deposit-and-mint operation: * NFTs (MachineNFT and/or ContractNFT) are transferred into the vault * Security tokens are minted to the depositor * This operation can only happen once per vault for security reasons ### Yield Distribution Vaults integrate with a `RewardDistributor` contract: * Anyone can deposit yield (revenue) into the vault * Yield is automatically allocated based on token holdings * Token holders can claim their proportional share at any time * Claims can be made to the holder's wallet or a specified recipient ### Identity Registry Each vault has an associated identity registry. Before an address can hold security tokens: * The address must have a valid Identity contract * The Identity must have the required KYC claim * The address must be registered in the vault's identity registry *** ## Users / Investors A **User** or **Investor** is any participant who wants to: * Own MachineNFTs representing physical assets * Hold security tokens representing fractional vault ownership * Receive yield from vault operations * Trade assets within the framework ### Prerequisites Every user must have: 1. **An Identity contract** - Deployed via ONCHAINID's `IdFactory` 2. **KYC approval** - A `CT_KYC_APPROVED` claim from a trusted Claim Issuer Without these, you cannot: * Own or transfer MachineNFTs * Hold or transfer security tokens * Receive yield distributions * Interact with vaults ### Onboarding Process **Step 1: Deploy Your Identity Contract** Create an Identity contract linked to your wallet: * Provide your wallet address to the identity factory * Use a unique salt value (can only be used once in the framework) * The deployed Identity contract becomes your on-chain identity **Step 2: Get KYC Approved** Obtain a KYC claim from a trusted Claim Issuer: 1. Contact a trusted Claim Issuer in the ecosystem 2. Provide required information: * Your `Identity` contract address * First name, last name * Date of birth (YYYY-MM-DD format) * Place of birth 3. Complete the issuer's verification process 4. Receive the signed claim 5. Add the claim to your Identity contract ### What You Can Do Once KYC approved, you can: | Action | Description | | ---------------------------- | ------------------------------------------------------------- | | **Own MachineNFTs** | Receive machines registered by Machine Issuers | | **Transfer MachineNFTs** | Send machines to other KYC'd addresses (fee applies) | | **Hold Security Tokens** | Own fractional vault shares (**must be registered in vault**) | | **Transfer Security Tokens** | Trade tokens with other verified holders (fee applies) | | **Claim Yield** | Withdraw your share of vault revenues | | **Create ContractNFTs** | Initiate multi-party agreements | | **Sign Contracts** | Participate in ContractNFT agreements | Ready to build? Check out the [SDK Reference](/peaqchain/sdk-reference/rwa/initialize) for more details. # Sending Bulk Transactions Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/sending-bulk-transactions The following will provide a process for **sending bulk transactions programmatically**. The focus is on Ethereum-based blockchains, but the principles can be adapted for other platforms with minor adjustments. Using tools like **ethers.js, smart contracts, or batching servers,** you can streamline the process of handling multiple transactions efficiently. ## Prerequisites * You have a **Node.js** environment set up. * You possess **wallet credentials** (private key or mnemonic for signing transactions). * Access to a peaq RPC endpoint. * Basic familiarity with blockchain SDKs (e.g., ethers.js or web3.js). ## Instructions ### 1. Install Required Dependencies Install ethers.js to handle blockchain interactions programmatically. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers ``` ### 2. Define Bulk Transactions Create and structure your transactions in an array to represent the recipient addresses and amounts to send. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const { ethers } = require("ethers"); require("dotenv").config(); // For environment variables const RPC_URL = process.env.PEAQ_RPC_URL; // Replace with your preferred peaq RPC URL const ETH_PRIVATE = process.env.ETH_PRIVATE; // Setup provider and wallet const provider = new ethers.JsonRpcProvider(RPC_URL); const wallet = new ethers.Wallet(ETH_PRIVATE, provider); const createTransactions = async () => { // Define the bulk transactions const transactions = [ { to: "0xRecipientAddress1", value: ethers.parseEther("0.1"), }, { to: "0xRecipientAddress2", value: ethers.parseEther("0.2"), }, { to: "0xRecipientAddress3", value: ethers.parseEther("0.3"), }, ]; } ``` ### 3. Putting it all together You can use the following JavaScript code to send batch transactions. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const { ethers } = require("ethers"); require("dotenv").config(); // For environment variables const RPC_URL = process.env.PEAQ_RPC_URL; // Replace with your preferred peaq RPC URL const ETH_PRIVATE = process.env.ETH_PRIVATE; const createTransactions = async () => { // Define the bulk transactions const transactions = [ { to: "0xRecipientAddress1", value: ethers.parseEther("0.1"), }, { to: "0xRecipientAddress2", value: ethers.parseEther("0.2"), }, { to: "0xRecipientAddress3", value: ethers.parseEther("0.3"), }, ]; } const sendBulkTransactions = async (txs) => { // connect to provider const provider = new ethers.JsonRpcProvider(RPC_URL); const wallet = new ethers.Wallet(ETH_PRIVATE, provider); // send transactions for (let tx of txs) { try { const transaction = await wallet.sendTransaction(tx); console.log(`Transaction sent: ${transaction.hash}`); await transaction.wait(); // Wait for confirmation console.log(`Transaction confirmed: ${transaction.hash}`); } catch (error) { console.error(`Failed to send transaction: ${error.message}`); } } }; const main = async () => { try { const txs = createTransactions(); sendBulkTransactions(txs); } catch (error) { console.error("Error in event listener:", error); process.exit(1); } }; main(); ``` # Tips 1. **Test with a small batch** before sending large-scale transactions. 2. **Monitor gas prices** and set appropriate limits to avoid failed transactions. 3. **Secure your private keys**—never expose them in code or logs. 4. **Implement error handling** to retry failed transactions where applicable. # x402 on peaq Source: https://docs.peaq.xyz/peaqchain/build/advanced-operations/x402-peaq **x402 enables blockchain-based micropayments over HTTPS, using the HTTP 402 Payment Required status code to gate access to APIs, digital content and machine-to-machine interactions, rather than traditional authentication or subscription systems.** With x402, a client can make an API request and automatically be prompted to authorize payment using any supported **ERC-3009** token on peaq, such as **USDC** or **bridged stgUSDT**. Once the facilitator confirms the payment transaction on-chain (via delegated or self-hosted verification), access is granted instantly, no account creation or manual verification required. In this guide, you'll learn how to implement a full x402 payment flow on peaq, using **Express** and **TypeScript**. You'll build the following components: 1. **Facilitator** - verifies and broadcasts the payment on-chain 2. **Resource Server** - defines which tokens are accepted and which APIs require payment 3. **Client(s)** - initiates payments and accesses paid endpoints, either through an automated backend wallet representing a machine identity, or a web wallet like MetaMask These components map directly to the x402 standard roles of Verifier (Facilitator), Resource Provider (Resource Server), and Client (Consumer). This setup demonstrates both **machine-to-machine** and **human-to-API** interactions. On peaq, machines can sign and authorize x402 transactions using their on-chain identity keys, enabling autonomous payment flows between themselves. # Native Integration In the following sections, we'll walk through a native integration on peaq, demonstrating how the payment negotiation and verification flow can be implemented directly on-chain. ## Facilitator The facilitator is responsible for verifying payment authorization and broadcasting transactions to the peaq blockchain. It acts as the verification layer between the client's signed request and the resource's API logic. There are two modes of operation: * **Delegated Verification** The resource server delegates payment verification to an external facilitator running on peaq. The facilitator validates transaction proofs and returns a signed receipt via HTTPS. This is ideal for smaller resource servers that prefer not to run blockchain nodes. For example, PayAI provides a hosted facilitator supporting peaq at: `https://facilitator.payai.network`. Please check out the [x402 Facilitator List](https://www.x402.org/ecosystem?category=facilitators) for more providers. * **Self-Hosted Verification** The resource server deploys a facilitator service connected directly to a peaq RPC node. This enables local validation of payment proofs and full control over on-chain interactions. While more complex, this reduces reliance on external services and grants full control over signing, verification, and transaction lifecycle management. It's the preferred configuration for enterprise or high-throughput API systems. ## Resource Server The resource server exposes one or more API endpoints protected by x402. When a client requests a protected resource, the server responds with a `402 Payment Required` response. The resource server specifies payment requirements per endpoint, including accepted EIP-3009 tokens using contract address and pricing units. These settings determine when the HTTP 402 response is triggered. Once the payment is confirmed via the facilitator, the resource server grants access and returns the API response. This layer bridges the facilitator verification with client authorization. ## Client Clients can be human users interacting through web wallets like MetaMask or Rabby, or autonomous agents using backend wallets linked to peaq machine identities. These clients sign and broadcast payment transactions as specified in the `Payment-Request` header, then resubmit the original API call with a `Payment-Receipt` proving settlement. We'll demonstrate the full flow with two example clients: * **Backend Autonomous Agent** - a programmatic client (with its own private key/machine identity) signs the authorization automatically, representing a “machine paying a machine” flow. x402-peaq-1 * **Frontend User (MetaMask)** - a human user signs a payment authorization using their wallet to unlock an API feature. x402-peaq-2 ## Project setup The [x402-peaq repository](https://github.com/peaqnetwork/x402-peaq/tree/dev) contains the complete implementation examples for: * **Facilitator** * **Resource Server** * **Clients (frontend + backend)** Each component below includes a code snippet and short explanation to help you understand the core logic without leaving this page. You can clone the repository and follow the instructions in the **README.md** file to get started. ### Facilitator Code Snippet: ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import express from "express"; import { config } from "dotenv"; import { verify, settle } from "x402/facilitator"; import { PaymentRequirementsSchema, PaymentPayloadSchema, createConnectedClient, createSigner, SupportedEVMNetworks, } from "x402/types"; config(); const app = express(); app.use(express.json()); app.post("/verify", async (req, res) => { try { const paymentRequirements = PaymentRequirementsSchema.parse(req.body.paymentRequirements); const paymentPayload = PaymentPayloadSchema.parse(req.body.paymentPayload); const client = createConnectedClient(paymentRequirements.network); const valid = await verify(client, paymentPayload, paymentRequirements); res.json(valid); } catch (err) { res.status(400).json({ error: "Invalid request" }); } }); app.post("/settle", async (req, res) => { try { const signer = await createSigner( req.body.paymentRequirements.network, process.env.FACILITATOR_PRIVATE_KEY! ); const tx = await settle(signer, req.body.paymentPayload, req.body.paymentRequirements); res.json(tx); } catch (err) { res.status(400).json({ error: "Settlement failed" }); } }); app.listen(4020, () => console.log("Facilitator running on http://localhost:4020")); ``` #### Explanation * `verify()` - checks if the client's signed authorization is valid for the given payment terms. * `settle()` - broadcasts the authorized payment on-chain once verified. * `PaymentRequirementsSchema` / `PaymentPayloadSchema` - ensure incoming data follows the x402 spec. * `createSigner()` - connects the facilitator's private key to the correct peaq RPC endpoint. ### Resource Server USDC Code Snippet: ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { config } from "dotenv"; import express from "express"; import { paymentMiddleware, type Resource } from "x402-express"; config(); const facilitatorUrl = process.env.FACILITATOR_URL as Resource; // e.g., "https://facilitator.payai.network" const payTo = process.env.MACHINE_B_ADDRESS as `0x${string}`; // receiver on peaq const port = process.env.SERVER_PORT ?? "4021"; const app = express(); // Protect routes with x402 app.use( // USDC on peaq (human-readable pricing) paymentMiddleware( payTo, { "GET /data": { price: "$0.01", // 1 cent in USDC network: "peaq", }, }, { url: facilitatorUrl } ) ); // Protected API: returns “secret” machine data once paid app.get("/data", (_req, res) => { res.json({ report: { machine: "machine B", data: "secret data" } }); }); app.listen(port, () => { console.log(`Resource Server listening at http://localhost:${port}`); }); ``` USDT Code Snippet: ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { config } from "dotenv"; import express from "express"; import { paymentMiddleware, type Resource } from "x402-express"; config(); const facilitatorUrl = process.env.FACILITATOR_URL as Resource; const payTo = process.env.MACHINE_B_ADDRESS as `0x${string}`; const port = process.env.SERVER_PORT ?? "4021"; const app = express(); // Protect routes with x402 — stgUSDT (atomic units) app.use( paymentMiddleware( payTo, { "GET /data": { price: { amount: "10000", // atomic units (10,000 = 0.01 for 6 decimals) asset: { decimals: 6, address: "0xf4d9235269a96aadafc9adae454a0618ebe37949", // stgUSDT on peaq eip712: { name: "Bridged stgUSDT", version: "1" }, }, }, network: "peaq", }, }, { url: facilitatorUrl } ) ); // Protected API app.get("/data", (_req, res) => { res.json({ report: { machine: "machine B", data: "secret data" } }); }); app.listen(port, () => { console.log(`Resource Server listening at http://localhost:${port}`); }); ``` #### Explanation * `paymentMiddleware(payTo, rules, { url })` - protects routes with x402. On first request returns 402 Payment Required with how-to-pay details; after payment + receipt, it lets the request through. * `payTo` - the receiver address on peaq (e.g., your machine or service wallet) that will receive funds. * `rules map` - keys like "METHOD /path" (e.g., "GET /data") define which endpoints require payment and at what price/network. * `Human-price format` - price: "\$0.01" (USDC on peaq); easy to read and configure for standard ERC-3009 tokens. * `Atomic-price format` - price: `{ amount, asset }` for custom tokens (e.g., stgUSDT). Include `asset.address`, `asset.decimals`, and optional `eip712 { name, version }`. * `network` - which chain to use (here: "peaq"). Must align with the client and facilitator. * `{ url: facilitatorUrl }` - points to your Facilitator (delegated or self-hosted) that verifies/settles the payment. * `app.get("/data", ...)` - your protected handler. It only runs after a valid payment receipt is presented on the retried request. * `Env vars` - FACILITATOR\_URL, MACHINE\_B\_ADDRESS, SERVER\_PORT; keep the client's displayed price in sync with the server rule. ### Clients Code Snippet: ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { peaq } from "viem/chains"; // or the chain you use (peaq/custom) import { wrapFetchWithPayment } from "x402-fetch"; // private key for Machine A that buys data from Machine B using USDC set in the server file const MACHINE_A_PRIVATE = process.env.MACHINE_A_PRIVATE_KEY!; // Create a wallet client const account = privateKeyToAccount(MACHINE_A_PRIVATE as `0x${string}`); const client = createWalletClient({ account, transport: http(), chain: peaq, }); // Wrap the fetch function with payment handling const fetchWithPay = wrapFetchWithPayment(fetch, client, 200000); // can define macValue in 3rd parameter. We set ours as $0.20 USDC // Make a request that may require payment const response = await fetchWithPay(url, { method: "GET", }); const paymentResponseHeader = response.headers.get("X-PAYMENT-RESPONSE"); if (!paymentResponseHeader) throw new Error("No payment response header"); const settlement = JSON.parse(atob(paymentResponseHeader)); const data = await response.json(); ``` #### Explanation * `privateKeyToAccount(MACHINE_A_PRIVATE)` - loads the machine identity (Machine A) as an EOA used to sign x402 authorizations on peaq. * `createWalletClient({ account, transport: http(), chain: peaq })` - creates a viem wallet client bound to peaq for signing the EIP-3009 style payment authorization. * `wrapFetchWithPayment(fetch, client, 200000)` - decorates fetch so that when a 402 Payment Required is returned, it: * reads the x402 payment requirements from the response * signs an authorization with client * sends it to the facilitator for verification/settlement * retries the original request with a Payment-Receipt. The 3rd argument caps the max value you're willing to authorize (here, “\$0.20 USDC” represented in atomic units set by your integration). * `await fetchWithPay(url, { method: "GET" })` - performs the request; if payment is needed, the wrapper handles the pay-then-retry flow automatically. * `response.headers.get("X-PAYMENT-RESPONSE")` - base64-encoded JSON “settlement receipt” returned by your server after successful payment; decode to inspect tx details. * `await response.json()` - the actual protected resource payload (e.g., machine data) you wanted after settlement. ## Conclusion With these three components - **Facilitator**, **Resource Server**, and **Client** - you now have a complete, native x402 payment flow on peaq. This setup enables secure, account-less micropayments for APIs, data streams, and machine interactions. To go deeper, explore the full examples and advanced configuration options in the [x402-peaq repository](https://github.com/peaqnetwork/x402-peaq/tree/dev), and for more facilitator urls check out the [x402 Facilitator List](https://www.x402.org/ecosystem?category=facilitators). # Block Explorers Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/block-explorers Subscan is the block explorer for peaq mainnet and agung; the Machine Explorer at machines.peaq.xyz is the machine-level view of the network. peaq has one block explorer, **Subscan**, and one machine explorer, the **Machine Explorer** at `machines.peaq.xyz`. Subscan indexes blocks, extrinsics, EVM transactions, accounts and contracts on both networks. The Machine Explorer indexes the machines themselves: who operates them, what tier they are on, how much PEAQ is bonded, and the validators securing the chain. peaqscan (`peaqscan.xyz`) and Blockscout (`scout.peaq.xyz`) were retired in August 2026. `peaqscan.xyz` redirects to Subscan; `scout.peaq.xyz` no longer resolves. Update any bookmarks or links you still have. ## Links | Network | Subscan | Machine Explorer | | --------------- | ------------------------------------------------------------- | ---------------------------------------------- | | peaq (mainnet) | [peaq.subscan.io](https://peaq.subscan.io/) | [machines.peaq.xyz](https://machines.peaq.xyz) | | agung (testnet) | [agung-testnet.subscan.io](https://agung-testnet.subscan.io/) | not indexed | ## Subscan Subscan serves pre-indexed data: blocks, extrinsics, EVM transactions, token transfers, accounts and contracts, with search, filtering and history. It covers both the Substrate and the EVM side of peaq, so a DID precompile call and an ERC-20 transfer show up in the same account view. ### Using Subscan 1. **Visit Subscan's peaq Explorer.** * Open [peaq.subscan.io](https://peaq.subscan.io/) for mainnet or [agung-testnet.subscan.io](https://agung-testnet.subscan.io/) for the testnet (agung in these examples). * The dashboard provides a high-level overview of the network, including key metrics like finalized blocks, signed extrinsics, and active accounts. 2. **Search for Accounts or Contracts** * In the search bar enter an **account address** or **contract address** to quickly locate its details. **Example of an account search:** The account above was searched by inputting `0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C` in the search bar. **Subscan** returned the account that is affiliated with this address. block-explorers-6 #### Account Details * **Account**: Includes the *address* of the account and the *nonce* which represents the number of transactions sent from this account. * **Balance:** Displays the amount of the native token an account has. Shows how much is transferable and how much is reserved (during pallet usage). * **EVM Transactions**: * Displays a list of transactions submitted by this account to the blockchain. * Each EVM Transaction contains the following: * **Tx Hash:** `0x0664....23c3c6` - The unique transaction hash used to identify this tx. * **From:** `0x9Eea....46641C` - Account used to send the tx. * **To:** `0x0000....000801` - Where the transaction was sent to (RBAC precompile in this case). * **Method:** `0x257c3c03` - Function method executed on the Smart Contract (human-readable when contract is verified). * **Time:** `2 days 19 hrs ago` - Indicates when the tx was added to the blockchain. * **Txn Fee:** `0.0000000000477PEAQ` - Cost to send the tx. * **Value:** `0.0000000000477PEAQ` - Amount of native token that was transferred during the operation. * **Result:** ✅ - Indicates that the extrinsic was successfully executed. * **ERC-20 Transfers**: Shows the fund transfers initiated by this account. * **EVM-NFT Transfers History:** Shows the NFT transfers initiated by this account.
**Example of a contract search:** The contract was found by submitting the deployed contract address of `0x17bD3d6639b28Ee774040D3aE2137F49390a584c` in the **Subscan** search bar. It took me to the page: block-explorers-7 #### Contract Details * **Account**: * **Unknown:** Indicates the account hasn't been assigned a custom or recognizable name by the user. * **Account Address:** Unique identifier of the EVM contract. * **Type:** EVM-based account, meaning it's a smart contract account created and executed within the EVM environment. * **Nonce**: Indicates that this contract account has performed `30` transactions so far. * **Contract Creator:** The EVM address of the account that deployed this contract. * **Created At:** Refers to the transaction hash where this contract was initially deployed. * **Balance:** * **Total Balance:** `9.57 AGUNG` – The contract currently has native AGUNG tokens. * **Transaction Details:** * **EVM Transactions (685):** Tracks interactions with this EVM contract. * **Txn Hash:** `0xcde5....de3647` - The unique identifier for the transaction involving this contract. Clicking this link shows more details about the transaction. * **From:** `0x9Eea...46641C` - The address of the sender who interacted with the contract (contract creator in this case). * **To:** `0x17bD....0a584c` - The address of this contract, which received the transaction. * **Method:** `executeTransaction()` - The method or function called within the contract. * **Time:** `6 days ago` - Indicates when this transaction was processed. * **Txn Fee:** `0.0000000000984PEAQ` - The transaction fee paid for executing this transaction on the network. * **Value:** `0 PEAQ` - No value (native tokens) was transferred with this transaction. * **Result:** ✅ - Indicates that the transaction was successfully executed. * **ERC-20 Transfers (22):** Indicates no transfers of ERC-20 tokens (standard fungible tokens) related to this contract. * **ERC-721 Transfers (0):** Indicates no transfers of ERC-721 tokens (standard NFTs) related to this contract. * **Contract:** Includes details on how to verify a contract. #### Transactions and blocks * Paste a transaction hash (`0x…`, 66 characters) or an extrinsic index (`-`) into the search bar to open it. The transaction page shows the sender, the target, the decoded method when the contract is verified, the fee, and the events the call emitted. * Paste a block number or block hash to open the block with its extrinsics and events. #### Verified contracts * A verified contract shows decoded method names and parameters on every transaction. See [Verify Smart Contract](/peaqchain/build/basic-operations/smart-contracts/verify-smart-contract). ## Machine Explorer The [Machine Explorer](https://machines.peaq.xyz) is not a block explorer: it indexes peaq mainnet for machines instead of transactions. * **Machines**: every machine registered on peaq, with operator, machine type and credit rating. Search by DID or address. * **Machine page**: `machines.peaq.xyz/machine/0x
` for a Tokenomics 1.0 or mirrored legacy machine, `machines.peaq.xyz/machine/` for a machine activated under [Economics 2.0](/peaqos/concepts/economics-2-0). Shows identity, activation and bond status, and the on-chain event history, with links to the Subscan events. * **Validators**: the chain validators with commission, delegator count, total stake and APR, plus the trust validators of Economics 2.0. * **Economics**: activated machines, PEAQ bonded, share of circulating supply, and the tier prices converted at the current oracle rate. Data comes from an indexer that reads finalized blocks, so allow a few minutes for a fresh activation to appear. Use it to check that a machine you onboarded is live; use Subscan for the transaction itself. ## Summary * **Subscan**: transactions, blocks, extrinsics, accounts and contracts on peaq mainnet and agung. Contract verification for readable method calls. * **Machine Explorer**: machines, operators, validators and Economics 2.0 figures on peaq mainnet. Not for transactions or blocks. # Deploying ERC-20 Token Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/deploying-erc-20-token In this guide, you will learn how to deploy an **ERC-20** token smart contract on AGUNG using **Remix**, a browser-based Solidity development environment. This guide uses a boilerplate ERC-20 contract, and you will customize key parameters such as: * `Token Name` * `Symbol` * `Decimals` * `Total Supply` By the end, you'll have your very own **ERC-20** token deployed on our AGUNG testnet. ## Prerequisites * **Basic understanding** of blockchain and ERC-20 tokens. * **Installed MetaMask** browser extension with AGUNG network set-up already. * **Familiarity** with Remix IDE ([https://remix.ethereum.org/](https://remix.ethereum.org/)). * Using the **OpenZeppelin library** for the ERC-20 boilerplate. * A **wallet** with AGUNG network PEAQ for gas fees. ## Instructions ### 1. Open Remix IDE * Visit Remix and create a new workspace. * Click on **"File Explorers"** and select **"Create a New File"**. * Name the file: `MyERC20Token.sol`. erc-20-1 ### 2. ERC-20 Code Copy and paste the following boilerplate ERC-20 smart contract code into your newly created file: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; // Import OpenZeppelin's ERC-20 implementation import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { // Here we are declaring the contract & inheriting ERC20 constructor(uint256 initialSupply) ERC20("MyToken", "MTK") { _mint(msg.sender, initialSupply * (10 ** uint256(decimals()))); } } ``` ### 3. Modify Token Details Update the following placeholders in the code: * **Token Name**: Replace `"MyToken"` with your token's name (e.g., `"ExampleToken"`). * **Symbol**: Replace `"MTK"` with a short symbol (e.g., `"EXT"`). * **Total Supply**: The `initialSupply` (input during deployment) defines the number of tokens created. **Example**: For a 1,000,000 total supply with 18 decimals, input **1,000,000** during contract deployment (Step 5). erc-20-2 ### 4. Compile the Contract In the **Solidity Compile** tab: * Select the Solidity version (e.g., `0.8.20+commit.a1b79de6`). * Click **Compile MyERC20Token.sol**. * Ensure there are no errors. erc-20-3 ### 5. Deploy the Contract * Go to the **Deploy & Run Transactions** tab in Remix. * Select **Injected Web3** as the environment to connect MetaMask. * Choose your **testnet wallet account** in MetaMask. * In the constructor field, enter your desired **Total Supply** (e.g., `1000000` for 1 Million). * Click **Transact** under **Deploy** and confirm the transaction in MetaMask. erc-20-4 ### 6. Verify Deployment erc-20-5 Remix IDE's console logging a successful execution of the transaction to deploy the smart contract ExampleToken (MyERC20Token.sol) to AGUNG network. * After deployment, your contract will appear under **Deployed Contracts** in Remix. * Use the **Read Functions (blue)** to verify details like `name()`, `symbol()`, and `totalSupply()`. * You can also **check your token balance** using the `balanceOf()` function by inputting your wallet address. erc-20-6 ## Summary 🎉 Congratulations! You have successfully deployed your first **ERC-20** token on our AGUNG testnet. You can now view your contract on a block explorer (like Subscan) and interact with it through the interface in Remix. For further enhancements to your ERC-20 token, explore features like **minting**, **burning**, or **transferring tokens**. # Deploying ERC-721 NFT Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/deploying-erc-721-nft Non-Fungible Tokens (NFTs) are **unique** digital assets that represent ownership of a specific item or piece of content on the blockchain. The **ERC-721** standard defines a common interface for **NFTs**, ensuring interoperability across platforms and marketplaces. Within the **peaq ecosystem**, NFTs can be more than digital collectibles. For example, organizations, innovators, and entrepreneurs building DePINs and dApps on the peaq network could use ERC-721s to: * Represent and manage the unique identity of connected devices in a Machine Economy (e.g., electric scooters, autonomous drones, or charging stations). * Facilitate digital identity (DID) whitelist solutions where each person or entity (like a vehicle or machine) is represented as a unique NFT, easing onboarding, access controls, and lifecycle management. * Tokenize real-world machine-based activities or digital certificates that attest to certain functionalities, ensuring authenticity and traceability within decentralized marketplaces. ## Prerequisites * **Basic Blockchain Knowledge:** You understand what the Ethereum Virtual Machine (EVM) and smart contracts are and have already deployed an ERC-20 token using Remix IDE before (see tutorial - [Deploying ERC-20 Token](/peaqchain/build/basic-operations/deploying-erc-20-token)). * **Access to Remix:** You have opened Remix in your browser and are familiar with its basic functionality. * **MetaMask & Test Network:** You have MetaMask or another Web3 wallet installed and connected to a test network (like AGUNG) or a local blockchain (e.g., Hardhat or Ganache) for testing. * **Standard ERC-721 Boilerplate:** You have a standard ERC-721 contract template (e.g., from OpenZeppelin’s library) ready to customize. ## Instructions ### 1. Obtain an ERC-721 Boilerplate * Navigate to the [OpenZeppelin ERC-721 GitHub repository](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol). * Copy or import this simple ERC-721 implementation (or a minimal example from the OpenZeppelin docs like `ERC721PresetMinterPauserAutoId.sol`). ### 2. Create a New File in Remix * In Remix, click on the **File explorer** icon (the first icon on the left) and create a new file named, for example, `MyFirstNFT.sol`. * Paste the boilerplate ERC-721 code into the file. ### 3. Fill in Relevant Details Within the constructor and contract parameters, you can set: * **Contract Name & Symbol:** Choose a name that represents your NFT collection (e.g., `MyFirstNFT`) and a symbol (e.g., `MFN`). * **Base URI:** If your contract includes a base URI, set it to a URL pointing to your NFT metadata storage location (e.g., `https://my-nft-metadata-api.com/metadata/`). IPFS storage systems like [Pinata](https://pinata.cloud/) can help store this metadata off-chain to be upgraded later if needed.

**Example Constructor Initialization:** ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} constructor() ERC721("MyFirstNFT", "MFN") { // Optional: _setBaseURI("https://my-nft-metadata-api.com/metadata/"); } ``` ### 4. Compile the Contract * Click on the **Solidity Compiler** icon (usually the second icon on the left). * Select the Solidity version that matches the pragma in your code. * Click the **Compile MyFirstNFT.sol** button. Ensure there are no compilation errors. ### 5. Deploy the Contract * Click on the **Deploy & Run Transactions** icon (usually the third icon on the left). * In the **Environment** dropdown, select your desired network (e.g., `Injected Web3` to use Metamask's currently selected network). * Click on the **Deploy** button. * Confirm the transaction in Metamask. Once the transaction is mined, your contract address will appear in Remix's deployed contracts section. ### 6. Minting and Interacting * Use the contract’s `mint` function (if included) or any custom function to create a new NFT. * After minting, you can view the token details (e.g., `ownerOf(tokenId)`, `tokenURI(tokenId)`) directly in Remix or using block explorers. ### 7. Testing & Next Steps * Verify your contract on a test network like AGUNG and ensure your NFT appears on NFT-supporting platforms or on block explorers like [Subscan (AGUNG)](https://agung-testnet.subscan.io/) / [Subscan (PEAQ)](https://peaq.subscan.io/). * Once confident, you can migrate to mainnet or integrate with the peaq ecosystem's unique functionalities—such as representing machine IDs or tokenizing machine-based services—and leverage peaq’s infrastructure for future improvements, liquidity, and interoperability. ## Summary By following these steps, you've successfully **deployed** an ERC-721 NFT contract. Over time, you can refine the contract, incorporate advanced features (like access control, royalties, or custom metadata handling, unique mint/burn functions, etc.), and build rich applications that harness the potential of **NFTs** in the peaq ecosystem's machine-centric marketplace. # Estimating Gas Fees Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/gas-operations/estimate-gas-fees On peaq and other EVM-compatible networks, transaction execution costs are measured in **gas**. Before sending a transaction on the network, it's often helpful to **estimate** the gas required so that you don't overpay, or risk your transaction running out of gas. Most EVM networks provide a built-in RPC method, `eth_estimateGas`, which allows you to programmatically estimate the gas usage for a given transaction. This estimation is done by **simulating** the transaction on a node, without actually broadcasting it. ## Prerequisites 1. **You have access to one of our EVM-Compatible RPC Endpoints:** * [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq) 2. **You know the Transaction Data you’re sending:** The parameters of the transaction you want to send are well-defined, including `from`, `to`, `data` (if interacting with a contract), and optionally `value` if sending PEAQ or the respective native token. You don't need the exact gas amount upfront; that's what you're estimating. 3. **JSON-RPC Client:** You can use a JSON-RPC client (e.g., `curl`, `Postman`, `web3.js`, `ethers.js`, or any library that supports JSON-RPC) to make requests to the RPC endpoint. In our example below we'll be sending a JSON payload using a `curl` request to an RPC Endpoint. ## Instructions ### 1. Prepare the JSON-RPC Request Use the `eth_estimateGas` method to request a gas estimate. The parameters should describe the transaction you intend to send. For example: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "jsonrpc": "2.0", "method": "eth_estimateGas", "params": [{ "from": "0xYourFromAddress", "to": "0xRecipientOrContractAddress", "value": "0x0", // hex for 0 "data": "0xYourEncodedData" // if calling a contract function }], "id": 1 } ``` Addresses and values must be in hex format. If you don't need to send value (PEAQ), you can omit it or set it to `"0x0"`. ### 2. Send the Request to the RPC Endpoint Using `curl` from your terminal, for example: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -X POST \ -H "Content-Type: application/json" \ --data '{ "jsonrpc":"2.0", "method":"eth_estimateGas", "params":[{"from":"0xYourFromAddress","to":"0xRecipientOrContractAddress","data":"0xYourEncodedData"}], "id":1 }' \ https://quicknode.peaq.xyz //Your chosen RPC provider's URL ``` ### 3. Read the Response The node will return a JSON response. A successful response might look like: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "jsonrpc":"2.0", "id":1, "result":"0x5208" } ``` The `"result"` field is a hexadecimal string representing the estimated gas limit. For example, `0x5208` (in hex) is `21000` in decimal—typical for a simple ETH transfer. ### 4. Convert and Use the Returned Value * **Conversion:** If the returned gas estimate is in hex, you can convert it to a decimal integer. In many programming languages, you can parse the hex string and turn it into an integer type. For example, in JavaScript: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const hexGasEstimate = "0x5208"; const gasEstimate = parseInt(hexGasEstimate, 16); // returns 21000 ``` * **Adjustment for Safety:** While `eth_estimateGas` gives a good baseline, you may want to add a buffer (e.g., add 10-20% more) to ensure your transaction doesn't fail if conditions change by the time it's mined. ### 5. Use the Estimated Gas in Your Transaction When constructing the final transaction, set the `gas` field to the estimated amount (plus any safety buffer). Consider the current gas price or fee structures (`base fee + priority fee`) when calculating the total transaction cost. ## Summary By making an `eth_estimateGas` call over a JSON-RPC endpoint and parsing the response, you can **accurately estimate** the **gas limit** needed for your transaction on an **EVM network**. This helps ensure you neither **waste gas** nor have insufficient gas for a successful execution. # Managing Gas Limits Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/gas-operations/managing-gas-limits When interacting with the peaq network, managing gas limits allows you to maintain **predictable** transaction costs, and safeguard against unexpected gas spikes. By explicitly setting a **gas limit**, you prevent overspending, if the network suddenly requires more gas than anticipated. Libraries like `ethers.js`, `web3.js`, etc. make it straightforward to configure this behavior. For the purpose of demonstration we'll be using `ethers.js`. ## Prerequisites * You understand basic concepts of EVM-like networks: transactions, gas, and gas pricing. * You have Node.js and `ethers.js` installed. * You have access to a **funded account** and the peaq RPC endpoint. * You know how to manage sensitive information using environment variables (e.g., `.env` files). ## Instructions ### 1. Setting Up the Environment * **Install Dependencies:** Ensure you install the necessary packages: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers dotenv ``` * **Set ESM Module:** Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` * **Configure Environment Variables:** Create a `.env` file with the following content: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PRIVATE_KEY=0xYOUR_PRIVATE_KEY PEAQ_RPC_URL=https://quicknode.peaq.xyz ``` Make sure your `.env` file is listed in `.gitignore` so it's not checked into version control. ### 2. Setting up the Provider and Wallet Create a `sendTransaction.js` file (for example) and load the environment variables using `dotenv`. Initialize the provider with the peaq RPC endpoint and load the wallet from the environment variable. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} //sendTransaction.js import dotenv from 'dotenv'; dotenv.config(); import { ethers } from 'ethers'; // Use environment variables from .env const provider = new ethers.providers.JsonRpcProvider(process.env.PEAQ_RPC_URL); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); ``` ### 3. Deciding on a Gas Limit Choose a suitable gas limit. Start with a safe upper bound, and adjust later as needed. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const gasLimit = 200000; // Example: Adjust as needed based on your transaction's complexity ``` ### 4. Crafting the Transaction Include the `gasLimit` in the transaction object. This ensures the transaction will revert if it exceeds the specified gas, protecting you from unexpected costs. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const tx = { to: '0xRECEIVER_ADDRESS', // Replace with the address you want to send to value: ethers.utils.parseEther('0.01'), // Example amount to send gasLimit: gasLimit }; ``` ### 5. Sending the Transaction Send the transaction and wait for it to be mined. If the transaction fails due to exceeding gas, you’ll catch the error in the `try/catch` block. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function sendTransaction() { try { const response = await wallet.sendTransaction(tx); console.log('Transaction hash:', response.hash); const receipt = await response.wait(); console.log('Transaction confirmed in block:', receipt.blockNumber); } catch (error) { // Failure could be due to insufficient gasLimit console.error('Transaction failed:', error); } } sendTransaction(); ``` ### 6. Adjusting Gas Limits Over Time * If your transaction often runs out of gas, consider **raising** the limit. * If you're consistently using less gas than your limit, you might **lower** it to be more cost-effective. * For automated scripts or systems that regularly send transactions, predefining a gas limit helps protect against sudden gas spikes. * Consider a dynamic approach like [Estimating Gas Fees](/peaqchain/build/basic-operations/gas-operations/estimate-gas-fees) in your calculation of a gas limit to prevent failed transactions. ## Summary By incorporating environment variables, you keep sensitive data secure and easily manage configurations for different environments. Using the `ethers.js` library, explicitly setting a **gas limit** gives you fine-grained control over your transaction costs on the peaq network. With a carefully chosen gas limit, you can execute transactions confidently, knowing you're safeguarded against unexpected fee changes. # Optimizing Gas Usage Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/gas-operations/optimizing-gas-usage On EVM networks, **gas fees** represent the **computational cost** of executing transactions and smart contract operations. Lowering these fees can help developers and users alike. Common strategies include **reducing unnecessary computations**, **optimizing data storage**, **leveraging efficient data structures**, **batching transactions**, and **offloading computation off-chain** when possible. Ultimately, the goal is to use fewer computational resources per transaction, thus lowering the gas cost and improving overall **efficiency**. ## Prerequisites 1. **EVM Compatibility:** Your project is deployed on peaq network, which has EVM compatibility, allowing you to apply common Ethereum-based **optimization** techniques. 2. **Basic Knowledge of Gas Mechanics:** You understand that gas fees increase with transaction complexity, storage operations, and on-chain computations. 3. **Developer-Level Access:** You have the ability to modify and deploy smart contracts, review their code, and adjust transaction creation parameters. 4. **Stable Contract Logic:** Your smart contract logic is largely **finalized**, enabling you to focus on gas optimization, without expecting major functional changes. ## Instructions for Optimizing Gas Fees ### 1. Minimize On-Chain Data Storage * **Data Compression:** Instead of storing raw large data sets, try to store **compressed** or hashed references. * **Hashing and Linking:** Use hashes (e.g., `Keccak256`) to reference large off-chain data, reducing on-chain storage operations. * **State Variable Efficiency:** Consolidate related data into fewer state variables or use bit-packing techniques to store multiple flags or small integers in a single variable. ### 2. Batch Transactions * **Multi-Call Transactions:** Combine **multiple** related operations into a single transaction when possible, reducing overhead and the total gas spent on repeated transaction components. * **Use Off-Chain Aggregation:** Aggregate user actions off-chain and submit them in **bulk** on-chain as a single batched update. ### 3. Optimize Smart Contract Logic * **Simplify Computations:** Remove redundant loops, pre-calculate results off-chain, and use efficient algorithms. * **Use Mappings and Arrays Wisely:** Accessing storage variables is expensive. Consider using more gas-efficient data structures (e.g., **mapping** instead of arrays for lookups) and keep arrays as short as possible. * **Leverage Immutable Variables and Constants:** Mark values that do not change as `constant` or `immutable` to reduce gas costs associated with lookups. ### 4. Regularly Audit and Test * **Iterative Testing:** Deploy test contracts on testnets to measure gas usage, iterating on code changes to verify improvements. * **Automated Tools:** Use gas profiling and analytics tools (e.g., `hardhat-gas-reporter`) to identify and track optimization progress. ### 5. Leverage Layered Architectures * **Off-Chain Computations and Oracles:** Perform complex calculations off-chain and feed only the necessary results into the contract. * **Rollups or Sidechains:** While peaq EVM may offer certain scaling capabilities, consider hybrid approaches that further reduce mainnet gas consumption. ## Summary By applying these strategies—reducing on-chain complexity, storing less data and opting for hashed data, batching operations, and thoroughly testing changes—you can **significantly lower gas fees** on the peaq EVM network without compromising your decentralized application's functionality. # chain_subscribeNewHeads Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/listening-parsing-chain-events/chain-subscribeNewHeads The peaq network exposes a **JSON-RPC interface** over WebSockets that allows clients to **subscribe** to various chain events. In this guide, we focus on the `chain_subscribeNewHeads` method. It is designed to notify you whenever a new **block header** is produced on the blockchain. When you call this method, the node returns a subscription ID. Subsequently, as new blocks are finalized or produced, the node pushes notifications to your client containing block header information (such as the **parent hash**, **block number**, **state root**, **extrinsics root**, and **digest logs**). Key points: * `chain_subscribeNewHeads` is a stable and well-supported JSON-RPC method for **header subscriptions**. * Once subscribed, the node sends notifications (commonly using the method `"chain_newHead"` or sometimes `"subscription"`) that include the block header data. * The subscription is maintained over the lifetime of the WebSocket connection. To clean up, you should unsubscribe using `chain_unsubscribeNewHeads` before closing the connection. ## Prerequisites Before proceeding, the following Prerequisites are made: 1. **WebSocket Endpoint:** * You have access to a peaq node via a WebSocket endpoint. The script expects this endpoint to be available via an environment variable (e.g., `PEAQ_WS_URL`). If not provided, it falls back to a default endpoint (e.g., `wss://quicknode.peaq.xyz`). 2. **JSON‑RPC Support:** * The node supports the standard JSON-RPC methods, including **`chain_subscribeNewHeads`** and **`chain_unsubscribeNewHeads`**. This guide assumes you are using the stable methods (rather than unstable alternatives that may not work reliably). 3. **Raw WebSocket Library (ws):** * To avoid any interference from additional wrappers (such as those found in some Web3 libraries), the example uses the `ws` package. This ensures that you receive the exact JSON-RPC messages directly from the node. 4. **Chain Activity:** * The network is producing new blocks. If no new blocks are produced after you subscribe, you may not see any notifications immediately. ## Instructions ### 1. Setting Up the Environment * **Install Dependencies:** Ensure you install the necessary Node.js packages: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ws dotenv ``` * **Set ESM Module:** Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` * **Environment Variables:** Create a `.env` file in your project directory (if not already present) and set the WebSocket endpoint URL: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PEAQ_WS_URL=wss://quicknode.peaq.xyz ``` ### 2. Understanding the Subscription Flow * **Initiating the Connection:** The script establishes a direct WebSocket connection to the peaq node. Once connected, it sends a JSON-RPC payload to subscribe to new block headers using the **`chain_subscribeNewHeads`** method. * **Subscription Request:** The JSON‑RPC request payload includes: * `"jsonrpc": "2.0"`: The protocol version. * `"method": "chain_subscribeNewHeads"`: The method to subscribe to block headers. * `"params": []`: No additional parameters are required. * `"id": 1`: A unique identifier for the request. * **Handling the Response:** The node responds with a JSON-RPC message that includes a subscription ID. This ID is saved locally and used to filter incoming notifications. * **Receiving Notifications:** After a successful subscription, the node pushes notifications (using either `"chain_newHead"` or the generic `"subscription"` method) that include a `params` object. The script checks if the incoming message's `subscription` field matches the stored subscription ID and then parses out the block header details. * **Graceful Shutdown:** When the client (or user) sends an interrupt (SIGINT, usually via Ctrl+C), the script sends an unsubscribe request using **`chain_unsubscribeNewHeads`** with the subscription ID. This cleanly ends the subscription and then closes the WebSocket connection. ### 3. Running the Boilerplate ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // chain_subscribeNewHeads.js import dotenv from 'dotenv'; dotenv.config(); import WebSocket from 'ws'; const WS_URL = process.env.PEAQ_WS_URL; const ws = new WebSocket(WS_URL); let subscriptionId = null; const SUBSCRIBE_REQUEST_ID = 1; const UNSUBSCRIBE_REQUEST_ID = 2; ws.on('open', () => { console.log("WebSocket connected"); // Send a subscription request using the stable method "chain_subscribeNewHeads" const subscribePayload = { jsonrpc: "2.0", method: "chain_subscribeNewHeads", params: [], id: SUBSCRIBE_REQUEST_ID, }; console.log("Sending subscription payload:", JSON.stringify(subscribePayload)); ws.send(JSON.stringify(subscribePayload)); }); ws.on('message', (message) => { try { const data = JSON.parse(message); console.log("Received message:", JSON.stringify(data, null, 2)); // Handle the subscription response (id: 1) if (data.id === SUBSCRIBE_REQUEST_ID && data.result) { subscriptionId = data.result; console.log("Subscribed to new heads with ID:", subscriptionId); } // Handle incoming notifications for new block headers // Some nodes may return notifications under the method "chain_newHead" or the generic "subscription" else if ((data.method === 'chain_newHead' || data.method === 'subscription') && data.params && data.params.subscription === subscriptionId) { console.log("New Block Header received:"); console.log(data.params.result); } } catch (e) { console.error("Error parsing message:", message); } }); ws.on('error', (error) => { console.error("WebSocket error:", error); }); ws.on('close', () => { console.log("WebSocket connection closed"); }); // Graceful shutdown: send an unsubscribe request and close the socket process.on('SIGINT', () => { console.log("\nGracefully shutting down..."); if (subscriptionId) { const unsubscribePayload = { jsonrpc: "2.0", method: "chain_unsubscribeNewHeads", params: [subscriptionId], id: UNSUBSCRIBE_REQUEST_ID, }; console.log("Sending unsubscribe payload:", JSON.stringify(unsubscribePayload)); ws.send(JSON.stringify(unsubscribePayload), () => { ws.close(); process.exit(); }); } else { ws.close(); process.exit(); } }); console.log("Listening for new block headers on the peaq network (chain_subscribeNewHeads) using ws..."); ``` * **Start the Script:** Run the script using Node.js: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} node chain_subscribeNewHeads.js ``` * You should see logs indicating that the WebSocket connection is established and that the subscription request has been sent. If new blocks are produced, you will see the raw notifications logged (including block header details). eth-subscribe-new-heads-1 * **Interpreting Logs:** * A log entry like `Subscribed to new heads with ID: ` confirms the subscription was successful. * When a new block header is received, the script prints the details under the message `New Block Header received:`. * If no notifications are received, double-check that the chain is active and that the node is emitting new block headers. * **Shutdown:** Press Ctrl+C to trigger the graceful shutdown. The script will send an unsubscribe request before closing the connection. ## Summary This guide walked you through the process of listening and parsing chain events (block headers) on the peaq network using the **`chain_subscribeNewHeads`** JSON-RPC method. Using this approach, you can reliably monitor **new block headers** on the peaq network and integrate these events into your blockchain applications or debugging workflows. # eth_newFilter + eth_getFilterChanges Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/listening-parsing-chain-events/eth-newFilter In blockchain systems, **logs** are low-level events generated by contracts and system actions that can provide insights into on-chain activity. Unlike block headers or pending transactions, logs can capture **specific events** or **state changes**, which are useful for debugging, monitoring, and analytics. Using the JSON-RPC method `eth_newFilter`, you can create a filter on the node that watches for new log events. Then, by periodically calling `eth_getFilterChanges`, you can poll the node for any new logs that match your filter criteria. This **push-poll model** (as opposed to a true push subscription) is especially useful when the node does not offer a native subscription for logs. In the provided **boilerplate**: * A **log filter** is created with the parameter `{ fromBlock: "latest" }` so that only logs from the current block onward are captured. * The code then polls for changes every 5 seconds by calling **`eth_getFilterChanges`** with the filter ID. * Each received log is then printed to the console. * The boilerplate also includes error handling and a graceful shutdown routine that uninstalls the filter and disconnects the WebSocket provider. ## Prerequisites * **Node.js Environment:** You are running this code in a Node.js environment. * **Dependencies:** The following packages are installed: * `web3` (v4.x) * `web3-providers-ws` * `dotenv` * **WebSocket Endpoint:** You are connecting to the peaq network via the WebSocket endpoint `wss://quicknode.peaq.xyz`, configured via a `.env` file. * **Basic Understanding:** You have a basic understanding of JSON‑RPC, WebSocket connections, and blockchain logs. ## Instructions ### 1. Setting Up the Environment * **Install Dependencies:** Make sure you install the necessary packages: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install web3 dotenv web3-providers-ws ``` * **Set ESM Module:** Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` * **Configure Environment Variables:** Create a `.env` file in your project directory with: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PEAQ_WS_URL=wss://quicknode.peaq.xyz ``` ### 2. Creating the Log Filter * **Using eth\_newFilter:** The method `eth_newFilter` creates a filter on the node to watch for specific log events. In our boilerplate, we call it with parameters like `{ fromBlock: "latest" }` to capture logs from the most recent block onward. The node responds with a filter ID that uniquely identifies this filter. ### 3. Polling for Log Changes * **Using eth\_getFilterChanges:** Once the filter is created, you poll for changes by calling `eth_getFilterChanges` with the **filter ID**. This method returns any logs that have been recorded since the last poll. The code uses a polling interval (5 seconds in this example) to check for new logs **continuously**. * **Processing Logs:** Each new log returned by `eth_getFilterChanges` is iterated over and printed to the console. This allows your application to process each event as needed. ### 4. Error Handling * **During Filter Creation and Polling:** Both the creation of the filter and the polling calls include error checks. If an error occurs (or if the node returns an error), the error is logged to the console. This ensures that issues such as network problems or incorrect parameters are surfaced immediately. ### 5. Graceful Shutdown * **Uninstalling the Filter:** Upon receiving a **shutdown signal** (e.g., Ctrl+C), the code sends a JSON‑RPC request using **`eth_uninstallFilter`** with the filter ID. This uninstalls the filter from the node, ensuring that resources are freed. * **Disconnecting the Provider:** After uninstalling the filter, the WebSocket provider is disconnected to cleanly close the connection before the process exits. ## Code Example ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // eth_filter_logs.js import dotenv from 'dotenv'; dotenv.config(); import Web3 from 'web3'; import WSProvider from 'web3-providers-ws'; const WS_URL = process.env.PEAQ_WS_URL; const provider = new WSProvider(WS_URL); const web3 = new Web3(provider); let logFilterId = null; // Create a log filter using a raw JSON-RPC call provider.send( { jsonrpc: "2.0", method: "eth_newFilter", params: [{ fromBlock: "latest" }], id: 1, }, (error, response) => { if (error || response.error) { console.error("Error creating log filter:", error || response.error); return; } logFilterId = response.result; console.log("Created log filter with ID:", logFilterId); // Start polling for log changes every 5 seconds setInterval(() => { provider.send( { jsonrpc: "2.0", method: "eth_getFilterChanges", params: [logFilterId], id: 2, }, (err, res) => { if (err || res.error) { console.error("Error polling log filter:", err || res.error); return; } if (res.result && res.result.length > 0) { res.result.forEach((log) => { console.log("Chain Log received:"); console.log(log); }); } } ); }, 5000); } ); // Graceful shutdown process.on('SIGINT', () => { console.log("\nGracefully shutting down..."); if (logFilterId) { provider.send( { jsonrpc: "2.0", method: "eth_uninstallFilter", params: [logFilterId], id: 3, }, (error, response) => { if (error || response.error) { console.error("Error uninstalling log filter:", error || response.error); } else { console.log("Uninstalled log filter:", logFilterId); } provider.disconnect(); process.exit(); } ); } else { provider.disconnect(); process.exit(); } }); console.log("Listening for chain logs on the peaq network (eth_newFilter + eth_getFilterChanges)..."); ``` ### Example logs: eth-new-filter-1 ## Summary This guide has demonstrated how to listen and parse chain log events on the peaq network using a combination of `eth_newFilter` and `eth_getFilterChanges`. By using these methods, you can efficiently monitor **chain logs**, enabling real-time analysis and responsive applications on the peaq network. This approach is especially useful when native log subscriptions are not available or when you prefer a controlled polling mechanism. Customize the provided boilerplate as needed to suit your application's requirements. # eth_subscribe Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/listening-parsing-chain-events/eth-subscribe In blockchain systems, pending transactions are those that have been submitted to the network but have **not yet been included** in a block. By subscribing to these events using `eth_subscribe`, your application can receive **real-time notifications** of transaction hashes as they enter the transaction pool (mempool). This push-based approach helps build responsive decentralized applications, monitor network activity, and provide transaction tracking or analytics in near real-time. In this guide, we focus on subscribing to pending transactions using **`eth_subscribe`**. The provided boilerplate uses a WebSocket provider (pointing to `wss://quicknode.peaq.xyz`), creates a subscription for pending transactions, handles incoming data and errors, and includes a graceful shutdown routine to properly clean up the subscription and disconnect the WebSocket provider. ## Prerequisites * **Node.js Environment:** You have Node.js installed on your system. * **Familiarity with JavaScript:** You are comfortable with JavaScript and asynchronous programming (using async/await). * **WebSocket Connection:** You will connect to a peaq network node via WebSocket (using `wss://quicknode.peaq.xyz`). * **Dependencies:** The required packages (`web3`, `dotenv`, and `web3-providers-ws`) are installed. * **Basic Blockchain Knowledge:** You understand the concepts of pending transactions, mempool, and real‑time event subscription. ## Instructions ### 1. Setting Up the Environment * **Install Dependencies:** Ensure you install the necessary packages: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install web3 dotenv web3-providers-ws ``` * **Set ESM Module:** Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` * **Configure Environment Variables:** Create a `.env` file with the following content: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PEAQ_WS_URL=wss://quicknode.peaq.xyz ``` ### 2. Understanding `eth_subscribe` The **`eth_subscribe`** method is a JSON-RPC method available over **WebSocket** connections. It allows you to subscribe to various types of events on the blockchain in real-time. When subscribing to pending transactions, the node sends updates as soon as new transactions are detected in the **mempool.** Here's how it works: * **Subscription Request:** When you call `web3.eth.subscribe('pendingTransactions')`, the node sets up a listener for new pending transactions. * **Data Event:** The subscription object returns a push-based stream of events. Each event provides the transaction hash of a pending transaction. The code attaches an event handler on the `"data"` event to process each incoming transaction hash. * **Error Handling:** An `"error"` event is also attached to handle any issues (e.g., network errors or connection drops). ### 3. **Implementing the Boilerplate** The provided boilerplate performs the following steps: * **Initialize the Provider and Web3 Instance:** A WebSocket provider is created using `web3-providers-ws`, connecting to the peaq network RPC endpoint. * **Subscribe to Pending Transactions:** An asynchronous function is used to call `web3.eth.subscribe('pendingTransactions')`. Once the subscription is created, event handlers for `"data"` and `"error"` are attached. * **Graceful Shutdown:** A signal handler for `SIGINT` (e.g., when pressing Ctrl+C) is registered. Upon receiving the shutdown signal, the code unsubscribes from pending transactions and disconnects the WebSocket provider to ensure a clean exit. ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // eth_subscribe_pendingTx.js import dotenv from 'dotenv'; dotenv.config(); import Web3 from 'web3'; import WSProvider from 'web3-providers-ws'; const WS_URL = process.env.PEAQ_WS_URL; const provider = new WSProvider(WS_URL); const web3 = new Web3(provider); let pendingSubscription; (async () => { try { pendingSubscription = await web3.eth.subscribe('pendingTransactions'); pendingSubscription.on("data", (txHash) => { console.log("Pending Transaction Hash received:"); console.log(txHash); }); pendingSubscription.on("error", (error) => { console.error("Error in pendingTransactions subscription:", error); }); } catch (err) { console.error("Error subscribing to pendingTransactions:", err); } })(); // Graceful shutdown process.on('SIGINT', async () => { console.log("\nGracefully shutting down..."); try { if (pendingSubscription) { await pendingSubscription.unsubscribe(); console.log("Unsubscribed from pendingTransactions."); } } catch (err) { console.error("Error unsubscribing from pendingTransactions:", err); } provider.disconnect(); process.exit(); }); console.log("Listening for pending transactions on the peaq network (eth_subscribe)..."); ``` The terminal will output the pending transaction hash as follows: eth-subscribe-1 ## Conclusion This guide demonstrates how to use the `eth_subscribe` JSON-RPC method to listen to pending transactions on peaq network. Using this approach, your application can efficiently monitor the mempool for new pending transactions, enabling you to build responsive, data-driven decentralized applications on the peaq network. Feel free to customize and extend the boilerplate code to suit your specific requirements. # Introduction Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/listening-parsing-chain-events/introduction Listening and parsing chain events is a crucial aspect of blockchain development. By subscribing to or querying for specific events—like **new blocks**, **pending transactions**, or **contract logs**—you can build real-time applications that respond instantly to on-chain activity. Whether you're monitoring blocks to confirm transactions or listening for specific smart contract events, understanding how to capture and interpret these updates is key to creating interactive and dynamic blockchain services. # Introduction In this section, we explore various methods for receiving and processing chain events. You'll learn how to use subscription-based APIs (like [`eth_subscribe`](/peaqchain/build/basic-operations/listening-parsing-chain-events/eth-subscribe) and [`chain_subscribeNewHeads`](/peaqchain/build/basic-operations/listening-parsing-chain-events/chain-subscribeNewHeads)) for live notifications, as well as filter-based approaches ([`eth_newFilter` + `eth_getFilterChanges`](/peaqchain/build/basic-operations/listening-parsing-chain-events/eth-newFilter)) for more targeted event tracking. By mastering these techniques, you'll be able to develop robust applications that keep pace with on-chain changes as they happen. # Smart Contract Events Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/listening-parsing-chain-events/smart-contract-events Contract events are a key feature of blockchain systems, enabling applications to react to specific state changes in an efficient manner. On peaq network, **chain events** can be emitted during contract execution and logged to the blockchain. These events can be captured and parsed to **build responsive dApps**, **monitor activity**, or **trigger off-chain workflows**. This guide provides step-by-step instructions on how to listen to and parse **smart contract events** on the peaq network. ## Prerequisites 1. You have a working knowledge of Ethereum-based networks and EVM-compatible smart contracts. 2. You have deployed a smart contract on the peaq network that emits events. 3. You are using **ethers.js** or **web3.js** in a JavaScript or Node.js environment. 4. You have access to a **peaq network RPC endpoint** (e.g., [https://quicknode.peaq.xyz](https://quicknode.peaq.xyz)). 5. You know the [**ABI**](https://www.alchemy.com/overviews/what-is-an-abi-of-a-smart-contract-examples-and-usage) of the contract you are monitoring and the address of the deployed contract. ## Instructions ### 1. Set Up Your Project Ensure your environment is set up with the necessary libraries and RPC configuration: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers ``` ### 2. Initialize Connection to the peaq Network Connect to the peaq network using ethers.js: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; const provider = new ethers.JsonRpcProvider("https://quicknode.peaq.xyz"); ``` ### 3. Load the Smart Contract To interact with the smart contract, load its address and ABI: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const contractAddress = "0xYourContractAddress"; // Replace with your smart contract's address const contractABI = [ // Add your contract's ABI here or import with require() instead ]; const contract = new ethers.Contract(contractAddress, contractABI, provider); ``` ### 4. Listen for Events Using ethers.js, you can set up listeners for specific events emitted by the contract: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract.on("YourEventName", (param1, param2, event) => { console.log(`Event received:`); console.log(`Param 1: ${param1}`); console.log(`Param 2: ${param2}`); console.log(`Full Event:`, event); }); ``` ### 5. Parse Events Ethers.js provides the raw event log data, which can be parsed for additional details: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} provider.on("logs", (log) => { try { const parsedLog = contract.interface.parseLog(log); console.log("Parsed Log:", parsedLog); } catch (err) { console.log("Not a relevant event:", err); } }); ``` ### 6. Filter Specific Events To reduce overhead, use filters to listen for specific events: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const filter = contract.filters.YourEventName(); // Add any indexed parameters if applicable provider.on(filter, (log) => { const parsedLog = contract.interface.parseLog(log); console.log(`Filtered Event: ${parsedLog.name}`); console.log(`Event Data:`, parsedLog.args); }); ``` ### 7. Testing the Listener Trigger events in your smart contract (e.g., using a wallet or script) and ensure the listener captures them correctly. **Example:** Emit an event in your smart contract: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} event YourEventName(address indexed user, uint256 value); emit YourEventName(msg.sender, 1000); ``` Expected console output: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Console Logs Example ................... Event received: Param 1: 0xYourWalletAddress Param 2: 1000 Full Event: {...} ``` Example final boilerplate incorporating previous steps, best practices for environment variables, and modular imports: ### Putting it all Together ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} //event-listener.js import dotenv from 'dotenv'; dotenv.config(); import { ethers } from "ethers"; // Load environment variables const RPC_URL = process.env.PEAQ_RPC_URL || "https://quicknode.peaq.xyz"; // Replace with your preferred peaq RPC URL const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS; // Replace with your contract address import CONTRACT_ABI from "./contract_abi.json"; // Replace with the path to your contract ABI JSON file // Initialize provider and contract const provider = new ethers.JsonRpcProvider(RPC_URL); const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider); // Function to listen for events const listenForEvents = () => { console.log("Listening for events on contract:", CONTRACT_ADDRESS); // Replace "YourEventName" with the event name from your contract ABI contract.on("YourEventName", (param1, param2, event) => { console.log("Event Received:"); console.log(`Param 1: ${param1}`); console.log(`Param 2: ${param2}`); console.log("Raw Event Data:", event); }); // Add a generic log listener (optional) provider.on("logs", (log) => { try { const parsedLog = contract.interface.parseLog(log); console.log("Parsed Event Log:"); console.log(parsedLog); } catch (error) { // Not a relevant event, ignore } }); }; // Error handling and script execution const main = async () => { try { listenForEvents(); } catch (error) { console.error("Error in event listener:", error); process.exit(1); } }; main(); ``` ## **Tips** 1. **Error Handling**: Always handle potential errors (e.g., connectivity issues, unexpected logs). 2. **Batch Processing**: Use a combination of historical event fetching (`getLogs`) and real-time listeners to ensure no data loss. 3. **Indexed Parameters**: Use indexed parameters in your contract events to filter by specific values (e.g., filter by user address). **Example:** ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} event YourEventName(address indexed user, uint256 value); ``` Then filter by the address: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const filter = contract.filters.YourEventName("0xSpecificAddress"); ``` 4. **Environment Variables**: Store sensitive information (e.g., RPC URLs) in environment variables using a `.env` file. Congratulations! 🎉 You now have the ability to efficiently listen to and parse smart contract events on the peaq network, empowering your dApp to respond dynamically to on-chain activity. # MultiTokenVesting Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/multitoken-vesting A gas-optimized, multi-token vesting contract for linear vesting schedules with optional cliffs. ## Overview `MultiTokenVesting` is a Solidity smart contract designed for **linear vesting** of **multiple ERC-20 tokens** from a single deployment. An Owner (admin) creates vesting schedules for beneficiaries, and beneficiaries claim vested tokens over time. Key behaviors: * Vesting is linear (optionally with a cliff) * Schedules can be **revoked** by the Owner * The contract enforces a **solvency check** by pulling tokens in at schedule creation ## Key features ### Multi-token support One contract can manage vesting schedules for many ERC-20 tokens (e.g., USDC, WETH, UNI), without redeploying per token. ### Revocable schedules When the Owner revokes a schedule: * The **vested** amount remains claimable by the beneficiary * The **unvested** portion is refunded back to the Owner ### Gas optimization The contract is optimized for production use through: * Packed data structures (addresses + flags) * Custom errors (cheaper than revert strings) * Direct claiming by schedule index ### Safety features * **Solvency check**: tokens required for a schedule are transferred into the contract at creation time * **Withdraw excess**: the Owner can withdraw only **excess tokens** accidentally sent to the contract (not those locked for active schedules) ## Supported tokens * **ERC-20** tokens are supported. Native gas tokens (e.g., using PEAQ as a native currency) are not supported directly by this contract. Wrap native tokens into an ERC-20 representation if needed. ## Vesting logic Vesting follows a simple timeline: * **Before cliff**: 0 claimable * **After cliff → end**: linear unlock * **After end**: 100% claimable Formula: `VestedAmount = (TotalAmount * (CurrentTime - StartTime)) / Duration` Example: * Total amount: 1000 * Duration: 1000 seconds * Cliff: 250 seconds Progression: * At 0s: 0 vested * At 250s: 250 vested * At 500s: 500 vested * At 1000s: 1000 vested ## Quickstart (peaq-friendly) You deploy `MultiTokenVesting` on peaq EVM like any Solidity contract: * Build and understand the contract: [Build Smart Contract](/peaqchain/build/basic-operations/smart-contracts/build-smart-contract) * Deploy it: [Deploy Smart Contract](/peaqchain/build/basic-operations/smart-contracts/deploy-smart-contract) * Interact with it: [Interact with Smart Contract](/peaqchain/build/basic-operations/smart-contracts/interact-with-smart-contract) If you prefer Foundry, these commands match common peaq setups: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} forge install OpenZeppelin/openzeppelin-contracts --no-commit forge build forge test -vv ``` ## How to use ### A) Approve tokens (ERC-20) Approve the vesting contract to pull tokens from the Owner’s wallet. This call is made on the **ERC-20 token contract**, not the vesting contract. ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} IERC20(tokenAddress).approve(vestingContractAddress, amount); ``` ### B) Create vesting schedule (Owner-only) Create a schedule that starts now, has a 30-day cliff, and vests linearly over 1 year. ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} vesting.createVestingSchedule( beneficiary, token, amount, block.timestamp, 2592000, // 30 days 31536000 // 1 year ); ``` ### C) Claim (Beneficiary-only) Beneficiaries claim by `scheduleIndex`: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} vesting.claim(scheduleIndex); ``` The `scheduleIndex` is emitted by the `ScheduleCreated` event. In practice, your app should index this event and store schedule indices per beneficiary. ### D) Revoke (Owner-only) The Owner can revoke a schedule: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} vesting.revoke(scheduleIndex); ``` On revoke: * Beneficiary keeps vested amount up to the revoke time * Remaining unvested amount returns to the Owner ### E) Withdraw excess (Owner-only) Withdraw tokens that are not locked in active schedules: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} vesting.withdrawExcess(tokenAddress); ``` ## API reference ### VestingSchedule fields * `beneficiary` (address) * `start` (uint64) * `revoked` (bool) * `claimed` (bool) * `token` (address) * `duration` (uint64) * `cliff` (uint64) * `totalAmount` (uint256) * `amountClaimed` (uint256) ### Errors | Error | Meaning | | --------------------------- | ---------------------------------------------------------------- | | `InvalidAddress` | An address argument is zero or invalid | | `InvalidAmount` | Amount is zero or invalid | | `InvalidDuration` | Duration is zero or invalid | | `InvalidCliff` | Cliff is invalid (e.g., greater than duration) | | `Unauthorized` | Caller is not allowed to perform this action | | `ScheduleClaimed` | Schedule was already fully claimed | | `NothingToClaim` | No tokens are currently claimable | | `InvalidIndex` | Provided schedule index does not exist | | `ScheduleWasRevoked` | Schedule was revoked and no longer claimable in the expected way | | `InsufficientExcessBalance` | Not enough excess balance to withdraw | ## Security notes * Solidity `^0.8.20` includes overflow checks by default. * Uses `SafeERC20` to safely handle non-standard ERC-20 implementations. * Claim flows should follow **Checks-Effects-Interactions**. * Centralization risk: the Owner can revoke schedules (beneficiaries must trust the Owner). # On-chain vs Off-chain Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/on-chain-vs-off-chain Blockchain technology provides a decentralized and immutable ledger for recording transactions and storing data proofs. However, because blockchain data is publicly accessible, it is not designed to secure sensitive information by default. Instead, blockchains are primarily used for storing **hash representations** or **encrypted versions** of data. This ensures that the ledger remains **efficient**, **tamper-resistant**, and **verifiable**, while larger or more sensitive data is securely managed through off-chain storage solutions. 1. **Blockchain Efficiency:** Storing large data directly on-chain is resource-intensive and costly. Blockchains are designed to prioritize consensus, security, and scalability. 2. **Data Integrity:** Storing hash representations of data on-chain ensures that the integrity and authenticity of the off-chain data can be verified without directly storing it. 3. **Access Control:** Off-chain storage often incorporates mechanisms to ensure only verifiable entities can access sensitive or private metadata. 4. **Hybrid Storage:** Combining on-chain and off-chain storage provides a balance between decentralization, cost, and performance. # Key Concepts ### **On-Chain Data:** On-chain data refers to data stored directly on the blockchain. Some examples with the peaq blockchain include: **hashes of DID Documents**, **transaction metadata**, **storage configurations**, **RBAC data**, and **smart contract states**. Data stored on the blockchain is **immutable** and **publicly verifiable**. **Benefits:** * **Immutability**: On-chain data is designed to be **tamper-evident**—once recorded, it cannot be altered. However, **immutability does not automatically guarantee full security**; the overall security depends on the broader context and additional safeguards. * **Verification**: Storing a **hash** ensures the **authenticity** of off-chain data without compromising **privacy**. * **Transparency**: On-chain data is **publicly accessible** and **verifiable**, promoting openness and auditability. ### **Off-Chain Data:** Off-chain data refers to data stored outside the blockchain (e.g., in distributed storage systems like **IPFS**, traditional databases (**MongoDB**), or **cloud services**). This will typically include large files, metadata, or sensitive information since the data is too large or too sensitive to be stored on a public blockchain. Typically the off-chain storage is accessible and controlled through cryptographic mechanisms or decentralized identifiers. **Benefits:** * **Scalability**: Reduces blockchain bloat by storing large or complex data elsewhere. * **Flexibility**: Allows for dynamic updates to metadata without altering on-chain records. * **Privacy**: Sensitive data can be encrypted and shared with authorized entities only. ### Example Use Case: DID Document * **On-Chain:** Store the hash of the DID Document along with its basic details. * **Off-Chain:** Store the metadata (e.g., entity metadata too large for DID Document) in an off-chain storage solution. * **Access Control:** Ensure only authorized entities can access the off-chain services via verifiable credentials or permissions. To learn more about how to use IPFS or MongoDB with a DID Document please check out the tutorial: [Off-Chain Storage Solutions](/peaqchain/build/advanced-operations/off-chain-storage/ipfs) This hybrid approach ensures scalability, security, and privacy, enabling blockchain-based systems to handle complex data requirements efficiently. # Fetching Data Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contract-storage/fetching-data Smart contract storage can be used to **read** from smart contract state variables and to **retrieve** stored information. This could be anything from user balances, configuration parameters, or complex data structures such as mappings and arrays. In this guide, we will walk through: * How smart contract storage works in general. * Common scenarios where you need to retrieve data from contract storage. * Step-by-step **instructions** on how to fetch data, using **Solidity** and a client library (e.g., `web3.js` or `ethers.js`). * Best practices and potential pitfalls. By the end, you will understand how to confidently and securely fetch data from your peaq-based smart contracts. ## Prerequisites * Understanding of Solidity * You have a working development environment set up with: * A Solidity compiler (e.g., via Hardhat, Truffle, or Remix). * Access to the peaq network's EVM endpoint (or a local test EVM). * Using a library like `web3.js` or `ethers.js` to interact with your deployed contracts. Alternatively, any other JSON-RPC client can be used if you are comfortable with lower-level interactions. * Already have a deployed contract on the peaq network, or you have the means to deploy one during testing. * Access to the contract's Application Binary Interface (ABI). This is generally generated automatically when compiling your Solidity smart contract. ## Instructions ### 1. Understanding EVM Contract Storage In EVM-based networks like peaq, each contract has its own allocated storage. Storage is typically broken down into **slots** of 256 bits each: * **State Variables** store data in these storage slots. * **Mappings** are hashed to find their storage slots. * **Arrays** and other data structures have their own packing and storage rules. When writing or reading from storage, gas costs and efficient layout (for writes) can be important. However, retrieving data (a read) is **cheaper** than storing data (a write). For reads, you can **query** the node's state without incurring on-chain transaction costs. ### 2. Declaring and Storing Data in Solidity Let's look at a simple example contract that stores some data: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 public storedValue; mapping(address => uint256) public balances; constructor(uint256 initialValue) { storedValue = initialValue; balances[msg.sender] = 100; } // A simple setter function function setValue(uint256 newValue) external { storedValue = newValue; } // A simple function that updates balances function updateBalance(address account, uint256 amount) external { balances[account] = amount; } } ``` * `storedValue` is a public `uint256`. By making it public, Solidity automatically creates a getter function `storedValue()` that returns the value. * `balances` is a public mapping from `address` to `uint256`. Likewise, `balances(address)` becomes a getter function to read the balance for any given address. ### 3. Interacting With the Contract to Fetch Data #### Using a Script (JavaScript + ethers.js/web3.js) Below is an example of how to fetch data from `storedValue` and `balances` using `ethers.js`. **Ethers.js Example:** ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from 'ethers'; // 1. Connect to the peaq network (replace with actual provider endpoint) const provider = new ethers.JsonRpcProvider("https://your-peaq-rpc-endpoint"); // 2. Define the contract ABI (simplified for this example) const abi = [ "function storedValue() view returns (uint256)", "function balances(address) view returns (uint256)" ]; // 3. The deployed contract address const contractAddress = "0xYourContractAddressOnPeaq"; // 4. Create an instance of the contract const contract = new ethers.Contract(contractAddress, abi, provider); async function readData() { try { // 5. Call the view functions const value = await contract.storedValue(); console.log("storedValue:", value.toString()); const someAddress = "0x1234..."; // Replace with a valid address const balance = await contract.balances(someAddress); console.log(`Balance of ${someAddress}:`, balance.toString()); } catch (error) { console.error("Error fetching data:", error); } } readData(); ``` **Key Points** * We use the `view` functions (`storedValue()` and `balances(address)`) to retrieve data without sending a transaction. * This means there is no cost in terms of gas for these read operations; only the provider's request/response overhead applies. #### Using the Remix IDE If you prefer a browser-based approach: 1. Open Remix IDE. 2. Select the “Solidity” environment and load your contract code. 3. Compile and Deploy to a peaq network-compatible endpoint (via “Deploy & run transactions” panel). 4. After deployment, you will see the contract’s interface in Remix. * Clicking on `storedValue` will fetch and display the current `storedValue`. * Providing an address to `balances` will fetch and display the mapping value for that address. ### 4. Fetching Custom or Complex Data Types If you have **custom** data structures like **structs** or **nested mappings**, you can expose them through custom getter functions. For example: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ComplexStorage { struct User { uint256 id; uint256 balance; address userAddress; } mapping(address => User) private users; // Insert or update user function setUser(address _address, uint256 _id, uint256 _balance) external { users[_address] = User(_id, _balance, _address); } // Public function to read user data function getUser(address _address) external view returns (uint256, uint256, address) { User memory user = users[_address]; return (user.id, user.balance, user.userAddress); } } ``` Here, the struct `User` is stored in a private mapping. We expose a `getUser` function that returns the data in a single call. You can fetch it from your JavaScript code similarly with a function call: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const contractAbi = [ "function getUser(address _address) view returns (uint256, uint256, address)" ]; const contract = new ethers.Contract(contractAddress, contractAbi, provider); const userData = await contract.getUser(someUserAddress); console.log("User Data => ID:", userData[0].toString(), "Balance:", userData[1].toString(), "User Address:", userData[2]); ``` This approach helps keep your contract's data **private** while still allowing necessary read-access to data via dedicated functions. ## Summary Fetching data from smart contract storage on the peaq network (or any EVM-compatible network) follows the same paradigm as Ethereum: * You **define** state variables in Solidity. * You **expose** those variables either with the `public` keyword or dedicated getter functions. * You **interact** with them through a client library (e.g., `ethers.js`, `web3.js`) or Remix IDE. **Key Takeaways**: * **Public Variables**: Automatically generate getter functions, convenient for straightforward retrieval. * **View/Pure Functions**: Cost no gas when called off-chain, making them ideal for reading data. * **Custom Getter Functions**: Useful for complex data structures or additional data processing. * **Security**: Ensure that any data you do not want freely accessible remains in private variables, and only expose it through controlled view functions as needed. With these core principles in mind, you can confidently **fetch data** from your peaq-based Solidity contracts, enabling rich dApp functionality and user-friendly front-end experiences. # Optimizing Storage Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contract-storage/optimizing-storage Storage **optimization** in Solidity smart contracts is an essential consideration given the relatively high costs associated with writing data to the blockchain. Every write operation has a **gas fee**, and more extensive storage usage equates to **increased** deployment and execution costs. The peaq network provides EVM compatibility, so the same best practices that apply to Ethereum also apply here—but within peaq's unique economy and infrastructure. **Key topics covered in this guide include:** * How the EVM (on peaq) stores data in 256-bit storage slots. * Why **mappings** can be more efficient than arrays in certain scenarios. * Additional strategies like data packing, using `bytes` effectively, and using events instead of on-chain storage. * Practical coding examples in Solidity. ## Prerequisites * Basic Solidity Knowledge * An understanding of how the EVM handles storage, memory, and call data, as well as the associated gas costs for these operations. * Aware that peaq network is EVM-compatible and that the best practices for storage optimization hold true in its environment, with potential additional benefits from the underlying Substrate-based infrastructure. ## Optimizations Below are some high-level strategies and examples to help you optimize your smart contract storage usage on the peaq network's EVM. ### Use Mappings Instead of Arrays (When Feasible) **Arrays** (especially dynamic arrays) often require more operations to manage (e.g., iteration, boundary checks) and can grow unbounded if not carefully restricted. **Mappings** store data more sparsely, **saving storage** when you do not need sequential elements. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // Example: Using a mapping instead of a dynamic array // Less optimal: using a dynamic array contract TestArray { uint256[] public items; function addItem(uint256 value) external { items.push(value); } // Accessing an element function getItem(uint256 index) external view returns (uint256) { return items[index]; } } // More optimal: using a mapping contract TestMapping { mapping(uint256 => uint256) public items; uint256 public itemCount; function addItem(uint256 value) external { items[itemCount] = value; itemCount++; } // Accessing an element function getItem(uint256 index) external view returns (uint256) { return items[index]; } } ``` **Explanation**: * In the above example, `TestMapping` is more flexible with sparse data and can save gas. * You can maintain a separate counter (`itemCount`) to track indices, mimicking array-like behavior. ### Use the Right Data Types and Pack Them The EVM stores data in 256-bit slots. When two or more variables fit into a **single** 256-bit slot, they can be **packed together** to reduce overall storage usage. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract DataPacking { // All of these will fit into a single 256-bit slot if ordered properly uint128 public valueA; // 128 bits uint64 public valueB; // 64 bits uint64 public valueC; // 64 bits // Another slot address public owner; // 160 bits bool public isActive; // 8 bits uint88 public counter; // 88 bits } ``` **Best Practice**: * Group smaller types **together** so they can occupy the same slot. * Reordering the variables to **minimize** wasted space in each 256-bit slot can substantially reduce gas costs. ### Store Data Off-Chain or Use Events When Appropriate If you only need data for historical or informational purposes (i.e., you don't need it to stay in contract storage for on-chain logic), consider storing it **off-chain** or **emitting** it in **events**. Events are cheaper than storing data on-chain and still let you retrieve the data from transaction logs. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract UseEvents { // Instead of storing all these logs on-chain... // mapping(uint256 => string) public logs; // This can grow unbounded // Emit an event to record data event LogRecorded(uint256 indexed id, string info); function recordLog(uint256 _id, string memory _info) external { emit LogRecorded(_id, _info); } } ``` **Note**: * Data in events is not accessible to contracts directly (only via off-chain **indexing**), so only move data to events if your contract does not need to rely on it for future state changes. * Check out our [indexing solutions](/peaqchain/build/advanced-operations/indexing) to see how to query these events. ### Consider Using `bytes` Arrays for Packed Storage In some scenarios (especially with strings or variable-length data), storing in a single `bytes` array can be more **efficient** than storing an array of fixed-size data types, as it packs data continuously without leaving empty space. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract BytesStorage { bytes public data; function appendData(bytes memory newData) external { // Append in memory, then store data = abi.encodePacked(data, newData); } } ``` ### Carefully Manage Storage Reads and Writes Each storage write operation **costs gas**, so **optimizing** writes—and reducing the number of expensive `SSTORE` operations—can lower costs. **Strategies**: * **Minimize writes** by caching frequently updated values in memory and writing only once when necessary. * **Use local variables** (memory) rather than reading from storage multiple times in a function. Each storage read is more expensive than a memory read. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract StorageWrites { uint256 public counter; function incrementCounter(uint256 times) external { uint256 temp = counter; // Only one storage read for (uint256 i = 0; i < times; i++) { temp++; // Increment in memory } counter = temp; // Single storage write } } ``` ### Reset Storage Slots to Zero When Possible The EVM provides a **gas refund** for clearing storage slots (i.e., writing zero to a previously non-zero slot). Although refunds are capped, this can still result in **net savings** when performing large transactions. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} contract StorageCleanup { mapping(address => uint256) public balances; function withdrawAll() external { uint256 amount = balances[msg.sender]; require(amount > 0, "No balance"); // Clear the slot balances[msg.sender] = 0; // Transfer or other business logic // ... } } ``` ### Use Structs Wisely Grouping related variables into **structs** can help you manage your data more systematically. However, remember that each struct field occupies storage in **256-bit slots**, and you can still leverage the same packing principles within a struct. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} struct UserInfo { uint8 age; uint64 score; bool isActive; // Note: reordering could pack these into fewer slots } contract ManageUsers { mapping(address => UserInfo) public users; function setUserInfo(uint8 _age, uint64 _score, bool _active) external { // Single store operation if we read the struct first into memory, modify it, then write once UserInfo memory tempUser = users[msg.sender]; tempUser.age = _age; tempUser.score = _score; tempUser.isActive = _active; users[msg.sender] = tempUser; } } ``` ## Summary Optimizing storage in Solidity smart contracts not only **reduces** gas costs for your end-users but also contributes to the overall **efficiency** of the peaq network. By adopting these best practices—using mappings, packing data, leveraging events instead of persistent storage, and resetting unused slots—you can ensure your dApps remain economical and performant. **Key Takeaways**: * **Mappings** often outperform arrays, especially for sparse data. * **Data Packing** aligns smaller data types to minimize wasted storage space. * **Events** are a cheaper alternative to on-chain storage if you only need data for off-chain retrieval. * **Minimizing Writes** to contract storage lowers gas costs significantly. * **Storage Slot Cleanup** can yield gas refunds and improve contract efficiency. As you develop on the peaq network's EVM, keep these strategies in mind to create secure, cost-effective, and efficient smart contracts. By following these guidelines, you will **optimize** your contracts' storage usage, reducing costs and enhancing the user experience for your dApps. # Writing Data Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contract-storage/writing-data ## What is Smart Contract Storage? Smart contract storage refers to the **persistent** data layer of an Ethereum Virtual Machine (EVM) contract. It's where your variables and state information are stored on-chain. Unlike local or in-memory variables, data stored in contract storage remains on the blockchain and can be accessed or updated by functions within your smart contract—or external actors that call those functions. ## Why Does it Matter? * **Data Persistence**: Stored data is immutable once published on-chain (except when updated through contract logic). * **Security**: The data is secured by the blockchain network's consensus mechanism. * **Accessibility**: The data is publicly accessible, ensuring transparency. ## Potential Use Cases * **Token Balances**: ERC-20 tokens keep track of users' balances and allowances in contract storage. * **User Registries**: Store user information like IDs, addresses, or roles. * **Voting Systems**: Keep track of votes, proposals, or election results. * **Marketplace Data**: Save product listings, ownership information, or transaction details. ## Prerequisites * Familiar with Solidity syntax, how to write a simple contract, and how to deploy it. * You have a working development environment set up with: * A Solidity compiler (e.g., via Hardhat, Truffle, or Remix). * Access to the peaq network's EVM endpoint (or a local test EVM). * Have access to an account or wallet (e.g., MetaMask) funded with enough test tokens (for a testnet) or actual tokens (for mainnet) to pay for gas fees. * You know how to connect to the peaq network or you're following official peaq documentation for endpoint configuration. ## Instructions Below is a step-by-step process for writing data to contract storage using **Solidity**. We'll create a simple contract that demonstrates both storing a single variable and mapping key-value pairs. ### 1. Define the Contract ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract DataStorage { // Simple storage variable uint256 public storedNumber; // Mapping to store addresses and their associated data mapping(address => string) public userData; /** * @dev Constructor that sets an initial number * @param _initialNumber The initial number to store */ constructor(uint256 _initialNumber) { storedNumber = _initialNumber; } } ``` #### Explanation * `storedNumber` is a public **state variable** of type `uint256`. * `userData` is a public **mapping** from an Ethereum (or peaq EVM) address to a string. * State variables and mappings like these are stored in the contract's storage on-chain. ### 2. Creating Functions to Write Data We'll add two functions: 1. **`setNumber()`** - to update the stored number. 2. **`setUserData()`** - to write user-specific data into the `userData` mapping. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract DataStorage { // Simple storage variable uint256 public storedNumber; // Mapping to store addresses and their associated data mapping(address => string) public userData; /** * @dev Constructor that sets an initial number */ constructor(uint256 _initialNumber) { storedNumber = _initialNumber; } /** * @dev Updates the stored number in contract storage * @param _newNumber The new number to be stored */ function setNumber(uint256 _newNumber) external { storedNumber = _newNumber; } /** * @dev Writes or updates user data in the mapping * @param _data The data string associated with the caller’s address */ function setUserData(string calldata _data) external { userData[msg.sender] = _data; } } ``` #### Explanation * When `setNumber` is called, it changes the `storedNumber` variable. * When `setUserData` is called, it sets or updates the string in the mapping for the calling address (`msg.sender`). ### 3. Deploying the Contract * **Configure your deployment environment** * If using Hardhat, update the `hardhat.config.js` (or `truffle-config.js` for Truffle) to point to a peaq EVM endpoint (e.g. [https://quicknode.peaq.xyz](https://quicknode.peaq.xyz) ) * If using Remix, simply select the appropriate network in the environment settings. * **Deploy** * In Hardhat: `npx hardhat run scripts/deploy.js --network yourPeaqNetworkConfig` * In Truffle: `truffle migrate --network yourPeaqNetworkConfig` * In Remix: Use the “Deploy” button after selecting the correct network and providing the constructor argument `_initialNumber`. ### 4. Interacting with the Deployed Contract After deployment, you can write data to the contract storage: * **Update the Number** * Call the `setNumber` function with a new number. For example, `setNumber(42)`. * **Write User Data** * Call the `setUserData` function with a string. For example, `setUserData("Hello, peaq!")`. Each transaction will: * Consume gas. * Write data to the blockchain. * Persist the updated data in the contract's storage. ## Summary Writing data to a **smart contract's storage** is fundamental in building decentralized applications. By storing variables and mappings within your Solidity code, you create a transparent, tamper-resistant record of information on the blockchain. Here's what you've learned: 1. **Smart Contract Storage** * How state variables and mappings persist data on-chain. 2. **Use Cases** * Simple counters, user registries, voting systems, and more. 3. **Implementation Steps** * Defining state variables, writing setter functions, deploying to the peaq EVM, and interacting with your contract. With this knowledge, you can confidently manage on-chain data. Be mindful of **gas costs** and **storage optimization techniques**—especially for larger, more complex data structures. For further exploration, look into: * **Events** to emit data without storing it on-chain. * [Optimizing gas](/peaqchain/build/basic-operations/smart-contract-storage/optimizing-storage) with `structs` or storage packing. * [Upgradable Contracts](/peaqchain/build/basic-operations/smart-contracts/upgradable-smart-contracts) for long-term maintenance. # Build Smart Contract Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/build-smart-contract Welcome to this comprehensive guide on deploying your first smart contract on an EVM-compatible blockchain network. A **smart contract** is a self-executing piece of code stored on the blockchain, where the contract's terms are directly written into code. This means that when the predefined conditions are met, the contract automatically executes without the need for intermediaries, ensuring secure, transparent, and trustless interactions. In this tutorial, you'll learn how to craft a smart contract using the `Solidity` language and deploy using **Remix IDE**, **Hardhat**, or **Foundry**. By understanding the inner workings of these contracts and how they operate on a decentralized network, you'll be well-equipped to build robust and efficient applications. By the end of this guide, you'll have developed a fully functional smart contract, ready for deployment on the peaq network. ## Prerequisites * You have basic programming knowledge (JavaScript is a plus) * You have [MetaMask wallet](/peaqchain/build/getting-started/get-test-tokens#create-evm-wallet) installed * You have access to [Remix](https://remix.ethereum.org/) * You have access to a code editor like Visual Studio Code * [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started) or [Foundry](https://getfoundry.sh/introduction/installation/) installed (for local development) * [Node.js](https://nodejs.org/en/download/package-manager) is installed * [Python](https://www.python.org/downloads/) is installed ## Remix The most simple way to write a smart contract is through a web-based IDE (Integrated Development Environment) called **Remix**. The software has been specifically tailored to the writing, development, and deployment of Solidity contracts. It contains certain features such as smart contract compilation and interaction with previously deployed contracts. Remix is the perfect platform for quick **plug and play** to get started with blockchain development. ### 1. Open Remix IDE Head over to the [Remix](https://remix.ethereum.org/) webpage. ### 2. Create a new File In the left-hand file explorer panel, click on the New File icon to generate a new file where the smart contract code will be written. Name the file `SimpleStorage.sol` (`.sol` being the standard extension for Solidity code). ### 3. Write the Code Here we will create a simple contract that stores and retrieves a number. Please copy and paste the following code in the file that you just created. ```Solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 private data; function set(uint256 _data) public { data = _data; } function get() public view returns (uint256) { return data; } } ``` #### Understanding the code **1. File License Declaration:** `// SPDX-License-Identifier: MIT` * Specifies the **license** under which the code is distributed, in this case **MIT**. It is important for open-source compliance and avoids compilation warnings. **2. Pragma Directive:** `pragma solidity ^0.8.0;` * Specifies the **version** of solidity that is required for the code. The `^` symbol indicates compatibility with versions 0.8.0 and above (up to but not including 0.9.0). **3. Contract Declaration:** `contract SimpleStorage { … }` * A **contract** is similar to a class in typical object oriented programming paradigms. It encapsulates state variables and functions. The contract name *SimpleStorage* is defined by the user and should represent its purpose. **4. State Variable:** `uint256 private data;` * State variables are stored **directly** on the blockchain, allowing for persistence and global access. `uint256` represents the variable type. In this particular case it is an unsigned integer that can hold large non-negative numbers. The identifier `private` restricts the access of the variable within the contract only. **5. Set Function** `function set(uint256 _data) public { … }` * Functions are used in Solidity to **interact** with the contract. The `set` function here allows users to update the value of `data`. The keyword `public` means that the function is accessible to everyone (any address can call it). The keyword parameter `_data` is used to represent the data a user sent to the contract. By convention these variables start with an underscore. **6. Get Function** `function get() public view returns (uint256) { … }` * Getter function that returns the value of the variable `data` that is stored on the blockchain. The keyword `view` is used to restrict the function to be **read-only** so it does not modify the state of the chain. `returns` specifies what type of value the function will return to the user. With that example, we now have a simple contract that stores an integer on a blockchain. For more complex examples and explanations of what you can do with the **Solidity programming language** please look at their [documentation](https://docs.soliditylang.org/en/v0.8.28/). The following two sections will show you how to set up a local **JavaScript** and **Python** environment to write smart contracts. The deployment and interaction of the written contract is done in the subsequent sections. ## Hardhat Remix is not the only tool you can use to deploy your smart contracts. A popular tool used in local Ethereum development is **Hardhat**. It allows for the development, compilation, and interaction of smart contracts on a local IDE. Similar to Remix in many ways, Hardhat does allow for more **flexibility** and **configurability**. If you would like to learn more please read about [Hardhat](https://hardhat.org/docs). ### Install & Initialize Hardhat #### 1. Install Hardhat * **Create a New Repository and Install Hardhat:** * Open your terminal and navigate to your project directory. * Run the following command to install Hardhat: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install --save-dev hardhat ``` #### 2. Initialize Hardhat * Initialize a new Hardhat project using: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat init ``` * Select **Create an empty hardhat.config.js** when prompted. #### 3. Install Hardhat Toolbox * Install the toolbox dependency for testing, contract interaction, and deployment: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @nomicfoundation/hardhat-toolbox ``` #### 4. Set Up Project Structure * Organize your project with the following standard folder structure: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} project-directory/ │ ├── contracts/ # Solidity files (e.g., SimpleStorage.sol) ├── ignition/modules/ # Deployment scripts ├── node_modules/ # Installed dependencies ├── scripts/ # Interaction scripts ├── .gitignore # Files to exclude from Git ├── hardhat.config.js # Hardhat configuration file ├── package.json # Node.js project configuration ├── package-lock.json # Dependency lock file └── .env # Environment variables (created in the next step) ``` ### Configure .env and Hardhat #### 1. Create `.env` File * In the root directory, create a `.env` file to store sensitive data. * Install dotenv to enable environment variable support: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install dotenv ``` * Add the following variables to the `.env` file: ```jsx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} RPC_URL="rpc_url" PRIVATE_KEY="wallet_private_key" ``` * Replace `rpc_url` with the [RPC URL](/peaqchain/build/getting-started/connecting-to-peaq#public-rpc-urls) of the peaq/agung test network. * Replace `wallet_private_key` with your wallet's private key. #### 2. Update hardhat.config.js * Update the `hardhat.config.js` file to include the network configuration * Networks added here are used in the deployment script so it knows what chain to send the bytecode to. The example below shows the test network agung. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} require("@nomicfoundation/hardhat-toolbox"); require('dotenv').config(); const RPC_URL = process.env.RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; module.exports = { solidity: "0.8.28", networks: { agung: { url: RPC_URL, chainId: 9990, // for agung accounts: [`0x${PRIVATE_KEY}`], }, }, }; ``` ### Create the SimpleStorage Smart Contract #### Write the Contract * Inside the `contracts/` folder, create a file named `SimpleStorage.sol`. * Copy the same code used above in the Remix contract and paste it into this newly created file: ```Solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 private data; function set(uint256 _data) public { data = _data; } function get() public view returns (uint256) { return data; } } ``` You have successfully created 2 separate environments that were used to write a smart contract. Please follow the next pages to see how to **deploy** and **interact** with the contract written on this page. ## Foundry Prefer Foundry? Use these steps to set up, configure, and compile with Foundry on peaq/agung. ### 1. Install Foundry * Follow the official instructions: [https://getfoundry.sh/introduction/installation/](https://getfoundry.sh/introduction/installation/) ### 2. Initialize a project ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} forge init my-project cd my-project ``` ### 3. Configure `foundry.toml` Create or update `foundry.toml` in the project root: ```toml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [profile.default] src = "src" out = "out" libs = ["lib"] evm_version = 'london' ``` Note: The `evm_version` is set to `london` for peaq-like networks, due to the fork used on the substrate-based network. ### 4. Add the example contract Create `src/SimpleStorage.sol` and paste the same contract used above: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; contract SimpleStorage { uint256 private data; function set(uint256 _data) external { data = _data; } function get() external view returns (uint256) { return data; } } ``` ### 5. Compile ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} forge build ``` Make sure to remove the autogenerated script that references the foundry native test contract to use the `SimpleStorage.sol` instead. # Deploy Smart Contract Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/deploy-smart-contract In the previous section you learned how to create a simple smart contract to store and retrieve an integer from the blockchain. Now it's time to **deploy** this contract to peaq network. Deployment is the process of broadcasting your contract's **bytecode** to the blockchain - making it possible to interact with. This guide will show you how to deploy using **Remix**, **Hardhat**, and **Foundry**, three popular tools for EVM smart contract development. ## Prerequisites * Completion of the [Build Smart Contract](/peaqchain/build/basic-operations/smart-contracts/build-smart-contract) guide. * Access to a [MetaMask wallet](/peaqchain/build/getting-started/get-test-tokens#create-evm-wallet) connected to the peaq/agung network. ## Instructions ### Deploying with Remix #### 1. Compile the Smart Contract * **Open the Compile Sidebar in Remix:** * Select the **Solidity Compiler** from the left sidebar. * **Compile the Contract:** * Ensure the Solidity version matches the pragma directive in your contract (e.g., `^0.8.0`). * Click the **Compile SimpleStorage.sol** button. * Ensure a green checkmark has appeared after successful compilation. #### 2. Deploy the Contract * **Open the Deploy & Run Transactions Sidebar:** * Select the **Deploy & Run Transactions** option in the Remix sidebar. * **Select the Environment:** * From the **Environment** dropdown, select **Injected Provider - MetaMask**. * Confirm that MetaMask is connected to the appropriate network by checking the **Chain ID**. * **Deploy the Contract:** * Click the orange **Deploy** button to initiate the transaction. * A MetaMask popup will appear for transaction confirmation. Ensure you have sufficient `$AGUNG/$PEAQ` tokens in your wallet for gas fees. #### 3. View Deployment Details Once the transaction is confirmed, the Remix console will display deployment logs, including: * **Transaction Hash:** Unique identifier for the transaction sent to the blockchain. The hash allows you to track and verify the transaction on the chain. It acts like a deployment receipt. You can use this transaction hash in an explorer to view detailed information about the transaction. * **Block Number:** The number of the block your transaction was included on-chain. Blocks are used to group transactions together and the number is used to indicate where your contract deployment was recorded. * **Gas Used:** The amount of computational effort that the blockchain required to process your transaction. Deploying a contract consumes gas since it involves writing the bytecode on-chain. Gas is used to show how resource-intensive the deployment was. * **Contract Address:** The unique address assigned to your deployed smart contract on the blockchain. This address allows you and other users to interact with your contract. It is the location of the contract on the blockchain network. ### Deploying with Hardhat #### 1. Create a Deployment Script * **Set Up the Script:** * In the `ignition/modules/` folder, create a file named `SimpleStorage.js` * **Add Deployment Logic:** * Add the following code to deploy your `SimpleStorage` js file: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); module.exports = buildModule("SimpleStorageModule", (m) => { const SimpleStorage = m.contract("SimpleStorage", []); return { SimpleStorage }; }); ``` #### 2. Deploy the Contract * **Run the Deployment Command:** * In your terminal, navigate to the project's root directory * Execute the following command to deploy the contract to the agung test network: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat ignition deploy ./ignition/modules/SimpleStorage.js --network agung ``` * **View Deployment Output:** * Upon successful deployment, the terminal will display: * **Deployed Contract Address:** The unique address assigned to your deployed smart contract on the blockchain. Record the contract address for future interactions. ### Deploying with Foundry Use Foundry to deploy using `forge create` or a script. #### 1. Prepare environment Create a `.env` in your project root with: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} RPC_URL="https://..." # peaq or agung RPC URL PRIVATE_KEY="" # deployer key ``` #### 2. Option A — `forge create` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} source .env forge create src/SimpleStorage.sol:SimpleStorage \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" \ --broadcast ``` #### 3. Option B — Deployment script Create `script/DeploySimpleStorage.s.sol`: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity 0.8.25; import {Script} from "forge-std/Script.sol"; import {SimpleStorage} from "../src/SimpleStorage.sol"; contract DeploySimpleStorage is Script { function run() external returns (SimpleStorage) { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); SimpleStorage c = new SimpleStorage(); vm.stopBroadcast(); return c; } } ``` Run it: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} source .env forge script script/DeploySimpleStorage.s.sol:DeploySimpleStorage \ --rpc-url "$RPC_URL" \ --broadcast ``` #### Agung note If gas estimates are too low on Agung, append: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} --gas-estimate-multiplier 300 --slow ``` ## Post-Deployment Validation **Verify Deployment on Blockchain Explorer:** * Use the [peaq](https://peaq.subscan.io/) or [agung](https://agung-testnet.subscan.io/) block explorer to search for your transaction hash or the deployed smart contract address. * Confirm that your contract is successfully deployed. After confirming that the transaction has been submitted and the smart contract address is valid, please follow our next tutorial which shows how to **interact** with the Deployed Contract. # Interact with Smart Contract Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/interact-with-smart-contract Now that your contract is deployed on the blockchain, the next step is to **interact** with it. Interacting with a smart contract involves calling its functions to read or modify its state. In this guide you will learn how to interact with your contract using **Remix**, **JavaScript**, and **Python**. ## Prerequisites * Successful smart contract deployment. * The **ABI** and **contract address** from the previous deployment are recorded. Generated after compilation and deployment. * Funded wallet connected to the appropriate network (peaq/agung). ## Instructions ### Remix #### 1. Access the Deployed Contract * In Remix, navigate to the **Deployed Contracts** section in the **Deploy & Run Transactions** sidebar. * You'll see your deployed contract listed, with an interactive UI displaying the contract's functions. #### 2. Perform Write Operations * In the Deployed Contracts interface, locate the **set** function. * Enter an integer as an input field (e.g. `10`) and click the set button. * A MetaMask popup will be triggered to confirm the transaction. Confirm the transaction and wait for it to be appended on-chain. * After completion the new value will be stored on the blockchain. #### 3. Perform Read Operations * Locate the **get** function in the Deployed Contracts drop down in the **Deploy & Run Transactions** tab. * Click the **get** button to fetch the current value stored in the contract. * The result will appear below the function, showing the updated value (e.g., `10`). ### JavaScript #### 1. Create an Interaction Script * Inside the `scripts/` directory of your project, create a new file named `interact.js` * Add the following script to interact with your deployed contract: ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const contractAddress = "YOUR_CONTRACT_ADDRESS"; // Replace with your contract address const ContractArtifact = require('../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'); async function main() { const [deployer] = await ethers.getSigners(); const contract = new ethers.Contract(contractAddress, ContractArtifact.abi, deployer); // Perform a read operation const storedValue = await contract.get(); console.log('Stored Value:', storedValue.toString()); // Perform a write operation const tx = await contract.set(10); // Setting value to 10 await tx.wait(); // Wait for transaction to complete // Verify the new value const updatedValue = await contract.get(); console.log('Updated Value:', updatedValue.toString()); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` #### 2. Execute the Interaction Script * In the root directory of your program run the cmd to execute the code written above. The code will deploy the contract on the agung network as defined in the `hardhat.config.js` file. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat run scripts/interact.js --network agung ``` 🎉 Congratulations! You have successfully written, deployed, and interacted with a smart contract! Now let's interact with this same contract, but now using another language - **Python**. ### Python #### 1. Install Dependencies * `web3` - Allows the interaction with EVM deployed contracts in Python. * `python-dotenv` - Provides basic protection for secret variables. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install web3 pip install python-dotenv ``` #### 2. Create Python File * Inside the `scripts/` directory of your project, create a new file named `interact.py` * Add the following script to interact with your deployed contract: ```py Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import json import os from web3 import Web3 from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() # Access environment variables RPC_URL = os.getenv('RPC_URL') PRIVATE_KEY = os.getenv('PRIVATE_KEY') CONTRACT_ADDRESS = "" # Your deployed contract address ABI = './artifacts/contracts/SimpleStorage.sol/SimpleStorage.json' # Path to Hardhat artifact JSON file def write(web3, contract, my_number): # Account and transaction details from_account = web3.eth.account.from_key(PRIVATE_KEY) # object representation of your account to sign transactions chain_id = web3.eth.chain_id # rpc_url chain id that is connected to web3 gas_price = web3.eth.gas_price # get current gas price from the connected network nonce = web3.eth.get_transaction_count(from_account.address) # obtain nonce from your account address # Obtain the estimated gas based on the expected transaction and your wallet address tx = contract.functions.set(my_number) estimated_gas = tx.estimate_gas({'from': from_account.address}) # Build transaction object tx = tx.build_transaction({ 'chainId': chain_id, 'gas': estimated_gas, 'gasPrice': gas_price, 'nonce': nonce }) # Sign the transaction from your account signed_tx = from_account.sign_transaction(tx) # Send the transaction tx_receipt = web3.eth.send_raw_transaction(signed_tx.rawTransaction) tx_hash = web3.to_hex(tx_receipt) # Wait for it to be added on chain receipt = web3.eth.wait_for_transaction_receipt(tx_hash) # Uncomment below to see completed transaction receipt # print("Receipt: ", receipt) def main(): # Create a Web3 connection instance web3 = Web3(Web3.HTTPProvider(RPC_URL)) # Load JSON to get ABI with open(ABI) as file: contract_json = json.load(file) contract_abi = contract_json['abi'] # Use contract address and abi to obtain an instance of the network deployed contract contract = web3.eth.contract(address=CONTRACT_ADDRESS, abi=contract_abi) # Write function is more complex as we have to build the transaction. # Put into a separate function my_number = 10 write(web3, contract, my_number) # Perform a simple read greeting = contract.functions.set().call() print(greeting) # change it again my_number2 = 20 write(web3, contract, my_number2) # Observe how the number has been changed print(contract.functions.set().call()) main() ``` #### 3. Execute the Interaction Script * To execute the code above you can use the following script: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python scripts/interact.py ``` After doing so, the code inside the Python file interacts with the SimpleStorage contract you made earlier! Now you understand how to get a smart contract on chain and interact with it. However, there are ways to create **upgradeable smart contracts** which are talked about in the next section. # Upgradable Smart Contracts Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/upgradable-smart-contracts Smart contracts are immutable by nature, meaning their code cannot be altered after deployment. While this immutability ensures security and trust in a blockchain, it also presents challenges when you need to fix bugs, add new features, or adapt to evolving regulations. **Upgradable smart contracts** address these challenges by allowing developers to update the logic of a deployed contract without affecting its state or address. This guide will show you how to build and deploy upgradable smart contracts using **OpenZeppelin's Upgrades Plugin** and **Hardhat** for deployment. ## Prerequisites * You have followed the previous tutorials and are confident on learning about new contract types. * You have an understanding of how to deploy contracts using Hardhat. * JavaScript programming language experience. * Understanding about upgradable smart contracts. ## Why Upgradable Smart Contracts? Traditional smart contracts are inherently limited in flexibility. Once deployed, they are **immutable** pieces of code residing on the blockchain. Only the state of the contract can change while the code itself remains fixed. This means that if bugs are discovered or if improvements are needed, you're left without an option to update the contract. Upgradable smart contracts address this limitation by offering **flexibility** and **adaptability** throughout the contract's lifecycle. They allow for seamless bug fixes and security patches on a deployed contract, helping to mitigate vulnerabilities and address security concerns. In an ever-evolving blockchain environment—where best practices and regulatory standards are **continuously being redefined**—using upgradable smart contracts enables you to update your code to comply with new regulations, ultimately facilitating broader adoption. ## Instructions ### Building an Upgradable Smart Contract For the creation of the upgradable smart contract we will be using **OpenZeppelin's** `deployProxy` plugin. #### 1. Install upgrades plugin * After creating a Hardhat environment you will need to download the **OpenZeppelin** package that contains the upgrade logic. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install --save-dev @openzeppelin/hardhat-upgrades ``` * Next you will need to add the following import in the `hardhat.config.js` to configure Hardhat to use this package ```jsx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // hardhat.config.js ... require('@openzeppelin/hardhat-upgrades'); ... module.exports = { ... }; ``` #### 2. Create the contract Now we need to create the contract that will be upgradable. For simplicity's sake, let us again use the `SimpleStorage.sol` contract from before. Please place this file in the `/contracts` directory of your project. The code is as follows: ```Solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 private data; // Emitted when the stored value changes event ValueChanged(uint256 _data); function set(uint256 _data) public { data = _data; emit ValueChanged(_data); } function get() public view returns (uint256) { return data; } } ``` Please note how we added an event which will be helpful later. #### 3. Write the deployment script Next, we need to deploy the contract as upgradable. This uses a bit different logic so please make sure to take special note of the code below. Rather than putting the deployment script in the *ignition/modules/* directory path, we place it in the *scripts/*. * Create a new file in the `/scripts` directory called `upgrade_storage.js`. * Paste the following code in the file: ```jsx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // scripts/upgrade_storage.js const { ethers, upgrades } = require('hardhat'); async function main () { const SimpleStorage = await ethers.getContractFactory('SimpleStorage'); console.log('Deploying SimpleStorage...'); const simple_storage = await upgrades.deployProxy(SimpleStorage, [10], { initializer: 'store' }); await simple_storage.waitForDeployment(); console.log('SimpleStorage deployed to:', await simple_storage.getAddress()); } main(); ``` #### 4. Deploy the contract * Run the following cmd to deploy the contract. For the upgradable contract we use the `npx hardhat run` cmd instead of the `ignition deploy`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat run --network agung scripts/upgrade_storage.js ``` * After successfully doing so, the deployed contract address will be displayed. Save this value so you can access the contract in the future. #### 5. Interact with the contract * **Hardhat console** will be used to interact with the smart contract we have deployed. To launch the console you can run the cmd: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat console --network agung ``` * A new terminal will open up where you can execute the following to check the deployment: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Welcome to Node.js v22.4.1. Type ".help" for more information. > const SimpleStorage = await ethers.getContractFactory('SimpleStorage') undefined > const simple_storage = await SimpleStorage.attach('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D'); undefined > (await simple_storage.get()).toString(); '10' ``` * Confirms that the contract has been properly stored and initialized at `10`. ### Upgrade previously deployed contract Let us say down the line, we want to upgrade this smart contract to have a new feature - increment the stored value. #### 1. Create a new contract * Create a new Solidity contract in the `contracts/` directory with the name `SimpleStorageV2` ```Solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorageV2 { uint256 private data; // Emitted when the stored value changes event ValueChanged(uint256 _data); function set(uint256 _data) public { data = _data; emit ValueChanged(_data); } function get() public view returns (uint256) { return data; } // new function added function increment() public { data = data + 1; emit ValueChanged(data); } } ``` Take special note of how the contract is exactly the same, except for the name of the contract and the new increment function that was added. #### 2. Upgrade previously deployed contract Use the `updateProxy` function from **OpenZeppelin** to update the contract: * Create a new file at `scripts/` called `upgrade_storagev2`. Use the following code: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // scripts/upgrade_box.js const { ethers, upgrades } = require('hardhat'); async function main () { const SimpleStorageV2 = await ethers.getContractFactory('SimpleStorageV2'); console.log('Upgrading SimpleStorage...'); await upgrades.upgradeProxy('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D', SimpleStorageV2); console.log('SimpleStorage upgraded'); } main(); ``` #### 3. Deploy upgradable contract Again use the `run` cmd to upgrade the contract from the set network. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} $ npx hardhat run --network agung scripts/upgrade_storagev2.js Upgrading SimpleStorage... SimpleStorage upgraded ``` The above will be displayed on a successful upgrade. The code has been updated to the latest version, while maintaining the same address and state from before. #### 4. Interact with the contract Again, let us use the Hardhat console to interact with the contract. Notice how we are able to call `increment()` that we added in our update. Launch the console: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx hardhat console --network agung ``` Terminal will open and execute the following: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Welcome to Node.js v22.4.1. Type ".help" for more information. > const SimpleStorageV2 = await ethers.getContractFactory('SimpleStorageV2'); undefined > const simple_storage = await SimpleStorageV2.attach('0xd9b5c9Abb57175C2f4B1fE644ED0c24bF4c3c49D'); undefined > await simple_storage.increment(); ... > (await simple_storage.get()).toString(); '11' ``` 🎉 Congratulations! You have created a smart contract and successfully **upgraded** it. Notice how the address and the state stayed the same, until the increment() function was called. Now you have more flexibility when dealing with your smart contracts. If you would like to learn more about how these updates work, please check out the [OpenZeppelin documentation](https://docs.openzeppelin.com/learn/upgrading-smart-contracts#how-upgrades-work). # Verify Smart Contract Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/verify-smart-contract In this section, we will go over the process of verifying a smart contract on peaq network using **Sourcify** or **Subscan**. Verifying your smart contract allows for readable and interpretable data (such as function calls and parameters) to be displayed **directly** in block explorers, making it easier to understand and debug interactions with the contract. ## Prerequisites * Familiar with **Block Explorers**. If not, check out the [Block Explorers](/peaqchain/build/basic-operations/block-explorers) section. * You have previously deployed a smart contract using one of the previous tutorials (e.g. `SimpleStorage.sol`). ## Method 1: Subscan Verification Subscan is peaq's block explorer, and a contract verified there shows decoded method names and parameters on every transaction. Steps: ### 1. Prepare Metadata from Remix We will be using the same [Remix IDE](https://remix.ethereum.org/) workspace that was used to deploy the `SimpleStorage.sol` contract. Once you are there: * Open up the `artifacts/build-info/hex_value` file. This contains the content we need to move to the `SimpleStorage_metadata.json` file. verify-smart-contract-1 * Copy and paste the entire `content` line as shown in the screenshot above. ### 2. Paste content into SimpleStorage\_metadata After copying this content we will need to paste it into the `SimpleStorage_metadata.json` file. * Navigate to the `SimpleStorage_metadata.json` file. * Find the `sources` object where the data for contract `SimpleStorage.sol` is stored. * Paste the content after the `license` field. verify-smart-contract-2 ### 3. Download the SimpleStorage\_metadata.json file The next step is to simply download this file to your local machine. It will be used later in the verification process. ### 4. Navigate to Subscan Now go to [peaq.subscan.io](https://peaq.subscan.io/) or [agung-testnet.subscan.io](https://agung-testnet.subscan.io/) and search for your deployed contract address. Then go to the `Contract` tab. ### 5. Verify Contract * Confirm the `Contract Address` field matches your Smart Contract to verify. * Select **Solidity (Standard-JSON-Input)** as `Compiler Type`. * Pick whether or not you would like to `Include Nightly Builds`. * Choose the `Compiler Version` that was used when you compiled your smart contract. * Upload the file that we downloaded from Remix earlier. * Finally, click the `Verify & Publish` button. verify-smart-contract-3 ### 6. Confirm Verification A proper Contract Verification will have a ✅ next to **Contract**. verify-smart-contract-4 #### Contract Information Displayed * **Contract Name**: `SimpleStorage` * **Compiler Version**: `v0.8.26+commit.8a97fa7a` * **EVM Version**: `cancun` * **Optimization**: `false` This indicates the deployed contract was compiled using Solidity version `0.8.26`, with the Ethereum Virtual Machine (EVM) set to `cancun`, and without optimization enabled. #### Contract ABI * The **ABI** (Application Binary Interface) specifies the interface of the contract. * It includes the function details such as: * **Function `get`**: * Inputs: None. * Outputs: A single `uint256` value. * Mutability: `view` (read-only). * **Function `set`**: * Inputs: A single `uint256` value (`_data`). * Outputs: None. * Mutability: `nonpayable` (modifies the state). This confirms the contract has two primary functions: * `set(uint256 _data)`: Stores a value. * `get()`: Retrieves the stored value. #### Contract Source Code The source code of the contract is displayed as it was written: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 private data; function set(uint256 _data) public { data = _data; } function get() public view returns (uint256) { return data; } } ``` * **Logic**: * `set`: Allows the user to save a `uint256` value in the contract's state. * `get`: Returns the saved value without modifying the state. #### Contract Bytecode * The **bytecode** is the compiled version of the Solidity source code. * It is what gets deployed to the Ethereum blockchain. * This bytecode matches the source code provided in the screenshot. ## Method 2: Sourcify Verification (decentralized record) **Sourcify** is a decentralized contract verification service that provides transparent and immutable verification for smart contracts. No peaq explorer reads Sourcify today, so this does not make the contract readable on Subscan; it gives you an immutable, explorer-independent record of the source. ### 1. Navigate to Sourcify Go to [sourcify.dev](https://sourcify.dev/) and click **"Verify"**. ### 2. Select Network * From the network dropdown, select **"peaq"** (Chain ID: 3338) * Ensure you're verifying on the correct network where your contract is deployed ### 3. Enter Contract Information * **Contract Address**: Enter your deployed contract address * Sourcify will automatically detect if the contract is already verified * If unverified, you'll proceed to the verification form ### 4. Upload Contract Files Choose your preferred verification method: #### Option A: Upload Source Files * **Drag and drop** or **browse** to upload your main Solidity file (e.g., `SimpleStorage.sol`) * Upload any **imported dependencies** or libraries * Maintain the **correct folder structure** as used during compilation * Include any **configuration files** (e.g., `hardhat.config.js`, `foundry.toml`) #### Option B: Upload Metadata JSON * **Hardhat users**: Upload the complete metadata from `artifacts/contracts/YourContract.sol/YourContract.json` * **Foundry users**: Upload the metadata from `out/YourContract.sol/YourContract.json` * **Remix users**: Download and upload the metadata JSON from the artifacts folder ### 5. Verify Compilation Settings Sourcify will automatically extract and verify: * **Compiler Version**: Must match the version used during compilation * **EVM Version**: Should be set to `london` (required for peaq network) * **Optimization Settings**: Enabled/disabled status and number of runs * **Source Maps**: For debugging and verification accuracy ### 6. Submit for Verification * Review the **contract details** and **compilation metadata** * Click **"Verify"** to submit your contract for verification * Sourcify will compile the contract and compare bytecode * Wait for the verification process to complete (usually takes 1-2 minutes) ### 7. Verification Results Upon successful verification: #### Perfect Match * Your contract achieves **"Perfect Match"** status * Source code is **permanently stored** on IPFS * Contract is **automatically indexed** by Sourcify * Verification data is **accessible across all Sourcify-compatible explorers** #### Partial Match * If you get a **"Partial Match"**, the core logic matches but metadata differs * Check compilation settings, especially optimization and EVM version * Consider re-verifying with exact compilation parameters ### 8. View Verified Contract After verification: * Visit the **Sourcify repository** to view your contract's source code * Subscan does not read Sourcify; verify there separately (Method 1) if you want decoded calls on peaq.subscan.io * Access the **contract's IPFS hash** for permanent source code storage * Use the **contract ABI** for frontend integration ## Summary ### Recommended approach Verify on **Subscan** ([peaq.subscan.io](https://peaq.subscan.io/) or [agung-testnet.subscan.io](https://agung-testnet.subscan.io/)). It is the explorer people use on peaq, and verification there is what turns raw call data into readable method names and parameters. ### Decentralized record Add a **Sourcify** verification ([sourcify.dev](https://sourcify.dev/)) when you want an explorer-independent, IPFS-stored record of the source with Perfect or Partial Match status. It does not show up on Subscan. # Verify Transaction Status Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/smart-contracts/verify-transaction-status After gaining an understanding of how smart contracts work and how to use blockchain explorers, verifying the **status of transactions** from these contracts becomes a straightforward process. In this section, we will focus on using the `SimpleStorage.sol` smart contract, which was created in the section: [Deploy Your First Smart Contract](/peaqchain/build/basic-operations/smart-contracts/build-smart-contract). We will use the **Subscan Explorer**, introduced in the section [Block Explorers](/peaqchain/build/basic-operations/block-explorers) to **track** and **verify** the **transactions** associated with deploying and interacting with the smart contract. This will include confirming the deployment, verifying function calls, and ensuring data was successfully stored and retrieved on-chain. ## Get Deployed Transaction hash From the contract you deployed at [Deploy Smart Contract](/peaqchain/build/basic-operations/smart-contracts/deploy-smart-contract) obtain the contract address that was deployed. We will use this to see if the operations that executed on the smart contract level have been correctly recorded on the online ledger. ## Using Subscan to Verify Smart Contract Transactions ### 1. Access Subscan Access the Subscan endpoint for the network you deployed your smart contract on: | **Network** | **Subscan Link** | | ----------- | ---------------------------------------------------------------------- | | peaq | [https://peaq.subscan.io/](https://peaq.subscan.io/) | | agung | [https://agung-testnet.subscan.io/](https://agung-testnet.subscan.io/) | ### 2. Confirming Deployment Paste your deployed smart contract in the search bar. If deployed correctly it will route you to the contract address page. ### 3. Verifying function calls Use Remix, the deployment script you created, or Hardhat console to **interact** with your smart contract. **Verify** the operations that you performed were recorded in Subscan. The following case is shown below: * User executes a deployment script which performs a **write** transaction on the network. * Wait for Subscan to be **updated** to show the smart contract transaction. * Transaction appears in the EVM Transactions table. Click the arrow to ensure the data was stored. verify-transaction-status-1 ### 4. Ensure data was successfully stored After clicking the arrow on the above transaction it will redirect you to information about the transaction. Similar to the following: verify-transaction-status-2 #### Transaction Overview * **Status:** ✅ Confirmed * The transaction has been successfully processed and included in a block on the blockchain. * **EVM Txhash:** `0xa540d982649de379fa083867a96ab4eb85fc90718dfb92af3beb29c5a26597c2` * Unique identifier (hash) for the transaction. Can be used to locate and reference this transaction across blockchain explorers. * **From:** `0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C` * Sender's Ethereum-compatible account address that initiated the transaction. * **To:** `0x12e27eabb913acf6c0D109a062ba1e266bFd8CcF` * Smart contract address that executed the function call. * **Value:** `0 PEAQ` * No PEAQ tokens were transferred as part of this transaction. * **Result:** ✅ Success * The transaction was successfully executed without errors. * **Nonce:** `851` * Number of transactions sent from the sender's address (`From`). * **Txn Fee:** `0.000000000044744704 PEAQ` * The transaction fee paid for processing this transaction on the blockchain. * **Input Data:** `set` `_data` `10` * Information about the function call executed. ## Summary Verifying the status of transactions from deployed smart contracts is an essential step in ensuring that your operations are successfully recorded on the blockchain. By using Subscan Explorer, you can confirm the **deployment** of your smart contract, **track** its interactions, and **validate** the success of each transaction. The **detailed** transaction overview, including the status, transaction hash, gas fees, and input data, provides **transparency** and **accountability** for your on-chain activities. # Submitting Transactions Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/submitting-transactions Submitting transactions is a core task when interacting with blockchains, like peaq. Transactions include **transferring funds**, **interacting with smart contracts**, or **deploying contracts**. A transaction is required each time we want to **write data** to the blockchain and it's the responsibility of the developer to construct the transaction call data. This guide provides a structured approach to submitting transactions using `ethers.js`, ensuring efficiency and security. ## Prerequisites Before following the instructions, ensure the following: 1. **Environment Setup**: * **Node.js** and **npm** (or pnpm/yarn) are installed. * Ethers.js library is installed: `npm install ethers`. 2. **Account Setup**: * You have access to a private key or a connected **wallet** (e.g., MetaMask). * Your wallet has sufficient funds for **gas** fees. 3. **Blockchain Setup**: * You know the URL of the RPC you're interacting with. * You understand the transaction details, including recipient address, value, and any data (if applicable). 4. **Security Awareness**: * Never expose your private key or mnemonic in your code. * Use environment variables for sensitive information. ## Instructions ### 1. Install and Import ethers.js In your JavaScript file, import ethers.js: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from 'ethers'; ``` ### 2. Set Up a Provider Connect to an Ethereum-compatible network: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const provider = new ethers.JsonRpcProvider(HTTPS_URL); ``` ### 3. Configure a Signer Create a wallet instance to sign transactions: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const privateKey = process.env.PRIVATE_KEY; //Please store private keys in environment variables const wallet = new ethers.Wallet(privateKey, provider); ``` ### 4. Define the Transaction Set up the transaction details: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const tx = { to: "0xRecipientAddressHere", // Replace with the recipient's address value: ethers.parseEther("0.1"), // Amount in ETH to send (converted from Ether) gasLimit: 21000, // Gas limit (for simple transfers) maxFeePerGas: ethers.parseUnits("20", "gwei"), // Max fee per gas maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // Priority fee }; ``` For interacting with smart contracts, include `data` in the transaction object. ### 5. Sign and Submit the Transaction Sign and send the transaction: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function sendTransaction() { try { const txResponse = await wallet.sendTransaction(tx); console.log("Transaction submitted:", txResponse.hash); const receipt = await txResponse.wait(); console.log("Transaction mined:", receipt); } catch (error) { console.error("Error submitting transaction:", error); } } sendTransaction(); ``` ### 6. Verify the Transaction Monitor the transaction using the `txResponse.hash` in a block explorer or programmatically check its status using the `provider.getTransactionReceipt` method. ## Notes * **Gas Estimation**: For more complex transactions, calculate gas estimates using `contract.estimateGas.methodName()`. * **Smart Contracts**: For interacting with contracts, set up the contract instance using its ABI and address: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const contract = new ethers.Contract(contractAddress, contractABI, wallet); ``` ## Putting it all Together Here's a complete boilerplate script that ties together all the steps for submitting a transaction using `ethers.js`. This script includes environment variable handling, transaction setup, and submission. ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // Import ethers.js import { ethers } from 'ethers'; require("dotenv").config(); // To use environment variables // Boilerplate Script async function main() { try { // Step 1: Set up the provider const rpcUrl = process.env.RPC_URL; // RPC URL for your blockchain if (!rpcUrl) throw new Error("RPC_URL is not defined in .env"); const provider = new ethers.JsonRpcProvider(rpcUrl); // Step 2: Configure the wallet (signer) const privateKey = process.env.PRIVATE_KEY; // Private key stored in environment variables if (!privateKey) throw new Error("PRIVATE_KEY is not defined in .env"); const wallet = new ethers.Wallet(privateKey, provider); console.log(`Connected wallet: ${wallet.address}`); // Step 3: Define the transaction const tx = { to: "0xRecipientAddressHere", // Replace with the recipient's address value: ethers.parseEther("0.1"), // Amount in ETH to send (e.g., 0.1 ETH) gasLimit: 21000, // Standard gas limit for a simple ETH transfer maxFeePerGas: ethers.parseUnits("20", "gwei"), // Max fee per gas maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // Priority fee }; console.log("Prepared transaction:", tx); // Step 4: Sign and send the transaction console.log("Sending transaction..."); const txResponse = await wallet.sendTransaction(tx); console.log(`Transaction submitted: ${txResponse.hash}`); // Step 5: Wait for transaction to be mined console.log("Waiting for transaction to be mined..."); const receipt = await txResponse.wait(); console.log("Transaction mined:", receipt); console.log(`Transaction successful! Block Number: ${receipt.blockNumber}`); } catch (error) { console.error("Error occurred:", error.message); } } // Execute the script main(); ``` ### Instructions for Using the Boilerplate Script 1. **Install Dependencies**: Ensure you have the required packages installed: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers dotenv ``` 2. **Set ESM Module:** Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` 3. **Set Up Environment Variables**: Create a `.env` file in the root directory of your project with the following keys: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} RPC_URL=https://quicknode.peaq.xyz PRIVATE_KEY=your-private-key ``` 4. **Run the Script**: Execute the script in your terminal: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} node script.js ``` 5. **Modify Transaction Details**: * Replace `0xRecipientAddressHere` with the recipient's address. * Adjust `value`, `maxFeePerGas`, and `maxPriorityFeePerGas` as needed. # Fireblocks Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/wallets/fireblocks **Type:** Custodian Wallet 1. Go to: [https://console.fireblocks.io/v2/welcome/login](https://console.fireblocks.io/v2/welcome/login) 2. Login 3. Go to Vaults 4. Select the vault you want to create the peaq wallet address in 5. Click create wallet 6. Search “PEAQ” 7. Click “Create Wallet” # Ledger Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/wallets/ledger **Type:** Hardware Wallet ## Instructions: 1. Set up your Ledger device and install Ledger Live on your computer: [https://support.ledger.com/article/4404389503889-zd](https://support.ledger.com/article/4404389503889-zd). 2. Connect your Ledger Device to your computer and install peaq app on your ledger device through Ledger Live: [https://support.ledger.com/article/4404382258961-zd](https://support.ledger.com/article/4404382258961-zd). 3. Set up Metamask wallet and add peaq network to it by following this instruction: [Create Metamask wallet and add peaq mainnet.](/peaqchain/build/getting-started/get-test-tokens#create-evm-wallet) 4. Go to Metamask and click on the down arrow next to your account name at the top of the screen: ledger-peaq-1 5. Click **Add account or hardware wallet** ledger-peaq-2 6. Next, click **Add hardware wallet** ledger-peaq-3 7. Now, go back to your connected Ledger device, unlock it, and select peaq app ledger-peaq-4 8. Once selected click the right button until you see **Open application**, then click both buttons on the device ledger-peaq-5 9. Click right button until you see **Expert mode: disabled** ledger-peaq-6 10. Click both buttons to enable Expert mode, this is how your screen will look after a successful change ledger-peaq-7 11. Now go back to your Metamask window, select Ledger, and then click **Continue** ledger-peaq-8 12. Next, select your Ledger device in the pop-up window and click **Connect** ledger-peaq-9 13. After that you will see this screen, we’ve previously added Ledger accounts to this Metamask, that's why we have non-zero balances in accounts 1 & 2. In your case, all accounts should display a 0 balance. Select any account and click **Unlock** (we've selected account 3). ledger-peaq-10 ## Receive \$PEAQ tokens with Ledger + MetaMask 1. This is your Ledger account, which we just created. Now we will copy the address, to send some \$PEAQ tokens to it. ledger-peaq-11 2. Once tokens are received you'll see them in the interface ledger-peaq-12 ## Send \$PEAQ tokens with Ledger + MetaMask 1. Now, let's try to send some PEAQ to another address, click **Send** ledger-peaq-13 2. Paste the address you would like to send $PEAQ tokens to and enter the amount in $PEAQ, then click **Next** ledger-peaq-14 3. Double-check transaction details and click **Confirm** ledger-peaq-15 4. This is the screen you’ll see next ledger-peaq-16 5. Verify and send the transaction on your Ledger (double-check all of the transaction details on your Ledger device screen and click Accept and send by clicking both buttons) # SAFE Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/wallets/safe **Type:** MultiSig Wallet ## Create new Safe Account 1. Go to [https://safe.peaq.xyz/welcome](https://safe.peaq.xyz/welcome), Click **Connect wallet** safe-peaq-1 2. Choose your wallet safe-peaq-2 3. For tutorial purposes we will be using Metamask 4. Make sure peaq is selected as a network and you’re using the correct account safe-peaq-3 5. Press **Continue with MetaMask** safe-peaq-4 6. Make sure peaq is selected as a network and choose the name for your Safe safe-peaq-5 7. Add additional owners if needed. For each additional owner: * Click Add new owner * Give the Owner a Name * Enter Owner's Address * Select how many owners will be required to confirm a transaction * Press Next safe-peaq-6 8\. Click **Create** and confirm the transaction. You will need a small amount of \$PEAQ to sign the tx. safe-peaq-7 ## Receive \$PEAQ tokens with SAFE 1. Copy your Safe address on peaq safe-peaq-8 2. Send \$PEAQ tokens from your Metamask or any other EVM wallet to this address safe-peaq-9 ## Send \$PEAQ tokens with SAFE 1. To send PEAQ, click **Send** safe-peaq-10 2. Enter the recipient and the amount, then click **Next** safe-peaq-11 3. Review details of your transaction and click **Execute** safe-peaq-12 4. After clicking submit you will have to sign the transaction with your signer wallet. Depending on the threshold policy used, the transaction will either be executed immediately or require additional signatures from other signers of the Safe 5. This is what you’ll see once the transaction is signed by every signer needed safe-peaq-13 # Checking Balances Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/working-with-wallet-addresses/checking-balances The following explains how to retrieve wallet balances on the peaq network using `ethers.js`. Developers can often reuse ethers-based approaches with the right configurations. If direct on-chain queries are difficult, we'll also explore how to fall back to `peaq.subscan.io`, a web-based explorer that allows you to look up balances via an HTTPS GUI. ## Prerequisites * Basic understanding of JavaScript and Node.js. * You have `ethers.js` installed and know how to initialize a provider. * Possess a target wallet address for which you want to check the balance. * Have access to a running peaq node's RPC endpoint or a compatible provider. * Understand how to use peaq.subscan.io as a fallback. ## Instructions ### 1. Set Up Your Environment: Ensure you have `node` and `npm` installed, then run: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers ``` ### 2. Initialize the Provider: In your JavaScript file (e.g., `checkBalance.js`), import **ethers** and instantiate a provider. Point it to the RPC endpoint of the peaq network you're connecting to (this might be a testnet or mainnet endpoint—adjust accordingly): ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from "ethers"; // Recommended / Example endpoint: replace with your chosen peaq RPC URL const peaqProvider = new ethers.JsonRpcProvider("https://quicknode.peaq.xyz"); ``` ### 3. **Fetch the Wallet Balance**: Use the provider's `getBalance` method, passing in the wallet address. The address should be an Ethereum-compatible format (e.g., 0x...): ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function getPeaqBalance(address) { try { const balanceBigInt = await peaqProvider.getBalance(address); const balanceEth = ethers.formatEther(balanceBigInt); console.log(`Balance of ${address}: ${balanceEth} PEAQ`); } catch (error) { console.error('Error fetching balance:', error); console.log('Falling back to peaq.subscan.io...'); } } // Example usage: const myAddress = "0xYourPeaqAddress"; getPeaqBalance(myAddress); ``` ### 4. Fallback to peaq.subscan.io If the direct query via ethers fails (due to network incompatibilities at the time of querying), you can manually check the balance using the peaq Subscan explorer: * Navigate to: [peaq Subscan](https://peaq.subscan.io/) (mainnet), [agung Subscan](https://agung-testnet.subscan.io) (testnet) * Enter the **wallet address** into the search bar. * Press "Enter" or click the search icon. * View the displayed account information and check the reported **balance**. ### 5. Interpreting the Results Whether you retrieved the balance programmatically via `ethers.js` or via `peaq.subscan.io`, you'll see the current token holdings associated with that address. Make sure you understand the units (often the smallest unit might be displayed, and you may need to convert them to a more human-readable format). ## Summary Checking **wallet balances** on the peaq network can be attempted through `ethers.js` if the network is EVM-compatible and you have the correct provider endpoint. In the event of issues, the `peaq.subscan.io` explorer provides a simple GUI fallback. By combining these approaches, developers and users can confidently monitor and verify the holdings in their peaq wallets. # Create and Manage Wallet Addresses Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/working-with-wallet-addresses/create-and-manage This guide is intended for developers who need to dynamically generate and manage **cryptocurrency wallet addresses** using the `ethers.js` library. Whether you are developing a decentralized application (dApp), implementing a user management system that provisions wallets for new sign-ups, or integrating with a service such as the Particle Network, this guide will demonstrate how to securely and efficiently **create** and **manage** **EVM-compatible** wallets within your JavaScript or TypeScript code. It outlines the prerequisites, provides detailed step-by-step instructions, and concludes with best practices for sustainable wallet management. ## Prerequisites * **Basic JavaScript/TypeScript knowledge:** You should know how to write and run JavaScript code in a Node.js environment. * **Familiarity with Node.js tooling:** You know how to install NPM packages and understand basic module imports. * **Ethers.js installed:** You have ethers.js available as a dependency in your project. * **Particle Network or similar integration:** If you're using Particle Network or another platform, you should have access to their SDK or integration documentation. This guide assumes you know how to authenticate and interact with that platform's services. * **Secure storage setup:** You have a strategy for securely storing private keys, such as an environment variable manager, a hardware security module (HSM), or a dedicated secret management service. ## Instructions ### 1. Generating a New Wallet Address `ethers.js` makes it easy to generate a new wallet address with a corresponding private key. The most straightforward method is to create a random wallet: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Wallet } from "ethers"; // Create a new random wallet const wallet = Wallet.createRandom(); console.log("New Wallet Address:", wallet.address); console.log("Wallet Private Key:", wallet.privateKey); ``` **Key points:** * `wallet.address` provides the **public** address. * `wallet.privateKey` is extremely sensitive and should be stored **securely**. * By default, `Wallet.createRandom()` uses a secure RNG to generate entropy. ### 2. Encrypting and Storing Wallets When managing a user's wallet, treat the private key as a secret that must be kept safe. `ethers.js` can **encrypt** wallets with a password: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} (async () => { const password = "superstrongpassword"; const encryptedJson = await wallet.encrypt(password); // Store this encrypted JSON in a secure database or secure storage solution console.log("Encrypted Wallet JSON:", encryptedJson); })(); ``` **Key points:** * The `encrypt` method returns a JSON Keystore V3 object. * This object can be safely stored and later **decrypted** using the password. * Consider integrating secure storage solutions such as AWS KMS, GCP KMS, or Vault for enterprise-level security. ### 3. Restoring an Existing Wallet To restore a wallet from an encrypted JSON, decrypt it with the **password**: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} (async () => { const password = "superstrongpassword"; const encryptedJson = "THE ENCRYPTED JSON FROM YOUR STORAGE"; const restoredWallet = await Wallet.fromEncryptedJson(encryptedJson, password); console.log("Restored Wallet Address:", restoredWallet.address); })(); ``` **Key points:** * The `fromEncryptedJson` method returns a fully functional Wallet instance. * Use this method whenever you need to perform transactions, sign messages, or display the user's address. ### 4. Integrating with Particle Network or Other Services If you are managing multiple user wallets and need to integrate with services like Particle Network for authentication or transaction relays, consider: * **User Authentication:** Leverage Particle Network's SDK to authenticate users and link their sessions to a particular wallet. * **Dynamic Wallet Creation:** On user registration or login, generate a new wallet on the fly and store the encrypted JSON associated with that user's account in your secure database. * **Transaction Management:** Use ethers.js methods (`wallet.sendTransaction`, `wallet.signTransaction`) to handle transactions programmatically. Particle Network or similar services may offer APIs to help broadcast these transactions or manage gas payments. Example snippet for integration logic (pseudo-code): ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function handleNewUserRegistration(userId, particleInstance) { // Authenticate user via Particle Network (pseudo-code) const userSession = await particleInstance.authenticateUser(userId); // Generate a new wallet const wallet = Wallet.createRandom(); // Encrypt and store it const encryptedJson = await wallet.encrypt(process.env.WALLET_ENCRYPTION_PASSWORD); await storeEncryptedWalletForUser(userId, encryptedJson); // Associate wallet address with user session in Particle Network await particleInstance.registerWalletAddress(userSession, wallet.address); } ``` **Key points:** * Always handle private keys and encrypted data within **secure** and **trusted** environments. * Consider **role-based access controls** (RBAC) and never expose raw private keys to the client side. ## Summary Creating and managing EVM-type wallet addresses with `ethers.js` is straightforward once you understand the building blocks: **generating a wallet**, **encrypting and storing keys**, and **restoring wallets** for future transactions. By combining these practices with secure storage solutions and optional integrations with services like Particle Network, you can confidently manage a dynamic userbase and ensure their funds remain secure. In the long run, following best practices for encryption, secure storage, and careful integration with authentication services will help you maintain trust, reliability, and seamless user experiences in your decentralized applications. # Multi-Sig Source: https://docs.peaq.xyz/peaqchain/build/basic-operations/working-with-wallet-addresses/multi-sig **Multi-signature** (multi-sig) wallets provide an added layer of security and decentralization by requiring **multiple approvals** for transactions. [Gnosis Safe](https://docs.safe.global/sdk/overview) is a widely used and trusted solution for implementing multi-sig wallets on EVM-compatible blockchains like the peaq network. This guide walks you through **creating a multi-sig wallet programmatically** using Gnosis Safe contracts and demonstrates some basic functions such as adding owners and executing transactions. ## Prerequisites * Basic knowledge of blockchain development and JavaScript/TypeScript. * Familiar with interacting with smart contracts using `ethers.js`. * Have access to a deployed instance of Gnosis Safe contracts on the peaq network or are able to deploy them yourself. * Have an RPC URL for the peaq network. * Have private keys for multiple signers to interact with the wallet. ### Instructions ### 1. Set Up Your Environment * Install the necessary dependencies: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers @gnosis.pm/safe-core-sdk @gnosis.pm/safe-ethers-lib dotenv ``` * Set ESM Module: Add the following to your `package.json` to allow for ESM modules. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "type": "module", ``` * Configure a `.env` file with the following variables: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} RPC_URL=https://quicknode.peaq.xyz PRIVATE_KEY=your_private_key_here ``` ### 2. Connect to the peaq Network Import dependencies and initialize the provider and signer: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers } from 'ethers'; import Safe from '@gnosis.pm/safe-core-sdk'; import SafeEthersLib from '@gnosis.pm/safe-ethers-lib'; import dotenv from 'dotenv'; dotenv.config(); const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); ``` ### 3. Deploy a New Multi-Sig Wallet Use the Gnosis Safe SDK to create a new multi-sig wallet: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const createSafe = async () => { const safeFactory = await Safe.SafeFactory.create({ ethAdapter: new SafeEthersLib.EthersAdapter({ ethers, signer }), }); const owners = [ '0xAddress1', '0xAddress2', '0xAddress3' ]; const threshold = 2; // Number of approvals required const safeAccountConfig = { owners, threshold }; const safe = await safeFactory.deploySafe({ safeAccountConfig }); console.log(`Multi-sig wallet deployed at: ${safe.getAddress()}`); return safe; }; createSafe(); ``` ### 4. Add an Owner Propose a transaction to add a new owner: ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const addOwner = async (safeAddress, newOwner, safe) => { const safeTransaction = await safe.createAddOwnerTx({ ownerAddress: newOwner, threshold: 2, }); const txHash = await safe.executeTransaction(safeTransaction); console.log(`Transaction hash: ${txHash}`); }; addOwner(safe.getAddress(), '0xNewOwnerAddress', safe); ``` ### **5. Execute a Transaction** Propose and execute a basic transaction (e.g., sending PEAQ): ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const executeTransaction = async (safeAddress, recipient, amount) => { const safe = await Safe.default.create({ ethAdapter: new SafeEthersLib.EthersAdapter({ ethers, signer }), safeAddress, }); const safeTransactionData = { to: recipient, value: ethers.parseEther(amount), // Amount in PEAQ data: '0x', }; const safeTransaction = await safe.createTransaction({ safeTransactionData }); const txHash = await safe.executeTransaction(safeTransaction); console.log(`Transaction executed. Hash: ${txHash}`); }; executeTransaction(safe.getAddress(), '0xRecipientAddress', '0.1'); ``` ## Summary Using **Gnosis Safe contracts** on the peaq network allows you to leverage the security of multi-signature wallets for DePIN projects. This guide demonstrated how to programmatically create a wallet, manage owners, and execute transactions. With these basics, you can **secure** your **assets** and **operations** on the peaq network with confidence. Expand further by integrating Gnosis Safe with front-end interfaces or automated scripts for enhanced usability and functionality. # Deploy your DePIN Source: https://docs.peaq.xyz/peaqchain/build/first-depin/deploy-your-depin After your DePIN is fully integrated with the peaq network—complete with machine wallets, DIDs, and verifiable data flows—the next step is to determine how your users or devices will interact with the system in production. There are several common deployment models depending on your use case, each with its own strengths: * **Server Implementation** * **Mobile Application** * **Web Application** Below is a high-level overview of how each approach works and what makes it unique. These models can also be combined depending on your architecture. ## Server Implementation A **server-side deployment** is the most flexible and common approach for DePINs, especially when dealing with multiple machines, background processes, or integrations with legacy infrastructure. ### Key Characteristics * **Control** of DID creation, wallet generation, and on-chain transactions. * Ideal for **IoT networks**, **machine fleets**, or **backend-only services**. * Can be scheduled, event-driven, or triggered via APIs from other systems. ### Typical Use Cases * Creating a machine DID as soon as a new device is provisioned. * Running cron jobs or real-time services to monitor machine behavior and update DIDs. * Hosting APIs that allow frontends (web/mobile) to interact with blockchain logic securely. ### Tech Stack * Node.js, Python, or Go backend * Cloud infrastructure (AWS, GCP, Azure) * Secure key management (e.g. HSMs, Vault, KMS) *Recommended when you need maximum control and scalability across a large number of machines or services.* ## Mobile Application A **mobile-based DePIN** enables end-users to manage their machine identities or interact with the network directly from their smartphone. ### Key Characteristics * Local wallet generation and signing using **device secure storage** (e.g. Keychain, Keystore). * Direct integration with peaq DID SDK and peaq storage via lightweight APIs. * Enables machines controlled or **managed via phones** (e.g. mobility services, consumer DePINs). * Integration with Account Abstraction solutions. ### Typical Use Cases * End-user onboarding flow to create a machine wallet and DID when setting up a device. * Enabling rideshare drivers, delivery partners, or remote workers to verify data from their device. * QR code scanning or Bluetooth-triggered DID operations. ### Tech Stack * React Native, Flutter, or native iOS/Android * Lightweight backend for transaction relaying (optional) * Integration with WalletConnect, push notifications, or secure enclave features *Recommended for DePINs targeting individual users, decentralized access, or mobile-first ecosystems.* ## Web Application A **web app-based DePIN** allows machines or users to interact with the peaq network through a browser interface, offering a lightweight and accessible entry point. ### Key Characteristics: * On-demand wallet generation using browser-based tools (e.g. `ethers.js`, `peaq-js-sdk`). * Great for **dashboard-style tools**, **data viewers**, or **admin portals**. * Ideal for browser-based configuration or onboarding flows. * Integration with Account Abstraction solutions. ### Typical Use Cases: * Admin portals that manage a fleet of machines via a web dashboard. * Frontend tools that allow users to submit or verify data tied to machine DIDs. * Public explorers, visualizations, or analytics for DePIN networks. ### Tech Stack: * React, Next.js, or Vue.js frontend * SDK integration for DID operations and storage writes * Optional backend for signing or relaying if key custody is needed *Recommended for data transparency, interactive dashboards, or lightweight onboarding experiences.* ## Choosing the Right Approach Each deployment method is valid and can be adapted to your specific needs. In many cases, projects combine two or more approaches. For example: * A **server** handles DID creation and background processes. * A **web app** provides admin controls. * A **mobile app** allows users to interact directly with machines. Once you've chosen your approach, continue to the Pre-deployment Checklist below to ensure a smooth launch to peaq mainnet. # Pre-deployment Checklist ### **Security Audits and Final Checks** * **Recommendation**: Before deploying to peaq mainnet, perform a final security audit of the smart contracts and DID implementation. Doing so will ensure there are no vulnerabilities that could compromise the integrity of the system or the users' assets. > Ensure all smart contracts and DIDs have passed a thorough security audit. Address any potential vulnerabilities and run additional stress tests to validate performance under mainnet conditions. ### **Community and Developer Engagement** * **Recommendation**: Engage with the community and developers during the launch phase. This will build trust and excitement while onboarding potential users and contributors. > Host an AMA (Ask Me Anything) or community event to showcase the deployment and answer questions about the project's functionality, vision, and roadmap. ### **Documentation and Tutorials** * **Recommendation**: Provide clear, user-friendly documentation and tutorials for interacting with the system to reduce friction for new users and developers while improving adoption. > Publish step-by-step guides and developer tutorials to help users and integrators interact with your DIDs, smart contracts, and token on peaq mainnet. ### **Incentive Programs** * **Recommendation**: Launch incentive programs such as bug bounties, user onboarding rewards, or machine data upload. This will further encourage participation and will help identify potential issues post-deployment. > Introduce a bug bounty program to reward security researchers for identifying vulnerabilities. Consider offering tokens or perks for early adopters who test or contribute to the system. ### **Monitoring and Analytics Setup** * **Recommendation**: Set up monitoring tools to track performance and detect anomalies on the mainnet to respond quickly to any issues. > Implement blockchain analytics and monitoring tools to track transaction volumes, smart contract activity, and any unusual patterns post-deployment. ### **Integration Announcements** * **Recommendation**: Publicize integrations or partnerships that will launch alongside the deployment to highlight the ecosystem's growth and potential. > Announce any collaborations, integrations, or ecosystem partnerships that will utilize your deployed DIDs or tokens on the peaq mainnet. ### **Governance and Tokenomics Introduction** * **Recommendation**: Provide detailed information on governance and token utility. This will help the users understand how to participate and what benefits the token brings. > Release governance details and tokenomics, including staking, voting mechanisms, or reward structures, to foster participation and transparency. ### **Post-Launch Support Plan** * **Recommendation**: Outline a plan for providing ongoing support and updates to build confidence in long-term reliability and engagement. > Share your post-launch roadmap and establish channels for technical support, including dedicated help desks, community moderators, or support emails. ### **Compliance and Regulatory Transparency** * **Recommendation**: Ensure the deployment complies with relevant laws and regulations. Doing so will mitigate the risks of legal issues that could arise post-deployment. > Provide transparency on regulatory compliance efforts and any legal frameworks adhered to during the deployment. ### **User Education Campaigns** * **Recommendation**: Educate end users about the system's benefits, features, and how to use it to facilitate adoption and trust in the platform. > Conduct user education campaigns, including webinars, videos, and blog posts, to familiarize users with the functionality of your deployed solutions. # Onboard a Machine Source: https://docs.peaq.xyz/peaqchain/build/first-depin/onboard-machine Onboarding a machine to the **peaq network** can be done in various ways, but the most straightforward approach is by using the **JavaScript SDK** to create a **DID Document** for the machine. Before deploying to the peaq mainnet, developers are strongly encouraged to test their implementations on the **agung testnet**. This ensures that machine onboarding, verification, and data storage mechanisms function correctly before deploying to an environment with real monetary value. ## Prerequisites Before onboarding a machine, it is recommended you have an understanding of: * **DID Documents** * See: [DID Document](/peaqchain/learn/peaqDID) from the learn section. ## Instructions The **JavaScript SDK** provides a developer-friendly way to: * Create a **DID Document** for a machine. * Define the **machine's cryptographic identity** using `EcdsaSecp256k1`, `Ed25519`, or `Sr25519` based on the wallet that will be used to create signatures. * Add **service endpoints** to link ownership and storage solutions. * Store and retrieve DID-related information from the **peaq blockchain**. ### 1. Installing ethers & peaq js To get started, install: * **ethers -** library used for wallet generation and token transfer. * **peaq network sdk -** generates DID Documents and offers peaq storage. In your local node environment run: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install ethers npm install @peaq-network/sdk ``` 📌 *If you are having trouble please refer to the [SDK Installation Guide](/peaqchain/build/getting-started/install-peaq-sdk).* ### 2. Creating a Machine Wallet Each machine onboarded to the peaq network needs a unique cryptographic identity. This is achieved by creating a dedicated wallet that represents the machine. The wallet enables the machine to sign data, authenticate itself, and perform transactions securely on the network. Since the peaq network is **EVM-compatible**, we use the **ECDSA (Elliptic Curve Digital Signature Algorithm)** scheme for key management. This allows machines to sign messages and execute transactions using EVM-standard tools. The example below shows how to generate a new wallet using the `ethers` npm package: ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Wallet } from "ethers"; // 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 main(){ // Connect the device to your application logic here // Create a new EVM-compatible wallet for the machine const machineWallet = new ECDSAKeyring(); // Retrieve the wallet's public address (used to identify the machine on-chain) console.log("Machine Address:", machineWallet.getAddress()); // Retrieve the private key (keep this secure and never share it) console.log("Private Key:", machineWallet.getPrivateKey()); } main(); ``` The `ECDSAKeyring` class abstracts the creation and usage of an EVM-compatible wallet. It provides access to the machine's public address and private key, and exposes a `signMessage` method that will be used later for signing operations and authentication. Never share or expose the machine's private key. Possession of this key allows anyone to impersonate the machine, sign data, and perform transactions on its behalf. Losing the key means losing access and control over the machine's on-chain identity. This is not the only way to onboard machines to the network. Sometimes private keys are not necessary when using Machine Smart Accounts. Learn more about Machine Smart Accounts in the Machine Station Factory section. *Coming Soon* ### 3. Token Transfer In order for a machine to interact with the peaq network, it must be able to execute on-chain transactions — such as registering itself, signing data, or interacting with smart contracts. On EVM-compatible networks like peaq, every write operation to the blockchain requires a fee paid in the network's native token. This fee, often referred to as **gas**, compensates the network for computation and storage resources, and helps maintain network integrity by preventing spam or malicious activity. After creating a wallet for your machine, you must transfer a small amount of the native token from your admin wallet to the machine wallet. This enables the machine to independently perform transactions on the blockchain. If you have no tokens please check out our [faucet page](/peaqchain/build/getting-started/get-test-tokens) to receive funds. The code below demonstrates how to transfer 1 token from an admin account to the machine's wallet: ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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); } } // Load the admin wallet async function getAdmin(){ const provider = new ethers.JsonRpcProvider("https://peaq-agung.api.onfinality.io/public"); // agung endpoint for testnet const adminPrivateKey = "0xADMIN_PRIVATE_KEY"; // Replace with admin's secure private key return new ethers.Wallet(adminPrivateKey, provider); } // Transfer native tokens from admin to machine async function transferNativeToken(adminWallet, machineWallet) { const tx = await adminWallet.sendTransaction({ to: machineWallet.getAddress(), value: ethers.parseEther("1") // transfers 1 token over }); console.log("Transaction sent:", tx.hash); await tx.wait(); console.log("Transaction confirmed."); } async function main(){ const MACHINE_PRIVATE = "0xMACHINE_PRIVATE_KEY"; // Replace with your machine's actual private key const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE); const adminWallet = await getAdmin(); await transferNativeToken(adminWallet, machineWallet); } main(); ``` 🧪 *This example uses the agung testnet RPC endpoint. Be sure to switch to the peaq mainnet when you're ready to onboard real machines in production.* After running this script, your **machine wallet** will hold 1 native token, making it capable of executing transactions on the peaq network. ### 4. Creating a Machine DID Document Once a wallet and funding are in place, the next step is to **register a Decentralized Identifier (DID) Document** for the machine. This DID represents the machine’s **on-chain identity** on the peaq network. A DID Document serves as a verifiable data structure that links the machine’s cryptographic keys to a unique identifier. It allows machines to authenticate themselves, prove ownership of data, and interact securely within the network. In this example, we use the **peaq JavaScript SDK** to register a DID for the machine on the **agung testnet**. The DID Document contains: * **Verification Methods -** Public keys that allow others to verify signatures made by the machine. * **Service Endpoints -** Useful metadata such as the machine's admin wallet address or off-chain services. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers, 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); } } // Create a new DID on chain async function generateDID(machineWallet, adminWallet){ const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // agung testnet endpoint const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "project_name"; // Replace with your project/DePIN specific name const customFields = { verifications: [{ type: "EcdsaSecp256k1RecoveryMethod2020" }], services: [ { id: '#admin', type: 'admin', data: `${adminWallet.address}` } ] }; // Create a transaction object to register the DID const tx = await sdk.did.create({ name: name, address: machineWallet.getAddress(), 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 registered with receipt:", receipt); } async function main(){ const MACHINE_PRIVATE = "0xMACHINE_PRIVATE_KEY"; // Replace with your machine key const ADMIN_PRIVATE = "0xADMIN_PRIVATE_KEY"; // Replace with your admin key const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE); const adminWallet = new ECDSAKeyring(ADMIN_PRIVATE); await generateDID(machineWallet, adminWallet); } main(); ``` The code above registers a DID Document that **links** the machine's address with a verification method based on the wallet the machine is tied to. A reference to the admin account responsible for it is added as well. By submitting this transaction from the machine's wallet, the peaq network **cryptographically** claims that the machine is the entity creating this identity. 📌 *For more advanced DID operations, including updates, deactivation, or retrieval, refer to the full reference:* [DID Operations](/peaqchain/sdk-reference/javascript/did-operations). ## Putting it all together The code below demonstrates the complete flow for onboarding a machine to the peaq network using the agung testnet. This full example includes: 1. Creating a **machine wallet**. 2. Loading an **administrator wallet** (with native tokens). 3. Transferring **tokens** to the machine wallet. 4. Registering a DID Document that represents the **machine's on-chain identity**. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ethers, Wallet } from "ethers"; import { Sdk } from "@peaq-network/sdk"; // Simple keyring class for managing EVM-compatible wallets class ECDSAKeyring { constructor(privateKey) { this.wallet = privateKey ? new Wallet(privateKey) : Wallet.createRandom(); } getAddress() { return this.wallet.address; } getPrivateKey() { return this.wallet.privateKey; } async signMessage(message) { return this.wallet.signMessage(message); } } // Load an admin wallet with tokens to fund new machine wallets async function getAdmin() { const provider = new ethers.JsonRpcProvider("https://peaq-agung.api.onfinality.io/public"); // Agung testnet const adminPrivateKey = "0xADMIN_PRIVATE_KEY"; // Replace securely return new Wallet(adminPrivateKey, provider); } // Transfer native tokens (gas) to the machine's wallet async function transferNativeToken(machineWallet, adminWallet) { const tx = await adminWallet.sendTransaction({ to: machineWallet.getAddress(), value: ethers.parseEther("1") // Transfer 1 native token }); console.log("Transaction sent:", tx.hash); await tx.wait(); console.log("Transaction confirmed."); } // Register a DID Document on the peaq network for the machine async function generateDID(machineWallet, adminWallet) { const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "project_name"; // Customize per project const customFields = { verifications: [{ type: "EcdsaSecp256k1RecoveryMethod2020" }], services: [{ id: '#admin', type: 'admin', data: `${adminWallet.address}` }] }; const tx = await sdk.did.create({ name: name, address: machineWallet.getAddress(), customDocumentFields: customFields }); const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: machineWallet.getPrivateKey() }); console.log("peaq DID EVM receipt:\n", receipt); } // Main execution flow async function main() { // 1. Create machine wallet const machineWallet = new ECDSAKeyring(); // Optional: store these values securely // console.log("Machine Address:", machineWallet.getAddress()); // console.log("Private Key:", machineWallet.getPrivateKey()); // 2. Load admin wallet const adminWallet = await getAdmin(); // 3. Transfer tokens to the machine await transferNativeToken(machineWallet, adminWallet); // 4. Create machine DID Document await generateDID(machineWallet, adminWallet); } main(); ``` ## Summary By running this script, you've completed the foundational steps to **onboard a machine** to the peaq network. Your machine now has a cryptographic identity (via EVM wallet), the necessary native tokens to operate, and a DID Document that serves as its verifiable on-chain identity. This setup allows your machine to participate securely in decentralized physical infrastructure networks (DePINs), enabling services such as credential validation, data authentication, autonomous task execution, and more. For production use, always: * Store private keys **securely** (e.g., encrypted storage or secure enclaves). * **Avoid** hardcoding secrets into codebases. * Use the peaq **mainnet** endpoint instead of the agung testnet. Now that your machine has a verifiable on-chain identity, the next step is to learn how to store and retrieve data using [**peaq storage**](/peaqchain/sdk-reference/javascript/storage-operations) — a decentralized storage layer that enables machines to persist important information on-chain. # Run and Test Source: https://docs.peaq.xyz/peaqchain/build/first-depin/run-and-test Now that you've successfully created a machine ID, stored signed data, and linked everything to a DID Document, the final step is to **verify** that everything is functioning as expected. In this section, you will learn how to: * Retrieve and inspect a machine's DID Document. * Confirm that the **linked signature** and data storage are correct. * Verify that the signed message was indeed created by the **machine** and approved by the **administrator**. * Explore and confirm relevant transactions on a **block explorer**. These steps are essential to ensure the **integrity** of your machine's on-chain identity and data, and to build confidence in the end-to-end trust model of the peaq network. ## Prerequisites Before proceeding, ensure that you have: * Completed the [Onboard a Machine](/peaqchain/build/first-depin/onboard-machine) section. * Completed the [Store Machine Data](/peaqchain/build/first-depin/store-machine-data) section. * A basic understanding of public/private key **cryptography** and **ECDSA** signature verification. * Familiarity with using **blockchain explorers** to inspect transactions and on-chain data. ## Code Examples ### Resolve DID Document Now that the machine has created and registered a DID Document, we can query it from the blockchain to verify its contents. In this step, we'll use the same machine wallet to resolve the DID and read its document structure. The code below: 1. Instantiates the `peaq-js-sdk`. 2. Reads the DID Document associated with the machine wallet's address. 3. Logs the full DID Document to the console, which includes verification methods, services, and the signature metadata. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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); } } async function readDID(machineWallet){ const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // Agung testnet endpoint const WSS_BASE_URL = "wss://wss-async-agung.peaq.xyz"; // Used to read previously stored DID const name = "project_name"; // Same name as before const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const document = await sdk.did.read({ name: name, address: machineWallet.getAddress(), wssBaseUrl: WSS_BASE_URL }); console.log(document.document); } 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 await readDID(machineWallet); } main(); ``` ### Return Object ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { id: 'did:peaq:0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B', controller: 'did:peaq:0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B', verificationMethods: [ { id: 'did:peaq:0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B#keys-1', type: 'EcdsaSecp256k1RecoveryMethod2020', controller: 'did:peaq:0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B', publicKeyMultibase: '0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B' } ], signature: { type: 'EcdsaSecp256k1RecoveryMethod2020', issuer: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', hash: '0x1d76e12aafe2701545e8f1a2be8854dcd0b209521aaef9d836548dc047650fad1af0fcb5922b125aa431a97be3eaf5d5703e86bed5e3163d59117cd832bb54651b' }, services: [ { id: '#unsignedMessage', type: 'peaqStorage', data: '392c33b7-aaec-490a-b392-97af8d1e3910' }, { id: '#admin', type: 'admin', data: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C' } ], authentications: [ 'did:peaq:0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B#keys-1' ] } ``` ### Explanation of the Returned DID Document `id` - The full Decentralized Identifier (DID) of the machine. It is based on the machine's EVM address. `controller` - The entity that controls this DID, in this case, the machine itself. `verificationMethods` - Defines how the machine proves its identity. * Indicates that the DID can be authenticated using an ECDSA signature generated by the machine's public key. * The key provided here (`publicKeyMultibase`) is the machine's wallet address. `signature` - Generated by the admin who signs a canonical representation of the machine's stored data. * **type -** `EcdsaSecp256k1RecoveryMethod2020` (the algorithm used for signature verification). * **issuer -** field identifies the admin wallet that signed the message. * **hash -** is the raw signature value (a string of bytes representing the signed message). > A verifier can use this field in combination with the unsigned message in peaq storage to confirm that the message was indeed signed by the admin & machine (see below). `services` - Links the DID Document to external data and related parties. * **unsignedMessage -** points to the key used in peaq storage for admin and machine wallets where the original (unsigned) message is stored. * **#admin -** links to the wallet that administers or manages this machine (set during DID creation). `authentications` - Specifies the verification method to be used for authenticating the DID. * This field references `#keys-1` from the `verificationMethods` section, meaning that the machine's public key is used to authenticate operations associated with the DID. ## Verify Storage In this step, we verify that the machine-generated data in **peaq storage** is authentic and has been approved by the trusted administrator. Using the resolved DID Document, we will: 1. Retrieve the unsigned admin approval data from peaq storage using the stored UUID. 2. Confirm that the stored approval object correctly references the storage key. 3. Retrieve the unsigned machine data using the machine's public key. 4. Verify the machine's signature—embedded within the admin-approved content—by recovering the machine's public key from the unsigned machine data. 5. Verify the admin's signature on the canonical approval data, ensuring that the trusted authority indeed approved the machine data. The following code demonstrates these steps using **ECDSA signature verification** via the ethers library: ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Wallet, ethers } 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); } } async function readDID(machineWallet){ const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // Agung testnet endpoint const WSS_BASE_URL = "wss://wss-async-agung.peaq.xyz"; // Used to read previously stored DID const name = "project_name"; // Same name as before const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const document = await sdk.did.read({ name: name, address: machineWallet.getAddress(), wssBaseUrl: WSS_BASE_URL }); return document.document; } async function readStorage(uuid, issuer){ const HTTPS_BASE_URL = "https://peaq-agung.api.onfinality.io/public"; // Agung testnet endpoint const WSS_BASE_URL = "wss://wss-async-agung.peaq.xyz"; // Used to read previously stored DID const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const key = "my_key"; const pair = await sdk.storage.getItem({ itemType: uuid, address: issuer, wssBaseUrl: WSS_BASE_URL }); return pair[uuid]; } async function main(){ // Get previously created wallet via the private key const machineWallet = new ECDSAKeyring(MACHINE_PRIVATE); // Same data value that was stored previously const doc = await readDID(machineWallet); // Get public keys for admin and machine const machinePublicKey = doc.verificationMethods[0].publicKeyMultibase; const adminPublicKey = doc.signature.issuer; // Get signature for the admin approved message const adminSignature = doc.signature.hash; // Get uuid to read from the storages const uuid = doc.services[0].data; // Read peaq storage to get the unsigned admin message const unsignedAdminMessage = await readStorage(uuid, adminPublicKey); // Make sure the unsigned admin data is relevant to the DID context const adminSignedObject = JSON.parse(unsignedAdminMessage); if (adminSignedObject.storageKey != uuid){ throw new Error("UUIDs do not match"); }; // Get unsigned machine data const unsignedMachineMessage = await readStorage(uuid, machinePublicKey); // Verify machine data const recoveredAddress = ethers.verifyMessage(unsignedMachineMessage, adminSignedObject.machineSignature); console.log("Machine Signature valid:", machinePublicKey === recoveredAddress); // Verify admin const recoveredAddress2 = ethers.verifyMessage(unsignedAdminMessage, adminSignature); console.log("Admin Signature valid:", adminPublicKey === recoveredAddress2); } main(); ``` If the verification is successful, in the terminal you'll see: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Machine Signature valid: true Admin Signature valid: true ``` It confirms that: * **Machine Verification:** The unsigned machine data stored in peaq storage was indeed signed by the machine (as the recovered address matches the machine's public key). * **Admin Verification:** The admin-approved content was signed by the trusted administrator (as the recovered address matches the admin's public key). 🎉 *Congratulations! You've successfully validated that the machine-generated data is both authored by the machine and approved by a trusted admin. This dual-layer signature verification ensures decentralized trust and accountability in your DePIN system.* ## Block Explorers To gain deeper insight into your machine's activity on the network, you can use a **block explorer**. Block explorers allow you to view all on-chain transactions associated with your wallet, track DID and storage interactions, and confirm whether data was successfully recorded. Depending on the network you're using, visit the appropriate **Subscan** explorer: | peaq | agung | | ----------------------------------- | -------------------------------------------- | | [Subscan](https://peaq.subscan.io/) | [Subscan](https://agung-testnet.subscan.io/) | In this guide, we'll focus on the **agung testnet explorer**. ### Viewing Your Wallet To begin, copy the **public address** of your machine wallet — the same one that executed your DID and storage transactions. For example: `0x1f7701CbdadDB1775C525Ca6FcF18ec4135D228B` Paste this address into the search bar at the top of the [agung explorer](https://agung-testnet.subscan.io/) homepage. You'll be directed to a detailed page for that wallet address, which looks like this: run-and-test-1 ### Token Balance Overview In the top-right section, you'll see the machine's **\$AGUNG token balance**. This is the native token used for transaction fees. In this example: * The account holds a total of **1.999 AGUNG** * **0.579 AGUNG** is currently *transferable* The remaining balance is locked as a **deposit** (or reserved fee), which occurs when interacting with peaq services like **creating a DID** or **storing data**. If needed, you can reclaim these deposits using appropriate "remove" or cleanup functions, depending on your use case. ### Viewing Transactions Scroll down to the **EVM Transactions** section. Here you'll see a list of all the actions executed by this wallet, such as: * `updateAttribute(...)` - Used to update the machine's DID Document. * `addStorage(...)` - Used to store a message in peaq storage. To view more details about a specific transaction, click the arrow on the far right. run-and-test-2 ### Viewing Transaction Events Once you click into a transaction, you'll see a breakdown of on-chain **events** triggered by the transaction. In the example below, you can see: * The extrinsic `peaqdid(AttributeUpdated)` was triggered. * The DID name `project_name` was updated. * The `BoundedVecValue` shows the actual value written — a serialized representation of the updated DID Document. This is especially useful for confirming what data was actually committed to the chain and debugging any mismatches between expected and actual values. run-and-test-3 ### Summary Block explorers like Subscan are essential tools for: * **Verifying** DID and storage transactions. * **Tracking** gas usage and deposit balances. * **Debugging** incorrect or failed writes. * **Confirming** that your machine's identity and data are verifiably recorded on-chain. Bookmark your machine's Subscan URL for quick reference during development and testing. 🛠️ # Store Machine Data Source: https://docs.peaq.xyz/peaqchain/build/first-depin/store-machine-data 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](/peaqchain/build/advanced-operations/off-chain-storage/ipfs). ## 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](/peaqchain/build/first-depin/onboard-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](/peaqchain/build/first-depin/onboard-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. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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](/peaqchain/build/advanced-operations/off-chain-storage/ipfs) 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) ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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 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 administrator's 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**. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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.xyz"; // 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. ```javascript JavaScript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 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.xyz"; // 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. # Connecting to peaq Source: https://docs.peaq.xyz/peaqchain/build/getting-started/connecting-to-peaq On this reference page, you will find the network types, RPC/WSS URLs, chain identifiers, etc. to connect to peaq or agung. ## Networks: | Network | Network type | | ------- | ------------ | | peaq | Mainnet | | agung | Testnet | ### Chain ID ```bash peaq theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 3338 ``` ```bash agung theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 9990 ``` ### Public RPC URLs ```bash peaq theme={"theme":{"light":"github-light-default","dark":"github-dark"}} https://quicknode1.peaq.xyz https://quicknode2.peaq.xyz https://quicknode3.peaq.xyz # Secondary Fallback Option https://peaq.api.onfinality.io/public https://peaq-rpc.publicnode.com ``` ```bash agung theme={"theme":{"light":"github-light-default","dark":"github-dark"}} https://peaq-agung.api.onfinality.io/public https://wss-async-agung.peaq.xyz ``` ### Public WSS URLs ```bash peaq theme={"theme":{"light":"github-light-default","dark":"github-dark"}} wss://quicknode1.peaq.xyz wss://quicknode2.peaq.xyz wss://quicknode3.peaq.xyz # Secondary Fallback Option wss://peaq.api.onfinality.io/public-ws wss://peaq-rpc.publicnode.com ``` ```bash agung theme={"theme":{"light":"github-light-default","dark":"github-dark"}} wss://peaq-agung.api.onfinality.io/public-ws wss://wss-async-agung.peaq.xyz ``` ## Wallet Configuration Guides Complete guide with video tutorial on how to manually update your wallet's RPC settings for peaq network. Step-by-step guide with desktop and mobile video tutorials on removing and re-adding the peaq network from your wallet. ### Private URLs You can create your custom peaq RPC/WSS endpoint with QuickNode or OnFinality. To do so, follow one of the guides below: * [QuickNode guide](https://www.quicknode.com/guides/quicknode-products/how-to-use-the-quicknode-dashboard#create-a-quicknode-endpoint) * [OnFinality guide](https://documentation.onfinality.io/support/the-enhanced-api-service) ## Block explorers #### peaq | Explorer | Type | URL | | ---------------- | --------------- | ---------------------------------------------- | | Subscan | EVM & Substrate | [Subscan](https://peaq.subscan.io/) | | Machine Explorer | Machines | [machines.peaq.xyz](https://machines.peaq.xyz) | #### agung | Explorer | Type | URL | | -------- | --------------- | -------------------------------------------- | | Subscan | EVM & Substrate | [Subscan](https://agung-testnet.subscan.io/) | ## Node setup ### Node hardware requirements ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} OS - Ubuntu 20.04 CPU - 3.3 GHz AMD EPYC 7002 Storage - 1TB SSD Memory - 8GB ``` ### Docker image peaq ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} docker run -v peaq-storage:/chain-data -p 9944:9944 peaq/parachain:peaq-v0.0.113 \ --parachain-id 3338 \ --chain ./node/src/chain-specs/peaq-raw.json \ --base-path chain-data \ --port 30333 \ --rpc-port 9944 \ --rpc-cors=all \ --execution wasm \ --state-pruning archive \ -- \ --execution wasm \ --port 30343 --rpc-port 9977 \ --sync warp ``` # DeFi on peaq Source: https://docs.peaq.xyz/peaqchain/build/getting-started/defi-guide # DEX Native DEX on peaq is provided by MachineX: [https://www.machinex.xyz/](https://www.machinex.xyz/) # Guide: DeFi Essentials on peaq How to access peaq on popular EVM-compatible wallets? ### Add peaq as a custom network on your wallet Most EVM wallets enable you to add a custom network — make sure to refer to their respective guides, such as: * [MetaMask](https://support.metamask.io/configure/networks/how-to-add-a-custom-network-rpc/) * [Rabby Wallet](https://www.quicknode.com/guides/ethereum-development/wallets/how-to-set-a-custom-provider-in-rabby) * [Trust Wallet](https://trustwallet.com/blog/guides/how-to-add-a-custom-network-on-the-trust-wallet-app) * [Base App](https://www.quicknode.com/guides/ethereum-development/wallets/how-to-setup-coinbase-wallet-with-quicknode) ### Use the following settings to add peaq: * RPC Endpoint: [https://quicknode3.peaq.xyz](https://quicknode3.peaq.xyz) * Chain ID: 3338 * Currency: PEAQ * Block Explorer URL: [https://peaq.subscan.io](https://peaq.subscan.io) ### You can now access peaq and dApps on peaq with your wallet For more network configuration information please check out the [connecting to peaq page](/peaqchain/build/getting-started/connecting-to-peaq). # How to add custom tokens on peaq to my wallet? Depending on your wallet, sometimes, you may need to manually add specific tokens on peaq to your wallet as custom tokens. Please see this guide on adding custom tokens from [MetaMask](https://support.metamask.io/manage-crypto/tokens/how-to-display-tokens-in-metamask/) as an example. For PEAQ itself, always take the address from the official [PEAQ Token Contracts](/peaqchain/learn/peaq-token-contracts) page. For other tokens on peaq, use the [peaq Token List](/peaqchain/build/getting-started/token-list) — never a third-party aggregator, which can list impostor tokens. Important Tokens on peaq: | Block Explorer | Contract Address | Symbol | Decimals | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | -------- | | Wrapped PEAQ ([official PEAQ contracts](/peaqchain/learn/peaq-token-contracts)) | [0x3cD66d2e1fac1751B0A20BeBF6cA4c9699Bb12d7](https://peaq.subscan.io/token/0x3cD66d2e1fac1751B0A20BeBF6cA4c9699Bb12d7) | WPEAQ | 18 | | Bridged stgUSDT (peaq USDT) | [0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949](https://peaq.subscan.io/token/0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949) | USDT | 6 | | Wrapped Parasail Staked PEAQ | [0xE5330a9fBA99504C534127D39727729899c9a506](https://peaq.subscan.io/token/0xE5330a9fBA99504C534127D39727729899c9a506) | wstPEAQ | 18 | # How to bridge to peaq? For the best experience, please use the desktop applications and make sure you have the relevant wallet browser extensions installed and peaq is added as a custom network in your wallet. ## Stargate Bridge Example: Bridging USDT from Arbitrum to peaq with Metamask browser extension ### Step 1 Go to [https://stargate.finance/bridge](https://stargate.finance/bridge) (note: please make sure you have some ETH on Arbitrum One to cover the gas fee for bridging) and connect your wallet. ### Step 2 Select the correct token and networks to bridge from and to. In our example, we are bridging 3 USDT from Arbitrum to peaq. defi-2 If it's your first time bridging to peaq, head to `Advanced Settings` and enable `Gas on Destination`. You can choose `Auto`, `Medium`, or `Max` for your gas configuration. defi-3 ### Step 3 On MetaMask, approve the spending cap request. defi-4 ### Step 4 On MetaMask, approve the transaction request. defi-5 ### Step 5 Once the transaction is confirmed, a pop-up will show up along with a link to the transaction. defi-6 ### Step 6 Wait for LayerZero to complete the bridging (you can monitor the progress on this status bar). defi-7 ### Step 7 Bridging complete — you will see the green tick on the interface. You will also find a log of this activity in your MetaMask under the Activity tab on Arbitrum One network. defi-8 ### Step 8 For the bridged USDT balance to show on your MetaMask wallet, find peaq on your wallet's list of custom networks. Click on **Import Tokens → Custom Tokens**, select **peaq**, and add the following: * Address: `0xf4D9235269a96aaDaFc9aDAe454a0618eBE37949` * Symbol: `USDT` * Decimals: `6` (if you have issues editing the decimal, please update or reinstall your browser extension). defi-9 Now your bridged USDT balance on peaq will show up in your wallet! defi-10 **Disclaimer:** The information in this guide is provided for educational purposes only and does not constitute financial, investment, legal, or tax advice. Bridging assets and interacting with blockchain networks and wallets carry risks including loss of funds due to smart-contract bugs, incorrect addresses, network congestion, and fees. Always double-check addresses and network settings, test with a small amount first, and consider consulting a qualified professional. We are not responsible for any loss, damage, or other consequences resulting from your use of this guide or third-party services. # EVM Onboarding Source: https://docs.peaq.xyz/peaqchain/build/getting-started/evm-onboarding ## 1. Working with Finality ### 1.1 Three Presets | Preset | Use-case | How to code it | Typical delay | | --------- | -------------------------- | -------------------------------- | ------------- | | **FAST** | UI refresh, analytics | `provider.getBlock("latest")` | *\~1 s* | | SAFE | Most user actions | `await tx.wait(7)` | *\~7-8 s* | | **FINAL** | Treasury, bridge, NFT mint | `provider.getBlock("finalized")` | *\~15-20 s* | > Golden rule: a latest block is optimistic; a finalized block is forever. ### 1.2 Decision matrix * **FAST (0 confirms, \~1 s)** – Great for real-time UI updates and analytics. Roll back if a reorg shows up. * **SAFE (\~7 authored blocks, \~7-8 s)** – Default for ordinary user actions (token transfers, approvals, simple swaps). Rarely reorgs yet still snappy. * **FINAL (relay-chain finalized, \~15-20 s)** – Use for anything that must be absolutely irreversible: treasury moves, bridge deposits, high-value NFT mints, etc. *** ## 2. Quick-Start Checklist 1. **RPC & chain-ID** * Mainnet RPC `https://quicknode1.peaq.xyz` ID `3338` * Testnet RPC `https://peaq-agung.api.onfinality.io/public` ID `9990` 2. **Pin EVM version to `london`** ``` solidity: { version: "0.8.21", settings: { evmVersion: "london" } } ``` 3. **Estimate gas then add a ×2 buffer** ``` const gas = await provider.estimateGas(tx); tx.gasLimit = gas.mul(2); ``` 4. **Deploy as usual** `npx hardhat run scripts/deploy.js --network peaq` *** ## 3. Handling Reorgs & Using Finalized Blocks **What is a Reorg?** When two blocks land at almost the same time the chain briefly forks; the shorter fork is later dropped, so its last few blocks - and any transactions inside - vanish and must be re-included. **Why does using Finalized block help in that case?** Extra votes lock a block in permanently; once finalized, that block (and all earlier ones) can never be dropped, so reorgs can only touch the unfixed tip of the chain. ``` // Latest for optimistic UI const latest = await provider.getBlock("latest"); // Finalized for deterministic reads const finalized = await provider.send( "eth_getBlockByNumber", ["finalized", false] ); ``` When monitoring blocks: ``` let lastHash; provider.on("block", async (n) => { const blk = await provider.getBlock(n); if (lastHash && blk.parentHash !== lastHash) { // reorg detected – revert optimistic state } lastHash = blk.hash; }); ``` *** ## 4. peaq-Specific Nuances | Topic | What’s different | What to do | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `block.difficulty` / `prevrandao` | Always `0` | Use Chainlink VRF or for low stakes `randomnessCollectiveFlip` queried at **finalized** height. | | Gas schedule | Mostly London-equivalent; storage a bit pricier | Always run `eth_estimateGas`, never hard-code. | | Account abstraction (ERC-4337) | Canonical EntryPoint v0.6 deployed at `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`; peaqOS machine smart accounts are wired to it | See [peaqOS smart contracts](/peaqos/concepts/contracts#erc-4337-smart-accounts). | | Deep traces | `debug_*` RPCs only on tracing-enabled nodes | Run a node with `--ethapi=debug,trace,txpool` & `--features evm-tracing`. | *** ## 5. Tracing & Debugging Enable in node startup: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ./peaq-node \ --ethapi=debug,trace,txpool \ --rpc-methods=Unsafe \ --state-pruning archive \ --wasm-runtime-overrides=parachain-peaq \ --runtime-cache-size 64 \ ``` Then: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -X POST -H "Content-Type: application/json" -d \ '{"jsonrpc":"2.0","id":1,"method":"debug_traceTransaction","params":["",{"tracer":"callTracer"}]}' \ https://evm.yourevmnode.xyz ``` > **💡 Note:**\ > Before enabling tracing RPCs, you must build the runtime with the `evm-tracing` feature and place the resulting `.wasm` file in a folder used for overrides. Then, start the node with tracing RPCs enabled and point `--wasm-runtime-overrides` to that directory (not a file). The override is **not** included in the Docker image, so you must mount your own folder. If the path isn’t a real directory or the file doesn’t end in `.wasm`, the override will be ignored and tracing won’t work. > > For more information on building the runtime with tracing enabled, see the [peaq-node-builder repository](https://github.com/peaqnetwork/peaq-node-builder). *** ## 6. Frequently Asked Questions **How many confirmations are really “safe”?** Waiting for the **finalized** tag is bullet-proof; seven authored blocks (≈ 7-8 s) is the community standard for ordinary user actions. **Can a finalized block revert?** Only if ≥ ⅓ of stake is slashed — an economically irrational scenario. **My tx failed – how do I debug?** Simulate with `provider.call(tx)` first; for deep dives use `debug_traceTransaction` on a tracing node. *** ## 7. Resources * Docs [https://docs.peaq.xyz](https://docs.peaq.xyz/) * Explorer [https://peaq.subscan.io](https://peaq.subscan.io/) * Discord [https://discord.gg/UKTFkPWsyH](https://discord.gg/UKTFkPWsyH) * GitHub [https://github.com/peaqnetwork](https://github.com/peaqnetwork) *Happy building – and welcome to peaq!* # Get test tokens Source: https://docs.peaq.xyz/peaqchain/build/getting-started/get-test-tokens The following form can be used to receive 3 \$AGNG per day to a low funded **EVM** or **Substrate** wallet:
## Wallet Quick Start In order to perform transactions on the blockchain you need to have native tokens to pay gas for the network fees. Before a user can get the tokens, they must have a wallet for the faucet to know where to send them. Since peaq is a Substrate chain with the interoperability of EVM-style chains built in, you can either use: * EVM H160 address: `0x48C9774C88736F7c169D2598278876727AFD1476` * Substrate SS58 address: `5FEw7aWmqcnWDaMcwjKyGtJMjQfqYGxXmDWKVfcpnEPmUM3q` ### Create EVM Wallet 1. Go to the MetaMask wallet [extension page](https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en) 2. Add extension 3. Create a new wallet 4. Add password for login on your connected device 5. Write down and store the mnemonic phrase 6. Complete reading the wallet on-boarding prompts After successfully creating a new wallet you will have obtained an EVM address that the faucet can send tokens to. ### Add peaq to Wallet After creating a MetaMask account you will need to add a custom network manually for it to appear in the wallet. 1. Open MetaMask and go to the network selector page 2. Click on `Add a custom network` 3. Input with the following information sourced from [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq). You can add all the networks in a similar fashion, just make sure to change the values for the relevant chain. | Network | RPC URL | Chain Id | Currency symbol | Block Explorer | | ------- | --------------------------------------------------------------------- | -------- | --------------- | ---------------------------------------------------------------------- | | peaq | [https://quicknode1.peaq.xyz](https://quicknode1.peaq.xyz) | 3338 | PEAQ | [https://peaq.subscan.io/](https://peaq.subscan.io/) | | agung | [https://wss-async-agung.peaq.xyz](https://wss-async-agung.peaq.xyz/) | 9990 | AGNG | [https://agung-testnet.subscan.io/](https://agung-testnet.subscan.io/) | 4. Click save and change to that network 5. PEAQ/AGNG appears as native token ## Create Substrate Wallet 1. Go to the polkadot.js wallet [extension page](https://polkadot.js.org/extension/) 2. Download for your browser 3. Click on the plus to create a new account 4. Safely store the mnemonic phrase, and continue 5. Allow for use on any chain 6. Create a name and password ### Substrate Wallet If you are using a substrate wallet, please click on the [peaq accounts page](https://polkadot.js.org/apps/?rpc=wss://peaq.api.onfinality.io/public-ws#/accounts) or [agung accounts page](https://polkadot.js.org/apps/?rpc=wss://wss-async-agung.peaq.xyz#/accounts) to be routed to the account management dashboard. On the top of the page is an extension pane which displays available accounts that are connected. If your wallet is not showing up in this location, first make sure you have the polkadot wallet extension from above downloaded, and a new wallet created. If the issue persists please manually connect the wallet by following the steps below: 1. Go to your extensions and open polkadot get-test-tokens-1 2. Click on the “Connect Accounts” tab on the top of the extension get-test-tokens-2 3. Next you are able to manually select which wallets you would like to appear in this pane on the accounts page. get-test-tokens-3 4. Then click connect and the wallets will appear! get-test-tokens-4 # EVM Token Transfer Source: https://docs.peaq.xyz/peaqchain/build/getting-started/how-to-send-receive-peaq/evm-token-transfer This guide provides a detailed breakdown of a \$PEAQ token transfer between two wallets on the EVM side of the chain. ## Prerequisites: * Download and install EVM- and Substrate-based wallets. * MetaMask and Polkadot.js will be shown in this tutorial. You can use your preferred wallets instead. * Review the previous [Token Guide](/peaqchain/build/getting-started/get-test-tokens). ## Receive \$PEAQ ### EVM → EVM 1. Open up your MetaMask account and copy your address. This is your public H160 (EVM address standard) address that can be used to receive \$PEAQ from another EVM address. evm-token-transfer-1 ### Substrate → EVM In order to receive tokens from a Substrate wallet, you need to convert your EVM address into an SS58 (Substrate address standard) address with the [Address Converter](https://snow-address-converter.netlify.app/). 1. On the Address Converter, select the "H160" address format, paste your EVM address into the Address field, and click the "Go!" button. 2. In the output, you will find your EVM address converted into different standards, including its SS58 representation. Use this address to receive \$PEAQ from a Substrate wallet. ## Send \$PEAQ ### EVM → EVM 1. Open your MetaMask wallet and click on the "Send" button.evm-token-transfer-4 2. Acquire the recipient's EVM address and paste it in the 'to' field. 3. In the next window, set the amount of \$PEAQ you would like to transfer and click on "Continue". evm-token-transfer-5 4. Review the transaction details, then click on "Confirm" if everything is correct.evm-token-transfer-6 5. The transfer will execute, and the transaction will appear in your MetaMask's "Activity" tab. evm-token-transfer-7 ## **EVM → Substrate** Please follow the EVM → Substrate guide on the [Substrate Token Transfer Page](/peaqchain/build/getting-started/how-to-send-receive-peaq/substrate-token-transfer#evm-→-substrate) to use Address Unification and transfer \$PEAQ from an H160 wallet to an SS58. # Substrate Token Transfer Source: https://docs.peaq.xyz/peaqchain/build/getting-started/how-to-send-receive-peaq/substrate-token-transfer This guide provides detailed examples of transferring peaq between a sender and a recipient on the Substrate side of the chain. ## Prerequisites: * Downloaded and installed an EVM and Substrate based wallet. * MetaMask and Polkadot.js will be shown in this tutorial. You can swap for your preferred wallet of choice. * Understanding of the previous [Token Guide](/peaqchain/build/getting-started/get-test-tokens). ## Receive \$PEAQ ### Substrate → Substrate 1. We'll first begin by heading over to the [Polkadot.js.org](https://polkadot.js.org/apps/?rpc=wss://peaq.api.onfinality.io/public-ws#/accounts) web app for peaq network. 2. Connect your wallet if not done so already as outlined in the [Token Guide](/peaqchain/build/getting-started/get-test-tokens). 3. Once your account(s) are connected, proceed to refresh the page. Your account(s) will now populate the page. substrate-token-transfer-1 4. Clicking on your wallet account's name will bring up a panel on the right that displays information about that wallet account. This public address (SS58 - Substrate) can be used to receive \$PEAQ. substrate-token-transfer-2 ### EVM → Substrate In the current system, sending tokens directly from an EVM-based wallet (which uses an H160 address) to a Substrate wallet (which uses an SS58 address) is not possible due to the differences in their address formats. However, we provide a solution called **Address Unification**, which creates a link between the two types of addresses. Through our specialized **pallets** (modular runtime components in Substrate), a Substrate wallet can generate a unique **linked H160 address**. Once this linkage is established, an EVM wallet can send tokens to the generated H160 address, and the funds will automatically appear in the corresponding Substrate wallet. If you want to unify your addresses and enable this transfer, you can do so now via our **Developer Extrinsics** page on [peaq.polkadot.js.org](https://polkadot.js.org/apps/?rpc=wss://peaq.api.onfinality.io/public-ws#/extrinsics) or [agung.polkadot.js.org](https://polkadot.js.org/apps/?rpc=wss://wss-async-agung.peaq.xyz#/extrinsics). This process ensures that your Substrate wallet can seamlessly receive tokens from EVM-based wallets while maintaining compatibility across both ecosystems. These instructions are shown below: 1. Navigate to the relevant polkadot.js.org link above for the network you are trying to unify addresses for. 2. On the `Developer->Extrinsics` tab, please select the following parameters: * The substrate account selected (e.g. **TESTER**) will be the recipient of the tokens. * submit the following extrinsic: **addressUnification claimDefaultAccount()** substrate-token-transfer-3 3. Once these parameters are set you can click on the Submit Transaction button. 4. An authorization prompt will need to be approved to execute the transaction. Click on the sign and submit button. substrate-token-transfer-4 5. Enter your wallet password and proceed to sign the transaction from your wallet extension. 6. You should see a green check mark notification indicating that the extrinsic was successful. substrate-token-transfer-5 7. In order to read the generated H160 address, you must go to the `Developer->Chain State` tab. Please select the following: * selected chain state query: **addressUnification evmAddress()** * The substrate account selected (e.g. **TESTER**) is the previously used wallet. 8. Now click on the + to query the state. Your bound address will be displayed. substrate-token-transfer-6 9. To execute a transfer from EVM to Substrate open up your MetaMask wallet. 10. Click the send button to transfer tokens over. In the `to` field, copy and paste the H160 address that was read in step 8. substrate-token-transfer-7 11. Submit the transfer. ### Verify transfer Substrate Wallet before transfer: substrate-token-transfer-8 Substrate Wallet after transfer: substrate-token-transfer-9 As you can see the tokens were successfully transferred over! ## Send \$PEAQ ### Substrate → Substrate 1. To send, locate the “send” button on the account you wish to send from. substrate-token-transfer-10 2. Input the Substrate address of the user you wish to send to, the amount of \$PEAQ you wish to send to the recipient, and proceed to click the “Make Transfer” button. substrate-token-transfer-11 3. This transfer extrinsic needs to be signed, proceed to “Sign and Submit” the transaction. 4. Enter your wallet password and proceed to sign the transaction from your wallet extension. 5. You should see a green check mark notification indicating that the transfer was successful. substrate-token-transfer-12 ### Substrate → EVM When transferring funds from a Substrate Wallet to an EVM Wallet, the recipient H160 address must be converted to an SS58 Address. Please see the [EVM Token Transfer](/peaqchain/build/getting-started/how-to-send-receive-peaq/evm-token-transfer#substrate-→-evm) guide in the Substrate→EVM section to see how to convert. Once you have obtained the converted address, please follow the same procedure as indicated in the above section for [Substrate→Substrate](/peaqchain/build/getting-started/how-to-send-receive-peaq/substrate-token-transfer#substrate-→-substrate-2) transfers. # Installing peaq SDK Source: https://docs.peaq.xyz/peaqchain/build/getting-started/install-peaq-sdk The quickest way to get started building on peaq is to use one of our prebuilt SDKs. They offer robust capabilities in DID, RBAC, and Storage - all communicated via the peaq blockchain. Please check out the official [SDK Reference](/peaqchain/sdk-reference/home) for more detailed information. ## Prerequisite: * [Node.js](https://nodejs.org/en/download/package-manager) version at least 16.x, recommended to use the latest one. ## Installation: Create a new JavaScript project and follow the steps below to set up a new instance of the peaq sdk. Run command `npm init -y` to create a Node.js project. ```bash npm theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm i @peaq-network/sdk ``` ```bash yarn theme={"theme":{"light":"github-light-default","dark":"github-dark"}} yarn add @peaq-network/sdk ``` In the `package.json` file, add `"type": "module"` to enable ES Modules. Create a new `.js` file. Write the JavaScript code that interacts with the SDK. Run command: `node file_name.js` to execute. After completing these steps the peaq sdk will have been successfully installed at your local JavaScript Project. For more information about the capabilities it offers check out the [JavaScript SDK Reference](/peaqchain/sdk-reference/javascript/create-instance). # Python SDK Quickstart ## Prerequisite: * [Python](https://www.python.org/downloads/) Version 3.8+ installed on your system. ## Installation: Mac/Linux: ```bash npm theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python -m venv peaq source peaq/bin/activate ``` Windows: ```bash npm theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python -m venv peaq peaq\Scripts\activate ``` ```bash npm theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-sdk==0.2.1 ``` ```bash npm theme={"theme":{"light":"github-light-default","dark":"github-dark"}} touch requirements.txt ``` Add `python-dotenv==1.1.0` to this file to enable environment variables. Install with `pip install -r requirements.txt`. Create a new `.py` file. Write Python code that interacts with the SDK. Run command: `python file_name.py` to execute. For more information about the Python SDK, take a look at the [Python SDK Reference](/peaqchain/sdk-reference/python/create-instance). # Token List Source: https://docs.peaq.xyz/peaqchain/build/getting-started/token-list The **peaq Token List** provides a canonical, up-to-date registry of ERC-20 and native tokens available on the **peaq Mainnet**. Developers, dApps, and wallets can reference this list to display verified tokens, ensure accurate metadata, and validate token contracts across the ecosystem. This list covers tokens on the peaq network (chainId 3338) only. The official PEAQ token addresses on other chains (Ethereum, BNB Smart Chain, Base, Solana) live on the [PEAQ Token Contracts](/peaqchain/learn/peaq-token-contracts) page. You can view the live JSON directly at: [https://tokenlist.peaq.xyz](https://tokenlist.peaq.xyz) ## Overview The token list follows the [Uniswap Token List standard](https://tokenlists.org/), making it compatible with most wallets, DEX interfaces, and indexers. Each entry includes: * **Token metadata** - name, symbol, decimals, and logo URI * **Tags** - classify the token (e.g., `Stablecoin`, `OFT`, `ERC20`, `peaqNative`) * **Extensions** - include helpful context such as Coingecko IDs, bridge information, and origin chain IDs This list ensures that applications using the peaq network can consistently identify tokens and display them with the correct attributes. ## JSON Structure | Key | Description | | ----------- | -------------------------------------------------------- | | `name` | Name of the token list (e.g., `"peaq Token List"`) | | `version` | Semantic version of the list (`major`, `minor`, `patch`) | | `timestamp` | Last update timestamp in ISO format | | `tags` | Classification of token types | | `tokens` | Array of token metadata objects | Each token entry contains: | Field | Description | | ------------ | -------------------------------------------------------------------------- | | `chainId` | Network chain ID (peaq = **3338**) | | `address` | Contract address of the token | | `symbol` | Short ticker symbol (e.g., `USDC`) | | `name` | Full token name | | `decimals` | Number of decimal places | | `logoURI` | IPFS link to the token logo | | `tags` | Categories describing the token | | `extensions` | Optional metadata (website, Coingecko ID, origin chain, bridge info, etc.) | ## Token Categories | Tag | Description | | -------------- | ---------------------------------------------------------------- | | **Stablecoin** | Tokens pegged to external assets such as USD. | | **OFT** | LayerZero Hydra Omnichain Fungible Token representation on peaq. | | **wNative** | ERC-20 representation of peaq’s native token. | | **peaqNative** | Tokens natively created on peaq. | | **ERC20** | Standard ERC-20 tokens deployed on peaq. | | **Precompile** | Native precompiled contracts for system tokens. | | **LST** | Liquid staking tokens (e.g., wrapped or derivative PEAQ). | ## Featured Tokens on peaq Mainnet | Symbol | Name | Category | Address | Notes | | ----------- | ---------------------------- | ----------------------- | -------------------------------------------- | ----------------------------------------------- | | **PEAQ** | peaq token | Native / Precompile | `0x0000000000000000000000000000000000000809` | Primary network token used for gas and staking. | | **WPEAQ** | Wrapped PEAQ | Wrapped Native / ERC-20 | `0x3cD66d2e1fac1751B0A20BeBF6cA4c9699Bb12d7` | ERC-20 Wrapped PEAQ. | | **USDC** | USD Coin | Stablecoin / OFT | `0xbba60da06c2c5424f03f7434542280fcad453d10` | Bridged via LayerZero Hydra from Ethereum. | | **USDT** | Tether | Stablecoin / OFT | `0xf4d9235269a96aadafc9adae454a0618ebe37949` | Bridged via LayerZero Hydra from Ethereum. | | **WETH** | Wrapped ETH | Bridged Native / OFT | `0x6694340fc020c5e6b96567843da2df01b2ce1eb6` | LayerZero Hydra OFT from Ethereum. | | **SLC** | Silencio | Native ERC-20 | `0x5c3126bfb9a68a7021d461230127470b3824886b` | Auditory layer for machine perception. | | **DEUS** | XMaquina (DEUS) | Native ERC-20 | `0x940a319b75861014a220d9c6c144d108552b089b` | Humanoid robotics and physical AI access. | | **RICE** | Rice Finance | Bridged ERC-20 / OFT | `0x1190fb50226b54958e284bd674eff346936af885` | Cross-chain AI research token from BNB Chain. | | **CODEC** | CodecFlow | Bridged ERC-20 / OFT | `0xee311e67825c22e5c7238546431651686ce91421` | Operator platform built on VLAs. | | **AUKI** | Auki Labs | Bridged ERC-20 / OFT | `0xf67db9d00401d9e883208882f5c100d7482b083d` | Spatial AI sensors for AR experiences. | | **OVR** | Over the Reality | Bridged ERC-20 / OFT | `0x08e6d50169863fd9beb88687ce1573cf9d6d1484` | Decentralized 3D mapping & physical AI. | | **ROBOT** | RoboStack | Bridged ERC-20 / OFT | `0xfd9d1b4a0384396a595345886d1e1d8abf439cef` | Robotics compute and environment modeling. | | **TOPS** | IG3 | Native ERC-20 | `0x6c1ca31a9f3a57bb680f82a8fe97fc00ac4aad21` | Edge network for real-time AI. | | **wstPEAQ** | Wrapped Parasail Staked PEAQ | LST / Native ERC-20 | `0xe5330a9fba99504c534127d39727729899c9a506` | Liquid staking representation of staked PEAQ. | | **ANYONE** | Anyone | Bridged ERC-20 / OFT | `0xe67F39FbE8C24ef8b3542efED1eE9963cEFC1f2a` | Decentralized VPN services. | | **BREW** | Homebrew Robotics Club | Bridged ERC-20 / OFT | `0x7f6c37d043d4c56371ab72f2f7c64d29e5793cdc` | Affordable robotics solutions. | > **Tip:** To verify any token address or metadata, look the address up on [peaq.subscan.io](https://peaq.subscan.io/). ## How to Use the Token List You can integrate the token list in your dApp or service using standard JSON retrieval methods. ### Example (JavaScript) ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const TOKENLIST_URL = "https://tokenlist.peaq.xyz/"; async function fetchTokenList() { const response = await fetch(TOKENLIST_URL); const data = await response.json(); console.log("peaq tokens:", data.tokens); } fetchTokenList(); ``` # PEAQ Token Contracts Source: https://docs.peaq.xyz/peaqchain/learn/peaq-token-contracts The official PEAQ token contract addresses on peaq, Ethereum, BNB Smart Chain, Base, and Solana. This page is the canonical registry of every official PEAQ token contract. It mirrors [peaq.xyz/token](https://www.peaq.xyz/token). Impostor tokens named peaq circulate on pump.fun, Clanker, and other launchpads, some with copycat addresses and faked liquidity. peaq will never DM you a contract address or ask you to migrate tokens. If an address is not on this page, do not trust it. ## PEAQ on peaq PEAQ is the native token of the peaq network. It pays for gas and secures the chain through staking. On the Substrate side it exists at protocol level and has no contract address. For EVM tooling it is exposed through a precompile, and WPEAQ is the canonical wrapped ERC-20. | Token | Standard | Address | Decimals | | -------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------- | | PEAQ | Native ERC-20 precompile | [`0x0000000000000000000000000000000000000809`](https://peaq.subscan.io/account/0x0000000000000000000000000000000000000809) | 18 | | WPEAQ (Wrapped PEAQ) | ERC-20, wrapped native | [`0x3cD66d2e1fac1751B0A20BeBF6cA4c9699Bb12d7`](https://peaq.subscan.io/account/0x3cD66d2e1fac1751B0A20BeBF6cA4c9699Bb12d7) | 18 | The LayerZero OFT adapter [`0xe4103e80c967f58591a1d7cA443ed7E392FeD862`](https://peaq.subscan.io/account/0xe4103e80c967f58591a1d7cA443ed7E392FeD862) is the bridge contract that locks native PEAQ for cross-chain transfers. It is infrastructure, not a token — do not add it to your wallet. ## PEAQ on other chains Every deployment below is a LayerZero Omnichain Fungible Token (OFT), pegged 1:1 to native PEAQ and mint/burn-bridged through LayerZero V2. These are the only official PEAQ deployments outside the peaq network. | Chain | Address | Decimals | | --------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | | Ethereum | [`0x1EeF208926667594E5136e89D0e9DD6907959197`](https://etherscan.io/token/0x1EeF208926667594E5136e89D0e9DD6907959197) | 18 | | BNB Smart Chain | [`0x8b9Ee39195eA99d6ddD68030F44131116bc218F6`](https://bscscan.com/token/0x8b9Ee39195eA99d6ddD68030F44131116bc218F6) | 18 | | Base | [`0x9B56B112BbD6343a8961a093315A9A60b8cB1F36`](https://basescan.org/token/0x9B56B112BbD6343a8961a093315A9A60b8cB1F36) | 18 | | Solana | [`PEAQjk7SRS6rXHVFFmpRr7zrC4g5ZuEebpwTxvaLr3b`](https://explorer.solana.com/address/PEAQjk7SRS6rXHVFFmpRr7zrC4g5ZuEebpwTxvaLr3b) | 9 | Cross-chain transfers use 6 shared decimals under the hood, so bridged amounts are truncated to 6-decimal precision regardless of a deployment's local decimals (18 on the EVM chains, 9 on Solana). ## How to verify an address yourself * Check it against this page or [peaq.xyz/token](https://www.peaq.xyz/token). * Look it up in [LayerZero's deployment registry](https://docs.layerzero.network/v2/deployments/chains/peaq): official OFT legs are pegged to native PEAQ on the peaq chain. * On the destination explorer, the EVM OFT contracts are verified-source contracts named `PeaqOFT` with symbol `PEAQ`. On Solana, the mint's authority is the OFT program's store account, and its supply is minted only by LayerZero messages from the peaq adapter. * Tokens on peaq itself are registered in the [peaq Token List](/peaqchain/build/getting-started/token-list). # Tokenomics Source: https://docs.peaq.xyz/peaqchain/learn/tokenomics **Ticker:** PEAQ **Total supply at genesis:** 4,200,000,000 *Note on supply terms: an earlier version of this page called the 4,200,000,000 PEAQ total supply at genesis a max supply. It is not. PEAQ follows the disinflationary schedule in the [Inflation](#inflation) section, which reaches 5,667,620,228.64 PEAQ at the end of year 17. The runtime does not enforce that figure as a cap today; a cap will be added in a later runtime upgrade.* ## Utility Like with any other Layer 1 blockchain's native asset, PEAQ’s primary utility is in enabling the most fundamental interactions on the network: paying transaction fees, producing blocks in a censorship-resistant way via a staking and slashing mechanism, and governing the network. Here is more information on these utilities: ### Transaction fees  PEAQ is used for transaction fee payment on the peaq blockchain. The amount of PEAQ needed for any particular transaction is calculated based on the weight, length, and other parameters of the transaction. As with other layer-1 blockchains, it is impossible to use the network without PEAQ, as no transactions can be carried out without paying the fee in PEAQ. Just like with dApps on Ethereum, all transactions from DePINs on peaq will require a fee in PEAQ. The millions of machines, vehicles, and sensors across these DePINs are expected to generate billions of transactions. ### Staking peaq relies on the work of Validators and Delegators for block production. To ensure that blocks are produced in an honest, censorship-resistant, and reliable way, peaq has a staking mechanism, which incentivizes honest work from Validators and Delegators. Validators need to provide a staking deposit to run a Validator node to have “skin in the game”. Delegators can delegate their stake to Validators of their choice in order to back them. Only those Validators with enough backing (stake) are able to produce blocks. Thus Delegators actively govern which Validators are producing blocks and can thereby ensure that trusted and well-operating Validators are active. If the Validator misbehaves or goes offline, its Delegator stops earning. Thus Delegators need to continuously check Validators’ behavior and manage their stake allocation actively. In Economics 2.0 terms these are chain validators. Trust Validators are the staking role that verifies machine activity; public staking consolidates there over the transition described in the [launch blog](https://www.peaq.xyz/blog/peaq-economics-2-0-goes-live-on-mainnet#s-4-stakers-and-node-operators). ### Governance Holding PEAQ will enable you to gradually guide the network by voting on key decisions via onchain governance concerning its development and future. **Initial Post-Launch Period:** Community suggestions are gathered via polls, AMAs, and calls. The foundation holds veto power to safeguard stability. **Midterm:** Governance shifts onchain. PEAQ holders, the Council and the Technical Committee can propose upgrades, parameter changes, or treasury spending. Council decisions can be overturned by the community; the Technical Committee can fast-track urgent or block risky proposals. **Longterm:** A fully open, advanced onchain governance model enables any PEAQ holder to propose changes and vote transparently onchain. Safeguards promote broad participation and approved proposals are implemented automatically. The community also manages funds via onchain governance. ## Inflation The inflation rate is initially set at 3.5%, ensuring sufficient incentives for early adopters. However, the inflation rate will decrease annually by 10% and will stabilize once the network reaches 1% inflation, following a disinflationary model. The first disinflation will occur at target block **7,890,590**. In the future, the community will be able to vote on adjustments to the inflation/deflation model via the network's governance. ### Disinflation Schedule The following table outlines the disinflation schedule, showing the inflation rate each year, newly minted tokens, and end-of-year supply: | Year | Inflation Rate | Newly Minted Tokens | End-of-Year Supply | | ---- | -------------- | ------------------- | ------------------ | | 0 | – | – | 4,200,000,000.00 | | 1 | 3.50% | 147,000,000.00 | 4,347,000,000.00 | | 2 | 3.15% | 136,930,500.00 | 4,483,930,500.00 | | 3 | 2.84% | 127,119,429.68 | 4,611,049,929.68 | | 4 | 2.55% | 117,650,939.63 | 4,728,700,869.31 | | 5 | 2.30% | 108,587,521.76 | 4,837,288,391.07 | | 6 | 2.07% | 99,998,971.96 | 4,937,287,363.03 | | 7 | 1.86% | 91,948,808.82 | 5,029,236,171.85 | | 8 | 1.67% | 84,516,259.87 | 5,113,752,431.72 | | 9 | 1.51% | 77,781,209.64 | 5,191,533,641.36 | | 10 | 1.36% | 71,819,757.27 | 5,263,353,398.63 | | 11 | 1.22% | 66,701,842.14 | 5,330,055,240.77 | | 12 | 1.10% | 62,490,937.16 | 5,392,546,177.93 | | 13 | 1.00% | 53,925,461.78 | 5,446,471,639.71 | | 14 | 1.00% | 54,464,716.40 | 5,500,936,356.11 | | 15 | 1.00% | 55,009,363.56 | 5,555,945,719.67 | | 16 | 1.00% | 55,559,457.20 | 5,611,505,176.87 | | 17 | 1.00% | 56,115,051.77 | 5,667,620,228.64 | The schedule above reaches **5,667,620,228.64 PEAQ** at the end of year 17. That figure is the projected supply under the schedule, not a hard cap: the runtime enforces no maximum supply today, and the 1% floor keeps minting after year 17. A supply cap will be added in a later runtime upgrade. ### Economics 2.0 The Economics 2.0 contracts went live on peaq mainnet on 2026-09-01 ([paper](https://www.peaq.xyz/economics-2-0); mechanics on the [Economics 2.0 concept page](/peaqos/concepts/economics-2-0)). Machines activate on a subscription tier priced in USD and bond PEAQ at the oracle rate; the first PEAQ price was committed on 2026-09-03 and the SDKs and CLI gained activation support on 2026-09-04. Machine bonds are held in the MachineSubscription contract ([0x9e37AD189c334C92e6B8a812Ca4c02f35Ac43895](https://peaq.subscan.io/account/0x9e37ad189c334c92e6b8a812ca4c02f35ac43895)) and count as circulating supply until they are burned. The bonded total is live on the [Machine Explorer economics page](https://machines.peaq.xyz/economics); activation steps are on the [activate page](/peaqos/functions/activate). A lapsed bond decays over a 14-day runoff, half to burn and half to the treasury; today the burn address in InfoDesk is unset, so the burn half does not reduce total supply yet. The block reward split changed to 70% treasury / 30% validators and delegators with runtime spec 113, enacted on mainnet on 2026-09-06 at 11:36 UTC (block 11,512,213). #### Tokenomics Adjustments: System wallet simplification and transparency improvements As peaq Economics 2.0 goes live and as announced July 1 in our [market structure update](https://www.peaq.xyz/blog/behind-the-chart-an-update-on-peaq-liquidity-supply-and-market-structure), the broader wallet structure is also being updated. This includes both system wallets and other Foundation-controlled wallets, which have historically been spread across a larger number of addresses. These wallets are now being consolidated into a smaller and clearer structure, making balances and movements easier to track for the community, exchanges, and other stakeholders. It also allows legacy wallets created for earlier initiatives, such as launch-era community campaigns, Get Real, and completed capital contribution activities, to be retired. The five legacy system wallets (Security Treasury, General Treasury, DePIN Incentive Pool, DePIN Staking, Machine Subsidization Pool) were emptied into the Treasury Reserve in partial moves; the remaining tokens follow by the end of September 2026. Legacy campaign wallets (Get Real, Early Adopters, Initial Community Campaign, Capital Contributions) moved to the Dynamic Treasury. The Community Reserve and Security Reserve moved to the Treasury Reserve. Every transfer is listed with its transaction hash in the [Economics 2.0 launch blog](https://www.peaq.xyz/blog/peaq-economics-2-0-goes-live-on-mainnet#s-5-tokenomics-adjustments-system-wallet-simplification-and-transparency-improvements). The foundation-controlled treasury now consists of these wallets: | Wallet | Address | Circulating supply | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------ | | Treasury Reserve | [0x4b4c2aABaDA91534B217a5B70E3b43A678F88D49](https://peaq.subscan.io/account/0x4b4c2aabada91534b217a5b70e3b43a678f88d49) | Excluded | | Ecosystem & Treasury Wallet | [0x46E967F90f9426823603db6765123Ba27AB983C0](https://peaq.subscan.io/account/0x46e967f90f9426823603db6765123ba27ab983c0) | Excluded | | Core Contributors Reserve | [0xaa53A7C827929ACA77FF846F7A5e526895a00c99](https://peaq.subscan.io/account/0xaa53a7c827929aca77ff846f7a5e526895a00c99) | Excluded | | Dynamic Treasury | [0x9df7202b08e68A14C3F732E1927BdA367835470E](https://peaq.subscan.io/account/0x9df7202b08e68a14c3f732e1927bda367835470e) | Counted | None of these moves change the circulating supply or the emission schedule. Tokens that were already classified as circulating moved to circulating wallets (the Dynamic Treasury). Non-circulating tokens moved to the Treasury Reserve and stay non-circulating. Live balances of all excluded accounts are on [Subscan](https://peaq.subscan.io/token/PEAQ) under Token Distribution. ## Economics 1.0: Allocation at genesis *The table is horizontally scrollable →* | **Allocation Category** | **Supply %** | **Distribution Address** | **Allocation Subcategory** | **Address Type** | **Supply %** | **Token Amount** | **Available at TGE** | **Lock-Up (Months)** | **Release after Lock-Up** | **Linear Vesting (Months)** | | ----------------------- | ------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------- | -------------------- | -------------------- | ------------------------- | --------------------------- | | Investors | 34 | [5Ejz...rbk3](https://peaq.subscan.io/account/5EjzcRN7j5okZsQ9ujWTXZopzrxAdbAnbWTYWK6z7F74rbk3) | Funding (Pre Seed) | / | 7 | 294,000,000 | 11,025,000 | 6 | 11,025,000 | 24 | | | | | Funding (Seed) | / | 5 | 210,000,000 | 13,125,000 | 6 | 13,125,000 | 24 | | | | | Funding (Private) | / | 13 | 546,000,000 | 40,950,000 | 6 | 40,950,000 | 18 | | | | | Funding (Pre Launch Private) | / | 9 | 378,000,000 | 28,350,000 | 6 | 28,350,000 | 18 | | Community | 21 | [5FqM...61NJ](https://peaq.subscan.io/account/5FqM4JGTBE1upbmCX8GbRRdYKRgVixZF2bg1JJioh94161NJ) | Community (Community Sale) | LayerZero contract: [0xe410...D862](https://peaq.subscan.io/account/0xe4103e80c967f58591a1d7cA443ed7E392FeD862) | 6 | 252,000,000 | 40,950,000 | 0 | 0 | 6 | | | | | Community (Get Real Campaign) | Foundation managed: [0xB22E...097](https://peaq.subscan.io/account/0xB22E7ebC96b27d9074F6724D5300f964691FA097) | 5 | 210,000,000 | 0 | 3 | 0 | 0 | | | | | Community (Early Adopters Campaign) | Foundation managed: [0x1A4e...bf5b](https://peaq.subscan.io/account/0x1A4ec1fd72Eb76DBe6109038a93f456c7cc1bf5b) | 2.53 | 106,260,000 | 0 | 3 | 0 | 0 | | | | | Community (Initial Community Campaign) | Further distribution address: [5HbJd...JEP](https://peaq.subscan.io/account/5HbJdBpyBwWMo8do8qvts3eHXpgZWhReqBwCRDmypDAJeJEP) | 2 | 84,000,000 | 84,000,000 | 0 | 0 | 0 | | | | | Community (Community Reserve) | Foundation managed: [0xe0a4...9d8F](https://peaq.subscan.io/account/0xe0a4B8d19Fb1E80b4EFbBd30Bd3D3a4158Db9d8F) | 5.47 | 229,740,000 | 0 | 0 | 0 | 0 | | Core Contributors | 11.5 | [5FqVA...DymA](https://peaq.subscan.io/account/5FqVAJqB9y51KUvN8T53oykTo9xZcMuDqE3Miyw1wZygDymA) | Core Contributors | / | 11.5 | 483,000,000 | 0 | 9 | 0 | 36 | | | 8.5 | [5GEde...sJVY](https://peaq.subscan.io/account/5GEdexSEePuLC6PutqFtewhM5mvasKBTuN9vvybrR2gYsJVY) | EoT Labs | / | 8.5 | 357,000,000 | 0 | 9 | 0 | 36 | | Network Security | 5 | [5F4r...Xyy](https://peaq.subscan.io/account/5F4rNUSF2PfLZoB1RnhhYcSRXN3VjWpUR22YFjEg4wjUaXyy) | Network Security (Security Reserve) | Foundation managed: [0x355A...1913](https://peaq.subscan.io/account/0x355A1EC71E87777Cf77e3181f96A2825073f1913) | 3.4583 | 145,248,600 | 0 | 0 | 0 | 0 | | | | | Network Security (Genesis Node Set) | Further distribution address: [5GGkf...SZMx](https://peaq.subscan.io/account/5GGkfFzRctse5eXmZ99ic9XPaCw6zjd985fVYkg3hcSDSZMx) | 0.0417 | 1,750,140 | 1,750,140 | 0 | 0 | 0 | | | | | Network Security (Core Time Lease) | / | 1.5 | 63,000,000 | 0 | 0 | 0 | 24 | | Ecosystem & Treasury | 20 | [5FWuQ...Exdo](https://peaq.subscan.io/account/5FWuQcik2gqEUuhrFMLoEWTNUMmjcpCZfhYQGGNVy5pzExdo) | Ecosystem & Treasury (Market Making & Liquidity) | Further distribution address: [5GEdz...ANWAF](https://peaq.subscan.io/account/5GEdzozaR1sgdUiU6QYhmiVoaGYVYYMbrF1i6yHnso5ANWAF) | 3.75 | 157,500,000 | 157,500,000 | 0 | 0 | 0 | | | | | Ecosystem & Treasury (Reserve) | Foundation managed: [0x4b4c...D49](https://peaq.subscan.io/account/0x4b4c2aABaDA91534B217a5B70E3b43A678F88D49) | 11.18 | 469,560,000 | 0 | 0 | 0 | 0 | | | | | Ecosystem & Treasury (Capital Contributions) | Foundation managed: [0x45AD...33B](https://peaq.subscan.io/account/0x45ADD7eEcF69bB6dE394cDaF317aCA96a4F3b33B) | 0.3 | 12,600,000 | 0 | 0 | 0 | 0 | | | | | Ecosystem & Treasury (Grants) | Foundation managed: [0x46E9...83C0](https://peaq.subscan.io/account/0x46E967F90f9426823603db6765123Ba27AB983C0) | 1.77 | 74,291,837.77 | 0 | 0 | 0 | 0 | | | | | Ecosystem & Treasury (Expansion Reserve) | Foundation managed: [0x7B38...455](https://peaq.subscan.io/account/0x7B38b140d02635f5a983E14467D70e7fE829e455) | 3 | 126,000,000 | 0 | 0 | 0 | 0 | Status on 2026-09-08: the table is the genesis record, several of its foundation-managed wallets have since been consolidated (see [wallet consolidation](#tokenomics-adjustments-system-wallet-simplification-and-transparency-improvements)). Get Real, Early Adopters and Capital Contributions moved to the Dynamic Treasury and are empty. Community Reserve and Security Reserve moved to the Treasury Reserve and are empty. Expansion Reserve holds 45,584 PEAQ, the rest moves to the Dynamic Treasury by the end of September 2026. Initial Community Campaign moved partially, the campaign wallet [0x9B921f6238900178b9bD1d90F0048B6f2Ed9C0e0](https://peaq.subscan.io/account/0x9b921f6238900178b9bd1d90f0048b6f2ed9c0e0) holds about 15M PEAQ pending the final move. Two wallets appear in both tables under different names: Ecosystem & Treasury (Reserve) 0x4b4c…D49 is the Treasury Reserve, and Ecosystem & Treasury (Grants) 0x46E9…83C0 is the Ecosystem & Treasury Wallet. ## Unlocks | **Month** | **PEAQ Unlock** | **Inflation**\* | **Locked PEAQ** | **Unlocked Supply**\*\* | **% Unlocked** | **Total Supply** | | --------- | --------------- | --------------- | --------------- | ----------------------- | -------------- | ---------------- | | 0 | 377,650,940 | 0 | 3,822,349,060 | 377,650,940 | 8.99% | 4,200,000,000 | | 1 | 39,900,000 | 12,250,000 | 3,789,799,060 | 422,450,940 | 10.03% | 4,212,250,000 | | 2 | 39,900,000 | 12,250,000 | 3,757,249,060 | 467,250,940 | 11.06% | 4,224,500,000 | | 3 | 75,110,000 | 12,250,000 | 3,689,489,060 | 547,260,940 | 12.92% | 4,236,750,000 | | 4 | 75,110,000 | 12,250,000 | 3,621,729,060 | 627,270,940 | 14.76% | 4,249,000,000 | | 5 | 75,110,000 | 12,250,000 | 3,553,969,060 | 707,280,940 | 16.60% | 4,261,250,000 | | 6 | 73,010,000 | 12,250,000 | 3,488,309,060 | 785,190,940 | 18.37% | 4,273,500,000 | | 7 | 131,285,000 | 12,250,000 | 3,364,374,060 | 921,375,940 | 21.50% | 4,285,750,000 | | 8 | 100,455,833 | 12,250,000 | 3,271,268,226 | 1,026,731,773 | 23.89% | 4,298,000,000 | | 9 | 106,079,167 | 12,250,000 | 3,172,539,060 | 1,137,710,940 | 26.40% | 4,310,250,000 | | 10 | 106,079,167 | 12,250,000 | 3,073,809,893 | 1,248,690,107 | 28.89% | 4,322,500,000 | | 11 | 106,079,167 | 12,250,000 | 2,975,080,726 | 1,359,669,273 | 31.37% | 4,334,750,000 | | 12 | 106,079,167 | 12,250,000 | 2,876,351,560 | 1,470,648,440 | 33.83% | 4,347,000,000 | | 13 | 106,079,167 | 11,410,875 | 2,777,118,918 | 1,581,291,957 | 36.28% | 4,358,410,875 | | 14 | 106,079,167 | 11,410,875 | 2,677,886,276 | 1,691,935,474 | 38.72% | 4,369,821,750 | | 15 | 88,579,167 | 11,410,875 | 2,596,153,635 | 1,785,078,990 | 40.74% | 4,381,232,625 | | 16 | 88,579,167 | 11,410,875 | 2,514,420,993 | 1,878,222,507 | 42.76% | 4,392,643,500 | | 17 | 88,579,167 | 11,410,875 | 2,432,688,351 | 1,971,366,024 | 44.76% | 4,404,054,375 | | 18 | 88,579,167 | 11,410,875 | 2,350,955,709 | 2,064,509,540 | 46.76% | 4,415,465,250 | | 19 | 88,579,167 | 11,410,875 | 2,269,223,068 | 2,157,653,057 | 48.74% | 4,426,876,125 | | 20 | 88,579,167 | 11,410,875 | 2,187,490,426 | 2,250,796,574 | 50.71% | 4,438,287,000 | | 21 | 88,579,167 | 11,410,875 | 2,105,757,784 | 2,343,940,090 | 52.68% | 4,449,697,875 | | 22 | 88,579,167 | 11,410,875 | 2,025,166,230 | 2,435,942,520 | 54.60% | 4,461,108,750 | | 23 | 88,579,167 | 11,410,875 | 1,944,574,675 | 2,527,944,949 | 56.52% | 4,472,519,625 | | 24 | 88,579,167 | 11,410,875 | 1,863,983,120 | 2,619,947,378 | 58.43% | 4,483,930,500 | | 25 | 85,954,167 | 10,596,272 | 1,785,446,344 | 2,709,080,427 | 60.28% | 4,494,526,772 | | 26 | 42,320,833 | 10,596,272 | 1,750,542,901 | 2,754,580,142 | 61.14% | 4,505,123,044 | | 27 | 42,320,833 | 10,596,272 | 1,715,639,459 | 2,800,079,856 | 62.01% | 4,515,719,316 | | 28 | 42,320,833 | 10,596,272 | 1,680,736,016 | 2,845,579,571 | 62.87% | 4,526,315,588 | | 29 | 42,320,833 | 10,596,272 | 1,645,832,574 | 2,891,079,285 | 63.72% | 4,536,911,860 | | 30 | 42,320,833 | 10,596,272 | 1,610,929,131 | 2,936,579,000 | 64.58% | 4,547,508,132 | | 31 | 42,320,833 | 10,596,272 | 1,576,025,688 | 2,982,078,715 | 65.42% | 4,558,104,404 | | 32 | 23,333,333 | 10,596,272 | 1,560,109,746 | 3,008,590,929 | 65.85% | 4,568,700,676 | | 33 | 23,333,333 | 10,596,272 | 1,544,193,803 | 3,035,103,144 | 66.28% | 4,579,296,948 | | 34 | 23,333,333 | 10,596,272 | 1,528,277,861 | 3,061,615,358 | 66.70% | 4,589,893,220 | | 35 | 23,333,333 | 10,596,272 | 1,512,361,918 | 3,088,127,573 | 67.13% | 4,600,489,492 | | 36 | 23,333,333 | 10,596,272 | 1,496,445,975 | 3,114,639,788 | 67.55% | 4,611,085,764 | | 37 | 23,333,333 | 9,804,321 | 1,479,975,667 | 3,140,914,417 | 67.97% | 4,620,890,085 | | 38 | 23,333,333 | 9,804,321 | 1,463,505,359 | 3,167,189,046 | 68.40% | 4,630,694,406 | | 39 | 23,333,333 | 9,804,321 | 1,447,035,050 | 3,193,463,676 | 68.82% | 4,640,498,727 | | 40 | 23,333,333 | 9,804,321 | 1,430,564,742 | 3,219,738,305 | 69.24% | 4,650,303,048 | | 41 | 23,333,333 | 9,804,321 | 1,414,094,434 | 3,246,012,934 | 69.66% | 4,660,107,369 | | 42 | 23,333,333 | 9,804,321 | 1,397,624,126 | 3,272,287,564 | 70.07% | 4,669,911,690 | | 43 | 23,333,333 | 9,804,321 | 1,381,153,817 | 3,298,562,193 | 70.49% | 4,679,716,011 | | 44 | 23,333,333 | 9,804,321 | 1,364,683,509 | 3,324,836,822 | 70.90% | 4,689,520,332 | | 45 | 0 | 9,804,321 | 1,371,546,534 | 3,327,778,118 | 70.81% | 4,699,324,653 | | 46 | 0 | 9,804,321 | 1,378,409,558 | 3,330,719,415 | 70.73% | 4,709,128,974 | | 47 | 0 | 9,804,321 | 1,385,272,583 | 3,333,660,711 | 70.64% | 4,718,933,295 | | 48 | 0 | 9,804,321 | 1,392,135,608 | 3,336,602,007 | 70.56% | 4,728,737,616 | | 49 | 0 | 9,048,960 | 1,398,469,880 | 3,339,316,695 | 70.48% | 4,737,786,576 | | 50 | 0 | 9,048,960 | 1,404,804,152 | 3,342,031,383 | 70.41% | 4,746,835,536 | | 51 | 0 | 9,048,960 | 1,411,138,424 | 3,344,746,071 | 70.33% | 4,755,884,496 | | 52 | 0 | 9,048,960 | 1,417,472,696 | 3,347,460,759 | 70.25% | 4,764,933,456 | | 53 | 0 | 9,048,960 | 1,423,806,968 | 3,350,175,447 | 70.18% | 4,773,982,416 | | 54 | 0 | 9,048,960 | 1,430,141,240 | 3,352,890,135 | 70.10% | 4,783,031,376 | \*From month 22 the inflation split follows runtime spec 113: 30% to staking, counted as unlocked, and 70% to the treasury, held in an excluded account. Months 1 to 21 use the 40/60 split in force at the time. ### Inflation impact on circulating supply The inflation has the following impact on the circulating supply: * 30% of yearly inflation is paid out as rewards to chain validators and their delegators, the Trust Validator staking share during the transition. * 70% of yearly inflation goes to the general treasury (since runtime spec 113, enacted 2026-09-06; 40% / 60% before that). Rewards paid to validators and delegators count as circulating once transferable. Inflation paid to the general treasury lands in an excluded system account and is not circulating until spent (see the definition below). ### Circulating supply Circulating supply is the total transferable PEAQ balance across all accounts, minus the system and foundation accounts that [Subscan](https://peaq.subscan.io/token/PEAQ) lists as excluded under Token Distribution. CoinMarketCap and CoinGecko republish Subscan's figure. Excluded accounts include the Treasury Reserve, the Ecosystem & Treasury Wallet, the retired system wallets and the legacy campaign wallets named above; Subscan shows the current list and each account's balance. Locked and vesting balances are not transferable and therefore not circulating. PEAQ bonded by machines under Economics 2.0 sits in the MachineSubscription contract as a transferable balance and counts as circulating until it is burned. Earlier history: on December 6, 2024 the 6% CoinList sale allocation was added to the reported circulating supply, following the CoinMarketCap and CoinGecko rule that public-sale tokens count even while locked. That moved the reported figure from 376,976,863 to 624,514,387 PEAQ on that date. ### Important note: locked tokens can be staked The vesting and lockup schedule applies only to the initial allocations of the total supply at genesis and does not apply to newly minted tokens generated by inflation. Tokens, regardless of their status (locked, unlocked, or under vesting), are eligible for staking. Users can participate as validators or delegators and stake their tokens. Staking rewards are immediately available and fully unlocked upon receipt. ### Important note: the unlock process may vary slightly Certain allocations are not locked or vested immediately at genesis. Specifically, unlock schedules related to community and ecosystem growth campaigns are based on projections and cannot be precisely determined at the outset. These unlocks are calculated using estimates and may be subject to adjustments over time. ## Inflation and transaction fee distribution The PEAQ token follows a disinflationary model, starting with an inflation rate of 3.5%, which decreases by 10% annually until it reaches 1%. Newly minted tokens and transaction fees are split two ways on chain since runtime spec 113 (enacted on mainnet on 2026-09-06 at 11:36 UTC, block 11,512,213). The Economics 2.0 [PEAQ Flow](https://www.peaq.xyz/blog/peaq-economics-2-0-goes-live-on-mainnet#s-1-phase-1-overview) distributes the same emissions to four destinations: 30% Trust Validator staking (which pays today's validators and delegators during the transition), 30% Machine Pool, 20% Treasury, 20% Activation Token Provision Pool. The last three are funded from the treasury's 70% share. On chain, the block reward pallet in runtime spec 113 has exactly two sinks, treasury 70% and staking 30%; the split of the 70% into Machine Pool, Treasury and Activation Token Provision Pool is applied off chain. **1. Trust Validator staking**: 30%, [modlpoolStake](https://peaq.subscan.io/account/5EYCAe5cKPAoFh2HnQQvpKqRYZGqBpaA87u4Zzw89qPE58is), paid to chain validators and their delegators during the transition This pool ensures efficient, reliable, and censorship-resistant block production. All funds are distributed directly to validators and delegators based on the validator's total stake and those of the delegators. Validators can set custom delegator fees. **2. General Treasury**: 70%, [modlpy/trsry](https://peaq.subscan.io/account/5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z) The funds of this pool finance the ongoing operations of the ecosystem, supporting further research and development around the network, its core function set, and other key features. The account keeps receiving new issuance while its legacy balance is moved to the Treasury Reserve as part of the wallet consolidation; both are true at the same time. Under Economics 2.0 the treasury also receives the burn-or-treasury half of lapsed machine bonds and half of the USDT activation proceeds (see the [Economics 2.0 concept page](/peaqos/concepts/economics-2-0)). Verified on chain over the 10,000 blocks after enactment: the general treasury received about 183,000 PEAQ and the three pools below received nothing; over the 10,000 blocks before enactment they received about 27,000 (security), 40,000 (DePIN) and 13,000 (subsidization) PEAQ. ### Pools that no longer receive block rewards Until spec 113 the split was 40% validators and delegators, 10% security treasury, 25% general treasury, 20% DePIN incentive pool, 5% machine subsidization pool. These accounts receive no new block rewards. Their balances were moved to the Treasury Reserve in partial transfers, with the remainder following by the end of September 2026 (see [wallet consolidation](#tokenomics-adjustments-system-wallet-simplification-and-transparency-improvements)): The General Treasury is the fifth of the five consolidated system wallets. It still receives the 70% share and is described above, so it is not listed here. * **Security Treasury** - [modlpoolCoret](https://peaq.subscan.io/account/5EYCAe5cKPAm67h3SeVgRKhZNE8fFKZwRreJo3kvGZAFjRe3): funded the purchase of Coretime. * **DePIN Incentive Pool** - [modlpoolDPInc](https://peaq.subscan.io/account/5EYCAe5cKPAmD11bPWaQjEYQ1GV7H56nqBpYt7XN2ZfKigDg) & [modlpoolDPStk](https://peaq.subscan.io/account/5EYCAe5cKPAmD15dKS4VkZXmNmuHpuBdBDF3113P6gBLRTZE): incentives for DePINs building on peaq and their liquidity across Machine DeFi protocols. * **Machine Subsidization Pool** - [modlpoolSubsi](https://peaq.subscan.io/account/5EYCAe5cKPAoFioiNofJ5BLTBbhsYn7anzfPTNUi1mvrMFdv): subsidized the onboarding of revenue-generating machines as Machine RWAs. #### Initial control of treasury pools Until on-chain governance is introduced, all Treasury pools are managed by the peaq foundation. The staking pool is not managed by the peaq foundation. #### Updating the distribution The above distribution presents an initial proposal by the peaq foundation. With future updates, the community will be able to adjust the distribution and usage of funds by voting via on-chain governance mechanisms. # SDK Reference Source: https://docs.peaq.xyz/peaqchain/sdk-reference/home Welcome to the peaq SDK reference documentation. Our SDKs enable developers to build applications that interact with the peaq network, providing tools for creating, managing, and interacting with decentralized machine identities, services, and data. ## Available SDKs Currently, we offer the following SDKs: Power any machine — from humanoids to drones — with on-chain identity, data, and payments on peaq. Start building using the JavaScript SDK. Our JavaScript SDK provides a comprehensive set of tools for web and Node.js applications to interact with the peaq network. New SDK to get you building on peaq with Python! Build regulated security tokens and digital assets on peaq. ## Core Features All peaq SDKs provide access to the following core functionalities: * **Identity Management**: Create and manage decentralized identities for machines, services, and users. * **Data Verification**: Validate and verify data integrity by reading DIDs running validation logic on the peaq network. * **Storage**: Store CIDs, hash values, etc. using peaq storage. * **Role Based Access Control**: Create your own management system by utilizing RBAC functions. (Python - coming soon) * **Precompile Interaction**: Interact with peaq pallets via precompiles for EVM transactions. * **Transaction Processing**: Create, sign, and submit transactions to the peaq network. ## Support If you have questions or need assistance with any of our SDKs, please: * Join our [Discord community](https://discord.gg/UKTFkPWsyH) * Check out our [GitHub repositories](https://github.com/peaqnetwork) ## Feedback We're continuously improving our SDKs based on developer feedback. If you have suggestions or encounter issues, please open an issue in the respective GitHub repository or contribute directly through pull requests. # Create Instance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/create-instance The [JavaScript SDK](https://www.npmjs.com/package/@peaq-network/sdk) supports EVM and Substrate transactions. In order to communicate and generate transactions using the peaq blockchain an instance must be created. ## createInstance(baseUrl, chainType, seed) | Parameter | Type | EVM | Substrate | Description | | ------------- | ----------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **baseUrl** | `string` | Required | Required | RPC/WSS URL used to connect to the blockchain. Reference [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq) for supported URLs. Use **RPC** for EVM and **WSS** for Substrate (and EVM reads). | | **chainType** | `ChainType` | Required | Optional | Defines whether the instance will generate EVM or Substrate transactions (defaults to Substrate). Accessible via the SDK. | | **seed** | `string` | N/A | Optional | Mnemonic phrase used in Substrate instances to execute write transactions on behalf of the user. | ```javascript EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // No direct API connection made to EVM so disconnect is unnecessary. ``` ```javascript Substrate theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env['SUBSTRATE_SEED']; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // disconnect when finished await sdk.disconnect(); ``` Now you will have an **sdk** object that can be used to execute the [DID](/peaqchain/sdk-reference/javascript/did-operations), [RBAC](/peaqchain/sdk-reference/javascript/rbac-operations/role), and [Storage](/peaqchain/sdk-reference/javascript/storage-operations) operations on the peaq blockchain. The EVM instance only constructs transaction objects, and does not send the transactions itself. This gives the user the ability to send peaq transactions themselves without ever sending a private key through the sdk. However, if you would like this process abstracted away, we do provide a [Send Evm Transaction](/peaqchain/sdk-reference/javascript/send-evm-tx) function. To mitigate concerns on how we use your seed/private key we have made our [peaq js](https://github.com/peaqnetwork/peaq-js) SDK fully open source. # DID Operations Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/did-operations This concept has been absorbed into peaqOS. See [peaqID](/peaqos/concepts/peaqid) for the current identity model. Using the instance created on the previous pages you are able to execute the peaq DID functionalities that allow for the creation of a DID Document. ## create(name, address, seed, customDocumentFields) Enables the creation of a decentralized identity on-chain for your given entity. | Parameter | Type | EVM | Substrate | Description | | ------------------------ | ---------------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID to be created. | | **address** | `string` | Required | Optional | Wallet address that is used to create the DID Document. If seed set for substrate, defaults to public address for that seed. | | **seed** | `string` | N/A | Optional | Mnemonic phrase used in Substrate instances to execute write transactions on behalf of the wallet. Not necessary when seed set at instance creation. | | **customDocumentFields** | `CustomDocumentFields` | Optional | Optional | Used to set specific fields of the DID Document. If none is set, an empty document will be generated with id and controller set to the signer's address. | ### customDocumentFields | Parameter | Type | EVM | Substrate | Description | | ----------------- | ---------------- | -------- | --------- | ------------------------------------------------------------------------------------------- | | **verifications** | `Verification[]` | Optional | Optional | Stores the verification method that is tied to the wallet used to create this DID Document. | | **signature** | `Signature` | Optional | Optional | Used to store an issuer's signature. | | **services** | `Services[]` | Optional | Optional | Links data, endpoints, or other metadata to the DID Document. | #### Verification | Parameter | Type | EVM | Substrate | Description | | ---------------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **type** | `string` | Optional | Optional | Allows user to manually set the type of verification to be used with the public key tied to the DID Document.

**EVM Wallets:**
- `EcdsaSecp256k1RecoveryMethod2020`
**Substrate Wallets:**
- `Ed25519VerificationKey2020`
- `Sr25519VerificationKey2020` | | **publicKeyMultibase** | `string` | Optional | Optional | Allows user to manually set the public Key MultiBase they would like to use for their verification method. | #### Signature | Parameter | Type | EVM | Substrate | Description | | ---------- | -------- | -------- | --------- | ----------------------------------------------- | | **type** | `string` | Required | Required | The algorithm used when generating a signature. | | **issuer** | `string` | Required | Required | Entity that issued the signature. | | **hash** | `string` | Required | Required | Hash value of the generated signature. | #### Services | Parameter | Type | EVM | Substrate | Description | | ------------------- | -------- | -------- | --------- | --------------------------------------------------------------- | | **id** | `string` | Required | Required | Identifier used to indicate what type of service is being used. | | **type** | `string` | Required | Required | Declares the type of service object being referenced. | | **serviceEndpoint** | `string` | Optional | Optional | URI/URL that is used to point to another data storage location. | | **data** | `string` | Optional | Optional | Data value that can be stored at this service. | ### Create DID Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "DID_NAME"; // possible example of custom document fields a DePIN could create const customFields = { services: [{ id: "#ipfs", type: "machineData", serviceEndpoint: "https://ipfs.io/ipfs/QmYwAPJzv5CZsnAzt8auVTLv3jE9n1Tz8exzhTHtVMJd3t" }] }; const tx = await sdk.did.create({ name: name, address: EVM_ADDRESS, customDocumentFields: customFields }); // Create DID using sendEvmTx() function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.create({}) return object { to: '0x0000000000000000000000000000000000000800', data: '0xcc4a70ca0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000084449445f4e414d45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018a3061333336343639363433613730363536313731336133303738333934353635363136323331363134333633363233313431333733303331363134353636343134323330333034363333363233383631333233373335363133333339333633343336333633343331343331323333363436393634336137303635363137313361333037383339343536353631363233313631343336333632333134313337333033313631343536363431343233303330343633333632333836313332333733353631333333393336333433363336333433313433326135393061303532333639373036363733313230623664363136333638363936653635343436313734363131613433363837343734373037333361326632663639373036363733326536393666326636393730363637333266353136643539373734313530346137613736333534333561373336653431376137343338363137353536353434633736333336613435333936653331353437613338363537383761363835343438373435363464346136343333373400000000000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000800', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x81d2c889f5563ff840ac05f5198a7bca7177aee11eb1dd6429cb53f171c42046', index: 0, blockHash: '0x800e6705226991336ccfc83207487567db7a3657a8f0ce3307221c6a4cf611cb', blockNumber: 4486798, logsBloom: '0x00000040000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 55057n, blobGasUsed: null, cumulativeGasUsed: 55057n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const name = "DID_NAME"; // possible example of custom document fields a DePIN could create const customFields = { services: [{ id: "#ipfs", type: "machineData", serviceEndpoint: "https://ipfs.io/ipfs/QmYwAPJzv5CZsnAzt8auVTLv3jE9n1Tz8exzhTHtVMJd3t" }] }; const response = await sdk.did.create({ name: name, customDocumentFields: customFields }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.create({}) return object { block_hash: '0x47bc5172ac83ac9088cb301e3072e114b0da174ac902afb5595422d2074f0376', unsubscribe: [Function (anonymous)] } ``` ## read(name, address, wssBaseUrl) Allows a user to read a previously created DID from the chain. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID stored. | | **address** | `string` | Required | Optional | Address of the wallet that created the DID Document. Reading from an EVM tx requires an address to be explicitly set. If a seed has been set for Substrate, the address tied to that phrase will be used. | | **wssBaseUrl** | `string` | Required | Optional | WSS URL must be used to read DID Document. EVM must explicitly set WSS (Substrate has it stored during instance creation). | ### Read DID Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "DID_NAME"; const document = await sdk.did.read({ name: name, address: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.read({}) return object { name: 'DID_NAME', value: '0a336469643a706561713a30783945656162316143636231413730316145664142303046336238613237356133393634363634314312336469643a706561713a3078394565616231614363623141373031614566414230304633623861323735613339363436363431432a590a052369706673120b6d616368696e65446174611a4368747470733a2f2f697066732e696f2f697066732f516d597741504a7a7635435a736e417a7438617556544c76336a45396e31547a3865787a68544874564d4a643374', validity: '4,294,967,295', created: '1,742,919,588,000', document: { id: 'did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', controller: 'did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', verificationMethods: [], signature: undefined, services: [ [Object] ], authentications: [] } } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; // can use read only or a read/write instance to read const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const name = "DID_NAME"; // when using read only an address must be defined const document = await sdk.did.read({ name: name, address: SUBSTRATE_ADDRESS }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.read({}) return object { name: 'DID_NAME', value: '0a396469643a706561713a35446634326d6b7a744c746b6b736751754c7934595636686d687a646a5976446b6e6f7848763151426b61593132506712396469643a706561713a35446634326d6b7a744c746b6b736751754c7934595636686d687a646a5976446b6e6f7848763151426b6159313250672a590a052369706673120b6d616368696e65446174611a4368747470733a2f2f697066732e696f2f697066732f516d597741504a7a7635435a736e417a7438617556544c76336a45396e31547a3865787a68544874564d4a643374', validity: '4,294,967,295', created: '1,742,920,026,000', document: { id: 'did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', controller: 'did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', verificationMethods: [], signature: undefined, services: [ [Object] ], authentications: [] } } ``` ## update(name, address, seed, customDocumentFields) Allows the owner of the DID Document to update a previously created one. Uses the same parameters as createDid() function above for the Custom Document Fields. | Parameter | Type | EVM | Substrate | Description | | ------------------------ | ---------------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID to be updated. | | **address** | `string` | Required | Optional | Wallet address that has the authority to update the DID Document. If Substrate seed at initialization, will default to the public address for that seed. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL must be used to read a DID Document. Used to read the previous DID Document at that location when updating a DID Document. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | | **customDocumentFields** | `CustomDocumentFields` | Optional | Optional | Used to update specific fields of the DID Document. Overwrites the previous DID Document so make sure you keep track of data previously stored. | ### Update DID Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "DID_NAME"; // add a verification method and include the previously set service const customFields = { verificationMethods: [{ type: "EcdsaSecp256k1RecoveryMethod2020" }], services: [{ id: "#ipfs", type: "machineData", serviceEndpoint: "https://ipfs.io/ipfs/QmYwAPJzv5CZsnAzt8auVTLv3jE9n1Tz8exzhTHtVMJd3t" }] }; const tx = await sdk.did.update({ name: name, address: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL, customDocumentFields: customFields }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // read to see the updated document const document = await sdk.did.read({ name: name, address: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.update({}) return object { to: '0x0000000000000000000000000000000000000800', data: '0x68b4b2c10000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000084449445f4e414d45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038630613333363436393634336137303635363137313361333037383339343536353631363233313631343336333632333134313337333033313631343536363431343233303330343633333632333836313332333733353631333333393336333433363336333433313433313233333634363936343361373036353631373133613330373833393435363536313632333136313433363336323331343133373330333136313435363634313432333033303436333336323338363133323337333536313333333933363334333633363334333134333161626630313061336136343639363433613730363536313731336133303738333934353635363136323331363134333633363233313431333733303331363134353636343134323330333034363333363233383631333233373335363133333339333633343336333633343331343332333662363537393733326433313132323034353633363437333631353336353633373033323335333636623331353236353633366637363635373237393464363537343638366636343332333033323330316133333634363936343361373036353631373133613330373833393435363536313632333136313433363336323331343133373330333136313435363634313432333033303436333336323338363133323337333536313333333933363334333633363334333134333232326133303738333934353635363136323331363134333633363233313431333733303331363134353636343134323330333034363333363233383631333233373335363133333339333633343336333633343331343332613539306130353233363937303636373331323062366436313633363836393665363534343631373436313161343336383734373437303733336132663266363937303636373332653639366632663639373036363733326635313664353937373431353034613761373633353433356137333665343137613734333836313735353635343463373633333661343533393665333135343761333836353738376136383534343837343536346434613634333337343332336136343639363433613730363536313731336133303738333934353635363136323331363134333633363233313431333733303331363134353636343134323330333034363333363233383631333233373335363133333339333633343336333633343331343332333662363537393733326433310000000000000000000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000800', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xc6a36b8a0b9be9bd5326b3f0c31c3d04450c92a50cce0582ee362d94cf914aba', index: 0, blockHash: '0x3c94593083019c0d586c09df6cc55453f71b1bf544e0b85ef2ba6882c3878bf6', blockNumber: 4487165, logsBloom: '0x00000040000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000004000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000', gasUsed: 48529n, blobGasUsed: null, cumulativeGasUsed: 48529n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const name = "DID_NAME"; // add a verification method and include the previously set service const customFields = { verificationMethods: [{ type: "Ed25519VerificationKey2020" }], services: [{ id: "#ipfs", type: "machineData", serviceEndpoint: "https://ipfs.io/ipfs/QmYwAPJzv5CZsnAzt8auVTLv3jE9n1Tz8exzhTHtVMJd3t" }] }; const response = await sdk.did.update({ name: name, customDocumentFields: customFields }); // read to see the updated document const document = await sdk.did.read({ name: name }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.update({}) return object { log: 'Successfully updated the DID Document of name DID_NAME at address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', block_hash: '0x0586bf5d7cf76f75cde30f35a213ffaa4d3ec6ccc1e94d63c3353f92f95fb019', unsubscribe: [Function (anonymous)] } ``` ## remove(name, address, seed) Deletes a previously created DID Document from the chain. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID to be deleted. | | **address** | `string` | Required | Optional | Wallet address that has the authority to delete the DID Document. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Remove DID Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const name = "DID_NAME"; const tx = await sdk.did.remove({ name: name, address: EVM_ADDRESS }); // Send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.remove({}) return object { to: '0x0000000000000000000000000000000000000800', data: '0xe8a816900000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000084449445f4e414d45000000000000000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000800', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xd5c548f2e5ded77441ba494eac7da77717ba24b4ee0a62fbd34baf5f2db34e56', index: 0, blockHash: '0x9d3c9356102d84ff7350104d5286c34f0db7f2a5e3e9356e98362dc96902475a', blockNumber: 4488660, logsBloom: '0x00000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000', gasUsed: 44223n, blobGasUsed: null, cumulativeGasUsed: 44223n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const name = "DID_NAME"; const response = await sdk.did.remove({ name: name }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.did.remove({}) return object { log: 'Successfully removed the DID of name DID_NAME from address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', block_hash: '0x3a1e33a2bbee8e73fc405910bc56693b0d65ce5077456f2afd60f55250318cfa', unsubscribe: [Function (anonymous)] } ``` # Group Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/rbac-operations/group ## createGroup(groupName, groupId, seed) Used to create a new group within the RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | ------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **groupName** | `string` | Required | Required | Name of the group to be created. | | **groupId** | `string` | Optional | Optional | ID of the group. If not provided, a new ID will be generated. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Create Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const groupName = "peaq-group-1"; const group = await sdk.rbac.createGroup({ groupName: groupName }); // Send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: group.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // Log to see what the auto generated groupId is console.log(group.groupId); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createGroup({}) return object { tx: { to: '0x0000000000000000000000000000000000000802', data: '0x65c1e09c35386135323638342d336163652d346537322d613032352d38353038356231640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000c706561712d67726f75702d310000000000000000000000000000000000000000' }, groupId: '58a52684-3ace-4e72-a025-85085b1d' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x07eb9b0e02ad279b04c4bea1affcc23ba1f5fc2e97f946a05e3db6cb36f32338', index: 0, blockHash: '0x686188b5eefb03c7ea40621a0e7d72459f1fa8387c7eb2e0cbab92c6eaeff04f', blockNumber: 4490069, logsBloom: '0x00000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 34430n, blobGasUsed: null, cumulativeGasUsed: 34430n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const groupName = "peaq-group-1"; const response = await sdk.rbac.createGroup({ groupName: groupName }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createGroup({}) return object { groupId: '63d6417e-1a03-4fd8-9439-295ce979' } ``` For the EVM tx it returns in a different form than what was used throughout the sdk. The purpose of this is to show the group-id that was autogenerated if it was not manually set. That way you are able to take note of the group-id for that group-name set. ## fetchGroup(owner, groupId, wssBaseUrl) Fetches group information from the RBAC system based on the provided group ID and owner's address. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of the group. | | **groupId** | `string` | Required | Required | ID of the group to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // example of a previously created group id const groupId = "58a52684-3ace-4e72-a025-85085b1d"; const response = await sdk.rbac.fetchGroup({ owner: EVM_ADDRESS, groupId: groupId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroup({}) response object { id: '58a52684-3ace-4e72-a025-85085b1d', name: 'peaq-group-1', enabled: true } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); // example of a previously created group id const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; const response = await sdk.rbac.fetchGroup({ owner: SUBSTRATE_ADDRESS, groupId: groupId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroup({}) response object { id: '63d6417e-1a03-4fd8-9439-295ce979', name: 'peaq-group-1', enabled: true } ``` ## fetchGroups(owner, wssBaseUrl) Used to fetch all the groups associated with the passed owner address. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | --------------------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of all the fetched groups. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Groups Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const response = await sdk.rbac.fetchGroups({ owner: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroups({}) response object [ { id: 'eda650fa-ec62-4d09-849b-b66c7771', name: 'group-name-123', enabled: true }, ... { id: '58a52684-3ace-4e72-a025-85085b1d', name: 'peaq-group-1', enabled: true } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const response = await sdk.rbac.fetchGroups({ owner: SUBSTRATE_ADDRESS }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroups({}) response object [ { id: '9763c33e-2b1b-4834-a908-03f2a9e0', name: 'group-name-123', enabled: true }, ... { id: '63d6417e-1a03-4fd8-9439-295ce979', name: 'peaq-group-1', enabled: true } ] ``` ## updateGroup(groupName, groupId, seed) Allows the owner to update the group name. | Parameter | Type | EVM | Substrate | Description | | ------------- | ------ | -------- | --------- | -------------------------------------------------------------------------------------------- | | **groupName** | string | Required | Required | Updated group name. | | **groupId** | string | Required | Required | ID of the group (32 bytes) to be updated. | | **seed** | string | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Update Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // new group name that will be set const groupName = "peaq-group-new"; // example of a previously created group id const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // build the evm tx const tx = await sdk.rbac.updateGroup({ groupName: groupName, groupId: groupId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch Group to confirm the name change const response = await sdk.rbac.fetchGroup({ owner: EVM_ADDRESS, groupId: groupId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.updateGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xadb32ee135386135323638342d336163652d346537322d613032352d38353038356231640000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000e706561712d67726f75702d6e6577000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xf0f8c39077ae3c3d631579d9c71ef1d84729bb63bbc5ec8b75ed175f71034707', index: 0, blockHash: '0xdfcd14d7be74ef7680791ec7455821ffaa37d642f055d14331be791904535577', blockNumber: 4500445, logsBloom: '0x00000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000', gasUsed: 35283n, blobGasUsed: null, cumulativeGasUsed: 35283n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // new group name that will be set const groupName = "peaq-group-new"; // example of a previously created group id const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; // build the evm tx const response = await sdk.rbac.updateGroup({ groupName: groupName, groupId: groupId }); // fetch Group to confirm the name change const fetchedGroup = await sdk.rbac.fetchGroup({ owner: SUBSTRATE_ADDRESS, groupId: groupId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.updateGroup({}) return object { message: 'Successfully update group 63d6417e-1a03-4fd8-9439-295ce979 with new name: peaq-group-new' } ``` ## disableGroup(groupId, seed) Disables a group within a permission management system. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **groupId** | `string` | Required | Required | The unique identifier (ID) of the group to be disabled. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Disable Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // group id to disable const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // build the evm tx const tx = await sdk.rbac.disableGroup({ groupId: groupId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch group to confirm it has been changed to disabled const response = await sdk.rbac.fetchGroup({ owner: EVM_ADDRESS, groupId: groupId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disableGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x47538f1235386135323638342d336163652d346537322d613032352d3835303835623164' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x4e70353fba441965af6fe54eb05c7968979f643892d7d02955c2cce78940cf7a', index: 0, blockHash: '0xe850a8b6aacf3ae4e06d127ae81b5420a3f0548ef41585eb1a7737768fa9d1e5', blockNumber: 4501188, logsBloom: '0x00000000000000000000000000000000000800000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 34680n, blobGasUsed: null, cumulativeGasUsed: 34680n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // group id to disable const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; // execute the extrinsic const response = await sdk.rbac.disableGroup({ groupId: groupId }); // fetch group to confirm it has been changed to disabled const fetchedGroup = await sdk.rbac.fetchGroup({ owner: SUBSTRATE_ADDRESS, groupId: groupId, }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disableGroup({}) return object { message: 'Successfully disable group 63d6417e-1a03-4fd8-9439-295ce979' } ``` ## assignRoleToGroup(groupId, roleId, seed) This function allows you to assign a specific role to a group within a permission management system. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **groupId** | `string` | Required | Required | The unique identifier of the group to which the role will be assigned. | | **roleId** | `string` | Required | Required | ID of the role that will be assigned to the group. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Assign Role to Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // group id (generated with createGroup) const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // role id to assign to group const roleId = "ada650fa-ec62-4d09-849b-b66c7777"; // build the evm tx const tx = await sdk.rbac.assignRoleToGroup({ groupId: groupId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignRoleToGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x03c4d7fb62363861353538392d313238342d343965392d383237362d303335396134323935386135323638342d336163652d346537322d613032352d3835303835623164' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xf65f894de591b5bbd7c3bb482c95ced55b7989ef07980cf7f469f19944ab02e1', index: 0, blockHash: '0x12c4ac10e6abed8148a7b6477b3c53cdabfbbfdb5abd42fa0f12aaba87aecf73', blockNumber: 4500565, logsBloom: '0x00000000000000000000000000000000000000000000080006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 31322n, blobGasUsed: null, cumulativeGasUsed: 31322n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // group id (generated with createGroup) const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; // role id to assign to group const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; // execute extrinsic const response = await sdk.rbac.assignRoleToGroup({ groupId: groupId, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignRoleToGroup({}) return object { message: 'Successfully assign role bc3f20d5-c519-4048-8db1-4bbf48dc to group 63d6417e-1a03-4fd8-9439-295ce979' } ``` ## fetchGroupRoles(owner, groupId, wssBaseUrl) Designed to retrieve roles associated with a specific group from the network's RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address of the owner of the roles. This is typically the account that manages the roles and permissions. | | **groupId** | `string` | Required | Required | Unique identifier of the group for whom roles are to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Group Roles Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const groupId = "58a52684-3ace-4e72-a025-85085b1d"; const response = await sdk.rbac.fetchGroupRoles({ owner: EVM_ADDRESS, groupId: groupId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroupRoles({}) return object [ { role: 'b68a5589-1284-49e9-8276-0359a429', group: '58a52684-3ace-4e72-a025-85085b1d' } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; const response = await sdk.rbac.fetchGroupRoles({ owner: SUBSTRATE_ADDRESS, groupId: groupId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroupRoles({}) return object [ { role: 'bc3f20d5-c519-4048-8db1-4bbf48dc', group: '63d6417e-1a03-4fd8-9439-295ce979' } ] ``` ## unassignRoleToGroup(groupId, roleId, seed) This function allows you to unassign a specific role from a group in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **groupId** | `string` | Required | Required | ID of the group from which the role should be unassigned. | | **roleId** | `string` | Required | Required | ID of the role that needs to be unassigned. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Unassign Role to Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // previously created groupId const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // role id to unassign to group const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.unassignRoleToGroup({ groupId: groupId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch to see it has been unassigned const response = await sdk.rbac.fetchGroupRoles({ owner: EVM_ADDRESS, groupId: groupId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignRoleToGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xad58943762363861353538392d313238342d343965392d383237362d303335396134323935386135323638342d336163652d346537322d613032352d3835303835623164' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x2a2134db9dd1e1ff3c70c5e350223c3d776e2e2822c8264bb2879a49b7d542da', index: 0, blockHash: '0x5e5dad9089357d1095b0b2177a26b13cce1f52f0e808f3f95260f588b4a9fe40', blockNumber: 4500781, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000040000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 29291n, blobGasUsed: null, cumulativeGasUsed: 29291n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // previously created groupId const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; // role id to unassign to group const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; // send extrinsic const response = await sdk.rbac.unassignRoleToGroup({ groupId: groupId, roleId: roleId }); // fetch to see it has been unassigned (throws an error) const fetchedUserGroups = await sdk.rbac.fetchGroupRoles({ owner: SUBSTRATE_ADDRESS, groupId: groupId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignRoleToGroup({}) return object { message: 'Successfully unassign role: bc3f20d5-c519-4048-8db1-4bbf48dc from group: 63d6417e-1a03-4fd8-9439-295ce979' } ``` ## assignUserToGroup(userId, groupId, address, seed) This function allows you to assign a user to a group within the RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------- | | **userId** | `string` | Required | Required | The unique identifier of the user to whom the group will be assigned. ID created by user and must be 32 bytes. | | **groupId** | `string` | Required | Required | ID of the group that will be assigned to the user. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Assign User to Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // user id to assign to group const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // group id (generated with createGroup) const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // build the evm tx const tx = await sdk.rbac.assignUserToGroup({ userId: userId, groupId: groupId, }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignUserToGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xa76110c439653863373836362d383433352d346237362d383638332d373039613033633935386135323638342d336163652d346537322d613032352d3835303835623164' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x9e1da68de86d6d1f68e600a6a525d117b4493be046be6fdfeda7a21a26c80cec', index: 0, blockHash: '0x24bc140d800cfc15d5c8fd45d201a8e4b31dfaa8aa044b1d799cb2a5cb7dc8d1', blockNumber: 4500957, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000001', gasUsed: 30248n, blobGasUsed: null, cumulativeGasUsed: 30248n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // user id to assign to group const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // group id (generated with createGroup) const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; // send the extrinsic const response = await sdk.rbac.assignUserToGroup({ userId: userId, groupId: groupId, }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignUserToGroup({}) return object { message: 'Successfully assign user 9e8c7866-8435-4b76-8683-709a03c9 to group 63d6417e-1a03-4fd8-9439-295ce979' } ``` ## fetchUserGroups(owner, userId, wssBaseUrl) Designed to retrieve groups associated with a specific user from the network's RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address of the owner of the roles. This is typically the account that manages the roles and permissions. | | **userId** | `string` | Required | Required | Unique identifier of the user for whom groups are to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch User Groups Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const response = await sdk.rbac.fetchUserGroups({ owner: EVM_ADDRESS, userId: userId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserGroups({}) return object [ { user: '9e8c7866-8435-4b76-8683-709a03c9', group: '58a52684-3ace-4e72-a025-85085b1d' } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const response = await sdk.rbac.fetchUserGroups({ owner: SUBSTRATE_ADDRESS, userId: userId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserGroups({}) return object [ { user: '9e8c7866-8435-4b76-8683-709a03c9', group: '63d6417e-1a03-4fd8-9439-295ce979' } ] ``` ## unassignUserToGroup(userId, groupId, address, seed) This function allows you to unassign a specific user from a group in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **userId** | `string` | Required | Required | ID of the user from which the group should be unassigned. | | **groupId** | `string` | Required | Required | ID of the group that needs to be unassigned. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Unassign User to Group Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // user id to unassign from group const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // previously created groupId const groupId = "58a52684-3ace-4e72-a025-85085b1d"; // build the evm tx const tx = await sdk.rbac.unassignUserToGroup({ userId: userId, groupId: groupId, }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch to see it has been unassigned const response = await sdk.rbac.fetchUserGroups({ owner: EVM_ADDRESS, userId: userId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignUserToGroup({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x0b9cc42839653863373836362d383433352d346237362d383638332d373039613033633935386135323638342d336163652d346537322d613032352d3835303835623164' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x1790d5bdc25d5ddd814e957a45a539400b4c717b5f0a25abec3d73896ac643ac', index: 0, blockHash: '0x3a862b4795987cdab60a62dd7d00fea2bd91e89b7a780a85af6c47c07b59ac65', blockNumber: 4501124, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000001000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 29169n, blobGasUsed: null, cumulativeGasUsed: 29169n, gasPrice: 100000943831n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // user id to unassign from group const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // previously created groupId const groupId = "63d6417e-1a03-4fd8-9439-295ce979"; const response = await sdk.rbac.unassignUserToGroup({ userId: userId, groupId: groupId, }); // fetch to see it has been unassigned const fetchedUserGroups = await sdk.rbac.fetchUserGroups({ owner: SUBSTRATE_ADDRESS, userId: userId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignUserToGroup({}) return object { message: 'Successfully unassign user: 9e8c7866-8435-4b76-8683-709a03c9 from group: 63d6417e-1a03-4fd8-9439-295ce979' } ``` # Permission Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/rbac-operations/permission ## createPermission(permissionName, permissionId, address, seed) Used to create a new permission within the RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | ------------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **permissionName** | `string` | Required | Required | Name of the permission to be created. | | **permissionId** | `string` | Optional | Optional | ID of the permission (32 bytes). If not supplied one will be generated for you. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Create Permission Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const permissionName = "peaq-permission-1"; const permission = await sdk.rbac.createPermission({ permissionName: permissionName }); // Send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: permission.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // log to see the generated id console.log(permission.permissionId); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createPermission({}) return object { tx: { to: '0x0000000000000000000000000000000000000802', data: '0x67a2e51533316263386639372d633538372d343738342d613732352d623663373365656400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011706561712d7065726d697373696f6e2d31000000000000000000000000000000' }, permissionId: '31bc8f97-c587-4784-a725-b6c73eed' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x66acb0dde4190d38536318a95376535b11d1c995dfa264e4c70690fa364b058d', index: 0, blockHash: '0xbe62e0bdb0b98645280bda895ff6d8a62ccc511d8c69c95d8c3494eb8be32be3', blockNumber: 4501439, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020008000000000000000000000000000000000008000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000', gasUsed: 34498n, blobGasUsed: null, cumulativeGasUsed: 34498n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const permissionName = "peaq-permission-1"; const response = await sdk.rbac.createPermission({ permissionName: permissionName }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createPermission({}) return object { permissionId: '77395752-8588-47c5-8bd5-0d2bf273' } ``` The EVM tx returns in a different form than what was used throughout the sdk. The purpose of this is to show the permission-id that was autogenerated if it was not manually set. That way you are able to take note of the permission-id for that permission-name set. ## fetchPermission(owner, permissionId, wssBaseUrl) Fetch a permission at the given permissionId and address. | Parameter | Type | EVM | Substrate | Description | | ---------------- | -------- | -------- | --------- | ------------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of the permission. | | **permissionId** | `string` | Required | Required | ID of the permission to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Permission Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // example of a previously created permission id const permissionId = "31bc8f97-c587-4784-a725-b6c73eed"; const response = await sdk.rbac.fetchPermission({ owner: EVM_ADDRESS, permissionId: permissionId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchPermission({}) response object { id: '31bc8f97-c587-4784-a725-b6c73eed', name: 'peaq-permission-1', enabled: true } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const permissionId = "77395752-8588-47c5-8bd5-0d2bf273"; const response = await sdk.rbac.fetchPermission({ owner: SUBSTRATE_ADDRESS, permissionId: permissionId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchPermission({}) response object { id: '77395752-8588-47c5-8bd5-0d2bf273', name: 'peaq-permission-1', enabled: true } ``` ## fetchPermissions(owner, wssBaseUrl) Used to fetch all the permissions associated with the passed owner address. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of all the fetched permissions. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Permissions Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const response = await sdk.rbac.fetchPermissions({ owner: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchPermissions({}) response object [ { id: '88dec9d0-354e-4ab6-9856-7a7eb45c', name: 'permission-name-123', enabled: true }, ... { id: '31bc8f97-c587-4784-a725-b6c73eed', name: 'peaq-permission-1', enabled: true } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const response = await sdk.rbac.fetchPermissions({ owner: SUBSTRATE_ADDRESS }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchPermissions({}) response object [ { id: '21f7ad5f-0f41-40d5-b9f6-7ce0aad3', name: 'permission-name-123', enabled: true }, ... { id: '77395752-8588-47c5-8bd5-0d2bf273', name: 'peaq-permission-1', enabled: true } ] ``` ## updatePermission(permissionName, permissionId, address, seed) Allows the owner to update the permission name. | Parameter | Type | EVM | Substrate | Description | | ------------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **permissionName** | `string` | Required | Required | Updated permission name. | | **permissionId** | `string` | Required | Required | ID of the permission (32 bytes) to be updated. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Update Permission Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // new permission name that will be set const permissionName = "peaq-permission-new"; // example of a previously created permission id const permissionId = "31bc8f97-c587-4784-a725-b6c73eed"; // build the evm tx const tx = await sdk.rbac.updatePermission({ permissionName: permissionName, permissionId: permissionId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch permission to confirm the name change const response = await sdk.rbac.fetchPermission({ owner: EVM_ADDRESS, permissionId: permissionId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.updatePermission({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xb0c5b5ad33316263386639372d633538372d343738342d613732352d623663373365656400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000013706561712d7065726d697373696f6e2d6e657700000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x5653326390b5277aad0e326eb9ecfe0a90bdef323c4e50c29be4a679044d3a30', index: 0, blockHash: '0xf95e4b31c28a23b6aff89d15411c9e36c833ec226ab3ced54325439124899458', blockNumber: 4501640, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000100000000000000', gasUsed: 35429n, blobGasUsed: null, cumulativeGasUsed: 35429n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // new permission name that will be set const permissionName = "peaq-permission-new"; // example of a previously created permission id const permissionId = "77395752-8588-47c5-8bd5-0d2bf273"; const response = await sdk.rbac.updatePermission({ permissionName: permissionName, permissionId: permissionId }); // Fetch permission to confirm the name change const fetchedPermission = await sdk.rbac.fetchPermission({ owner: SUBSTRATE_ADDRESS, permissionId: permissionId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchPermission({}) return object { message: 'Successfully update permission 77395752-8588-47c5-8bd5-0d2bf273 with new name: peaq-permission-new' } ``` ## disablePermission(permissionId, address, seed) Disables a permission within a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **permissionId** | `string` | Required | Required | The unique identifier (ID) of the permission to be disabled. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Disable Permission Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // permission id to disable const permissionId = "31bc8f97-c587-4784-a725-b6c73eed"; // build the evm tx const tx = await sdk.rbac.disablePermission({ permissionId: permissionId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // Fetch permission to confirm it has been changed to disabled const response = await sdk.rbac.fetchPermission({ owner: EVM_ADDRESS, permissionId: permissionId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disablePermission({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x727e011e33316263386639372d633538372d343738342d613732352d6236633733656564' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xb63e38df51375830ad61be4afa89b512a812375a0f84800c765821f82176f800', index: 1, blockHash: '0x13c29ec81311aad9dfce89630938885e1ca4f3f0b8a8fb81526072ba8fb05dc4', blockNumber: 4502232, logsBloom: '0x00000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000020000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 34682n, blobGasUsed: null, cumulativeGasUsed: 78842n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // permission id to disable const permissionId = "77395752-8588-47c5-8bd5-0d2bf273"; // execute the extrinsic const response = await sdk.rbac.disablePermission({ permissionId: permissionId }); // Fetch permission to confirm it has been changed to disabled const fetchedPermission = await sdk.rbac.fetchPermission({ owner: SUBSTRATE_ADDRESS, permissionId: permissionId, }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disablePermission({}) return object { message: 'Successfully disable permission 77395752-8588-47c5-8bd5-0d2bf273' } ``` ## assignPermissionToRole(permissionId, roleId, address, seed) This function allows you to assign a permission to a role in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **permissionId** | `string` | Required | Required | ID of the permission to be assigned to the role. | | **roleId** | `string` | Required | Required | ID of the role to which the permission will be assigned. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Assign Permission to Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const permissionId = "31bc8f97-c587-4784-a725-b6c73eed"; const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.assignPermissionToRole({ permissionId: permissionId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignPermissionToRole({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x404147f133316263386639372d633538372d343738342d613732352d623663373365656462363861353538392d313238342d343965392d383237362d3033353961343239' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xb3c8a7b2127754ef031cd3469afdec0aca6c8736e4b29373c9ad49a171a652c1', index: 2, blockHash: '0x2281ba090e6e1449ed5c5f0bd3ddff6231a4bf1782546a19cc1e0d7bcaa50eeb', blockNumber: 4501755, logsBloom: '0x00000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000400000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 31342n, blobGasUsed: null, cumulativeGasUsed: 73342n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const permissionId = "77395752-8588-47c5-8bd5-0d2bf273"; const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; const response = await sdk.rbac.assignPermissionToRole({ permissionId: permissionId, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignPermissionToRole({}) return object { message: 'Successfully assign permission 77395752-8588-47c5-8bd5-0d2bf273 to role bc3f20d5-c519-4048-8db1-4bbf48dc' } ``` ## fetchRolePermissions(owner, roleId, wssBaseUrl) Designed to retrieve permissions associated with a specific role. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ------------------------------------------------------ | | **owner** | `string` | Required | Required | Address of the owner of the roles. | | **roleId** | `string` | Required | Required | ID of the role for whom permissions are to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | ### Fetch Role Permissions Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const roleId = "b68a5589-1284-49e9-8276-0359a429"; const response = await sdk.rbac.fetchRolePermissions({ owner: EVM_ADDRESS, roleId: roleId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRolePermissions({}) return object [ { permission: '31bc8f97-c587-4784-a725-b6c73eed', role: 'b68a5589-1284-49e9-8276-0359a429' } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; const response = await sdk.rbac.fetchRolePermissions({ owner: SUBSTRATE_ADDRESS, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRolePermissions({}) return object [ { permission: '77395752-8588-47c5-8bd5-0d2bf273', role: 'bc3f20d5-c519-4048-8db1-4bbf48dc' } ] ``` ## unassignPermissionToRole(permissionId, roleId, address, seed) This function allows you to unassign a permission from a role in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **permissionId** | `string` | Required | Required | ID of the permission to be unassigned. | | **roleId** | `string` | Required | Required | ID of the role from which to unassign the permission. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Unassign Permission from Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const permissionId = "31bc8f97-c587-4784-a725-b6c73eed"; const roleId = "b68a5589-1284-49e9-8276-0359a429"; const tx = await sdk.rbac.unassignPermissionToRole({ permissionId: permissionId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch to see it has been unassigned const response = await sdk.rbac.fetchRolePermissions({ owner: EVM_ADDRESS, roleId: roleId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignPermissionToRole({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xba427c9e33316263386639372d633538372d343738342d613732352d623663373365656462363861353538392d313238342d343965392d383237362d3033353961343239' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x55678a0cbbbb5089acdaa15427b21879270d985f2bb82aaea331f5316d3ef566', index: 1, blockHash: '0x5cce714cf1c11ed6f65680458bf989e69b3749fa0b0b85124176119ae6cc1ea9', blockNumber: 4501891, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 29178n, blobGasUsed: null, cumulativeGasUsed: 68884n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const permissionId = "77395752-8588-47c5-8bd5-0d2bf273"; const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; const response = await sdk.rbac.unassignPermissionToRole({ permissionId: permissionId, roleId: roleId }); // fetch to see it has been unassigned const fetchedRolePermissions = await sdk.rbac.fetchRolePermissions({ owner: SUBSTRATE_ADDRESS, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignPermissionToRole({}) return object { message: 'Successfully unassign role: bc3f20d5-c519-4048-8db1-4bbf48dc from permission: 77395752-8588-47c5-8bd5-0d2bf273' } ``` ## fetchUserPermission(owner, userId, wssBaseUrl) Fetches the permissions associated with a user. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ------------------------------------------------------ | | **owner** | `string` | Required | Required | Address of the owner of the permissions. | | **userId** | `string` | Required | Required | ID of the user for whom you want to fetch permissions. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | In order to fetch a user permission the following flow must take place: 1. Create permission 2. Create role 3. Assign permission to role 4. Assign role to user 5. Fetch user permissions The following code shows this flow. ### Fetch User Permissions Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // 1. Create a permission const permissionName = "peaq-permission-2"; const txPermission = await sdk.rbac.createPermission({permissionName: permissionName}); await Sdk.sendEvmTx({tx: txPermission.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 2. Create a role const roleName = "peaq-role-2"; const txRole = await sdk.rbac.createRole({roleName: roleName}); await Sdk.sendEvmTx({tx: txRole.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 3. Assign permission to role const tx = await sdk.rbac.assignPermissionToRole({permissionId: txPermission.permissionId, roleId: txRole.roleId}); await Sdk.sendEvmTx({tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 4. Assign role to user const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const tx2 = await sdk.rbac.assignRoleToUser({userId: userId, roleId: txRole.roleId}); await Sdk.sendEvmTx({tx: tx2, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 5. Fetch user permissions const response = await sdk.rbac.fetchUserPermissions({owner: EVM_ADDRESS, userId: userId, wssBaseUrl: WSS_BASE_URL}); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserPermissions({}) return object [ { id: '763207f6-32ef-4236-969d-d1b81ac4', name: 'peaq-permission-2', enabled: true } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // 1. Create a permission const permissionName = "peaq-permission-2"; const txPermission = await sdk.rbac.createPermission({permissionName: permissionName}); // 2. Create a role const roleName = "peaq-role-2"; const txRole = await sdk.rbac.createRole({roleName: roleName}); // 3. Assign permission to role const p2rResp = await sdk.rbac.assignPermissionToRole({permissionId: txPermission.permissionId, roleId: txRole.roleId}); // 4. Assign role to user const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const r2uResp = await sdk.rbac.assignRoleToUser({userId: userId, roleId: txRole.roleId}); // 5. Fetch user permissions const response = await sdk.rbac.fetchUserPermissions({owner: SUBSTRATE_ADDRESS, userId: userId}); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserPermissions({}) return object [ { id: 'fc49ee05-f0ad-47ce-b336-62efdfec', name: 'peaq-permission-2', enabled: true } ] ``` ## fetchGroupPermissions(owner, groupId, wssBaseUrl) Fetches the permissions associated with a group. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ------------------------------------------------------- | | **owner** | `string` | Required | Required | Address of the owner of the permissions. | | **groupId** | `string` | Required | Required | ID of the group for whom you want to fetch permissions. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. | In order to fetch a group permission the following flow must take place: 1. Create permission 2. Create role 3. Assign permission to role 4. Create group 5. Assign role to group 6. Fetch group permissions The following code shows this flow. ### Fetch Group Permissions Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // 1. Create a permission const permissionName = "peaq-permission-2"; const txPermission = await sdk.rbac.createPermission({permissionName: permissionName}); await Sdk.sendEvmTx({tx: txPermission.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 2. Create a role const roleName = "peaq-role-2"; const txRole = await sdk.rbac.createRole({roleName: roleName}); await Sdk.sendEvmTx({tx: txRole.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 3. Assign permission to role const tx = await sdk.rbac.assignPermissionToRole({permissionId: txPermission.permissionId, roleId: txRole.roleId}); await Sdk.sendEvmTx({tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 4. Create group const groupName = "peaq-group-2"; const txGroup = await sdk.rbac.createGroup({groupName: groupName}); await Sdk.sendEvmTx({tx: txGroup.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 5. Assign role to group const tx2 = await sdk.rbac.assignRoleToGroup({groupId: txGroup.groupId, roleId: txRole.roleId}); await Sdk.sendEvmTx({tx: tx2, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE}); // 6. Fetch group permissions const response = await sdk.rbac.fetchGroupPermissions({owner: EVM_ADDRESS, groupId: txGroup.groupId, wssBaseUrl: WSS_BASE_URL}); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroupPermissions({}) return object [ { id: '322f3070-826c-4dad-ba53-75fbfeee', name: 'peaq-permission-2', enabled: true } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // 1. Create a permission const permissionName = "peaq-permission-2"; const txPermission = await sdk.rbac.createPermission({permissionName: permissionName}); // 2. Create a role const roleName = "peaq-role-2"; const txRole = await sdk.rbac.createRole({roleName: roleName}); // 3. Assign permission to role const p2rResp = await sdk.rbac.assignPermissionToRole({permissionId: txPermission.permissionId, roleId: txRole.roleId}); // 4. Create group const groupName = "peaq-group-2"; const txGroup = await sdk.rbac.createGroup({groupName: groupName}); // 5. Assign role to group const r2gResp = await sdk.rbac.assignRoleToGroup({groupId: txGroup.groupId, roleId: txRole.roleId}); // 6. Fetch group permissions const response = await sdk.rbac.fetchGroupPermissions({owner: SUBSTRATE_ADDRESS, groupId: txGroup.groupId}); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchGroupPermissions({}) return object [ { id: '158a1097-3ba4-4cae-b682-c58e7a4a', name: 'peaq-permission-2', enabled: true } ] ``` # Role Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/rbac-operations/role ## createRole(roleName, roleId, seed) Create a new role in the peaq network's Role-Based Access Control (RBAC) system. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **roleName** | `string` | Required | Required | Name of the role to be created. | | **roleId** | `string` | Optional | Optional | ID of the role (32 bytes). If not supplied one will be generated for you. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Create Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const roleName = "peaq-role-1"; const role = await sdk.rbac.createRole({ roleName: roleName }); // Send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: role.tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // Log to see what the auto generated roleId is console.log(role.roleId); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createRole({}) return object { tx: { to: '0x0000000000000000000000000000000000000802', data: '0xbca838d662363861353538392d313238342d343965392d383237362d30333539613432390000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000b706561712d726f6c652d31000000000000000000000000000000000000000000' }, roleId: 'b68a5589-1284-49e9-8276-0359a429' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x9b3d1ae6b9045e36b0985de3f4704aa84e5fd0d02b6f84fc1091e124a7dad8ee', index: 0, blockHash: '0x62b334b67c157a754b76276244911a086fc13f0f951ee7a3d1d0277f74d393de', blockNumber: 4489206, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000002000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 34414n, blobGasUsed: null, cumulativeGasUsed: 34414n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const roleName = "peaq-role-1"; const response = await sdk.rbac.createRole({ roleName: roleName }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.createRole({}) return object { roleId: 'bc3f20d5-c519-4048-8db1-4bbf48dc' } ``` The EVM tx returns in a different form than what was used throughout the sdk. The purpose of this is to show the role-id that was autogenerated if it was not manually set. ## fetchRole(owner, roleId, wssBaseUrl) Fetch a role at the given roleId and address associated. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | --------------------------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of the role. | | **roleId** | `string` | Required | Required | ID of the role to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. Substrate WSS is stored at instance creation. | ### Fetch Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // example of a previously created role id const roleId = "b68a5589-1284-49e9-8276-0359a429"; const response = await sdk.rbac.fetchRole({ owner: EVM_ADDRESS, roleId: roleId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRole({}) response object { id: 'b68a5589-1284-49e9-8276-0359a429', name: 'peaq-role-1', enabled: true } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); // example of a previously created role id const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; const response = await sdk.rbac.fetchRole({ owner: SUBSTRATE_ADDRESS, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRole({}) response object { id: 'bc3f20d5-c519-4048-8db1-4bbf48dc', name: 'peaq-role-1', enabled: true } ``` ## fetchRoles(owner, wssBaseUrl) Used to fetch all the roles associated with the passed owner address. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | --------------------------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address that represents the owner of all the fetched roles. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. Substrate WSS is stored at instance creation. | ### Fetch Roles Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const response = await sdk.rbac.fetchRoles({ owner: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRoles({}) response object [ { id: 'e09fe342-f8ee-46d1-82e2-a60a5b6e', name: 'role-name-123', enabled: true }, ... { id: 'b68a5589-1284-49e9-8276-0359a429', name: 'peaq-role-1', enabled: true } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const response = await sdk.rbac.fetchRoles({ owner: SUBSTRATE_ADDRESS }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchRoles({}) response object [ { id: 'bb3d5a05-3a4a-452b-a96d-372832ff', name: 'role-name-123', enabled: true }, ... { id: 'bc3f20d5-c519-4048-8db1-4bbf48dc', name: 'peaq-role-1', enabled: true } ] ``` ## updateRole(roleName, roleId, seed) Allows the owner to update the role name. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **roleName** | `string` | Required | Required | Updated role name. | | **roleId** | `string` | Required | Required | ID of the role (32 bytes) to be updated. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Update Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // new role name that will be set const roleName = "peaq-role-new"; // example of a previously created role id const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.updateRole({ roleName: roleName, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch Role to confirm the name change const response = await sdk.rbac.fetchRole({ owner: EVM_ADDRESS, roleId: roleId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.updateRole({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x7cc0a21862363861353538392d313238342d343965392d383237362d30333539613432390000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000d706561712d726f6c652d6e657700000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x9fea31cc7edc8571f5236249b62e4fa45b5515a6c316fb62998bda01129f1236', index: 1, blockHash: '0x3e0a30ab7a32d3d9bb46bde23571ab0e5e5db1c428b4ab1585e0e76883b1a3be', blockNumber: 4489512, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 35281n, blobGasUsed: null, cumulativeGasUsed: 89785n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // new role name that will be set const roleName = "peaq-role-new"; // example of a previously created role id const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; const response = await sdk.rbac.updateRole({ roleName: roleName, roleId: roleId }); // fetch Role to confirm the name change const fetchedRole = await sdk.rbac.fetchRole({ owner: SUBSTRATE_ADDRESS, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.updateRole({}) return object { message: 'Successfully update role bc3f20d5-c519-4048-8db1-4bbf48dc with new name: peaq-role-new' } ``` ## disableRole(roleId, seed) Disables a role within a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **roleId** | `string` | Required | Required | The unique identifier (ID) of the role to be disabled. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Disable Role Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // role id to disable const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.disableRole({ roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // Fetch Role to confirm it has been changed to disabled const response = await sdk.rbac.fetchRole({ owner: EVM_ADDRESS, roleId: roleId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disableRole({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x7c4a13b162363861353538392d313238342d343965392d383237362d3033353961343239' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x9cd4cfecbdeac0fc5c501f26a1b1c52314bdf24731c26bf05797b2b6ca00588f', index: 0, blockHash: '0x90f3daabfb05d1e3ed83850398e8593ff8a08d59df3cf20db2b4538169491459', blockNumber: 4489862, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000020000000000000000000000020000020000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 34782n, blobGasUsed: null, cumulativeGasUsed: 34782n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // role id to disable const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; // execute the extrinsic const response = await sdk.rbac.disableRole({ roleId: roleId }); // Fetch Role to confirm it has been changed to disabled const fetchedRole = await sdk.rbac.fetchRole({ owner: SUBSTRATE_ADDRESS, roleId: roleId, }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.disableRole({}) return object { message: 'Successfully disable role bc3f20d5-c519-4048-8db1-4bbf48dc' } ``` ## assignRoleToUser(userId, roleId, seed) This function allows you to assign a specific role to a user in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------- | -------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------- | | **userId** | `string` | Required | Required | The unique identifier of the user to whom the role will be assigned. ID created by user and must be 32 bytes. | | **roleId** | `string` | Required | Required | ID of the role that will be assigned to the user. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Assign Role to User Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // self-generated userId const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // role id to assign to user const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.assignRoleToUser({ userId: userId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignRoleToUser({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0xae8cdd7962363861353538392d313238342d343965392d383237362d303335396134323939653863373836362d383433352d346237362d383638332d3730396130336339' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x71b13a8507b540937cbba58843497516edc505ce1af09a3e573cd5789c48cccf', index: 0, blockHash: '0xc44b43afbc16ecba36b2051a88a62f9b942293394ace5c193cbea73f8e9f2460', blockNumber: 4489679, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000004000000000000000000008000000000000000000000000000000000000000000000000000000', gasUsed: 31390n, blobGasUsed: null, cumulativeGasUsed: 31390n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // self-generated userId const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // role id to assign to user const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; // build the evm tx const response = await sdk.rbac.assignRoleToUser({ userId: userId, roleId: roleId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.assignRoleToUser({}) return object { message: 'Successfully assign role bc3f20d5-c519-4048-8db1-4bbf48dc to user 9e8c7866-8435-4b76-8683-709a03c9' } ``` ## fetchUserRoles(owner, userId, wssBaseUrl) Designed to retrieve roles associated with a specific user from the network's RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------- | | **owner** | `string` | Required | Required | Address of the owner of the roles. This is typically the account that manages the roles and permissions. | | **userId** | `string` | Required | Required | Unique identifier of the user for whom roles are to be fetched. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL used to query RBAC storage. Substrate WSS is stored at instance creation. | ### Fetch User Roles Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const response = await sdk.rbac.fetchUserRoles({ owner: EVM_ADDRESS, userId: userId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserRoles({}) return object [ { role: 'b68a5589-1284-49e9-8276-0359a429', user: '9e8c7866-8435-4b76-8683-709a03c9' } ] ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE }); const userId = "9e8c7866-8435-4b76-8683-709a03c9"; const response = await sdk.rbac.fetchUserRoles({ owner: SUBSTRATE_ADDRESS, userId: userId, }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.fetchUserRoles({}) return object [ { role: 'bc3f20d5-c519-4048-8db1-4bbf48dc', user: '9e8c7866-8435-4b76-8683-709a03c9' } ] ``` ## unassignRoleToUser(userId, roleId, seed) This function allows you to unassign a specific role from a user in a permission management system. | Parameter | Type | EVM | Substrate | Description | | ---------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **userId** | `string` | Required | Required | ID of the user from which the role should be unassigned. | | **roleId** | `string` | Required | Required | ID of the role that needs to be unassigned. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Unassign Role to User Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // self-generated userId const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // role id to unassign to user const roleId = "b68a5589-1284-49e9-8276-0359a429"; // build the evm tx const tx = await sdk.rbac.unassignRoleToUser({ userId: userId, roleId: roleId }); // send using Sdk function const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // fetch to see it has been unassigned (should throw an error) const response = await sdk.rbac.fetchUserRoles({ owner: EVM_ADDRESS, userId: userId, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignRoleToUser({}) return object { to: '0x0000000000000000000000000000000000000802', data: '0x7663575062363861353538392d313238342d343965392d383237362d303335396134323939653863373836362d383433352d346237362d383638332d3730396130336339' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000802', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0xe128c68cd4ea9480c33d6fde4a5929d6d27b0ec8c20ddf1060505da2a9f31ab0', index: 1, blockHash: '0xcf871f18022d80ba2c3e627cb44f4d4ccae9ce3191ccd70518574a7019a3cf44', blockNumber: 4489791, logsBloom: '0x00000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000002000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 29181n, blobGasUsed: null, cumulativeGasUsed: 159305n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_ADDRESS = process.env["SUBSTRATE_ADDRESS"]; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); // self-generated userId const userId = "9e8c7866-8435-4b76-8683-709a03c9"; // role id to unassign to user const roleId = "bc3f20d5-c519-4048-8db1-4bbf48dc"; // send extrinsic const response = await sdk.rbac.unassignRoleToUser({ userId: userId, roleId: roleId }); // fetch to see it has been unassigned (should throw an error) const fetchedRoles = await sdk.rbac.fetchUserRoles({ owner: SUBSTRATE_ADDRESS, userId: userId }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.rbac.unassignRoleToUser({}) return object { message: 'Successfully unassign user: 9e8c7866-8435-4b76-8683-709a03c9 from role: bc3f20d5-c519-4048-8db1-4bbf48dc' } ``` # Send EVM TX Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/send-evm-tx When you would like to use the SDK to perform an EVM operation, you must first create a transaction object. The different classes of the SDK: [DID](/peaqchain/sdk-reference/javascript/did-operations), [RBAC](/peaqchain/sdk-reference/javascript/rbac-operations/role), and [Storage](/peaqchain/sdk-reference/javascript/storage-operations) all show how an EVM tx is generated. After receiving an EVM Transaction object back from the class you called, you are then able to use the SDK to send a transaction on behalf of the seed. ## sendEvmTx(tx, baseUrl, seed) | Parameter | Type | EVM | Substrate | Description | | ----------- | ---------------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **tx** | `EvmTransaction` | Required | N/A | EVM compatible transaction object returned from specific SDK method. Used to execute the corresponding peaq function. Gives the user the option to manually send the tx using ethers.js, web3.js, or to use the sendEvmTx() in the peaq SDK to perform the operation. | | **baseUrl** | `string` | Required | N/A | RPC url used to connect to the blockchain. Reference [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq) for supported RPC urls. | | **seed** | `string` | Required | N/A | Ethereum private key linked to the wallet executing the transactions. Allows the backend to perform write transactions on behalf of the user. | ```javascript Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env['EVM_PRIVATE']; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); // e.g. sdk.did.create(), sdk.did.update(), sdk.storage.create() ... etc. // all return back a valid EVM tx that is used in sendEvmTx() const tx = await sdk.operation.function({}); const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // receipt object of EVM Transaction for a create DID Operation TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000800', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x2eebdc26f2997cf1908bc742c8567ef607498880e5e603012836fcede9a481f3', index: 0, blockHash: '0x857356cffe490f429900d550b3f5fec7c3874c7de53c149116e834c4cc43d5dc', blockNumber: 2621813, logsBloom: '0x00000040000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 48090n, blobGasUsed: null, cumulativeGasUsed: 48090n, gasPrice: 1024n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` # Serialized DID Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/serialized-did Our SDK offers the ability to generate a serialized DID Document. On the blockchain itself we do not store the entire DID Document Object representation, rather we store the serialized document in hexadecimal. This is useful if you would like to manually send the document on-chain. To get a better understanding about what is being stored please refer to the DID Document [Custom Document Fields](/peaqchain/sdk-reference/javascript/did-operations#customdocumentfields) section. ## generateDidDocument(address, customDocumentFields) | Parameter | Type | EVM | Substrate | Description | | ------------------------ | ---------------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | | **address** | `string` | Required | Required | Used for the id and controller fields in the generated DID Document. | | **chainType** | `ChainType` | Required | Required | Defines the expected type of address to be passed. H160 for EVM and SS58 for Substrate. Defaults expect SS58 address. | | **customDocumentFields** | `customDocumentFields` | Optional | Optional | Used to set specific fields of the DID Document. If none is set, an empty document will be generated with id and controller set. | ```javascript Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; // evm address example const address = '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C'; // possible example of custom document fields a DePIN could create const customFields = { services: [{ id: "#ipfs", type: "machineData", serviceEndpoint: "https://ipfs.io/ipfs/QmYwAPJzv5CZsnAzt8auVTLv3jE9n1Tz8exzhTHtVMJd3t" }] } const result = await Sdk.generateDidDocument({ address: address, chainType: Sdk.ChainType.EVM, customDocumentFields: customFields }); ``` ```javascript Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // result object of generated DID Document for the value stored on chain { value: '0a336469643a706561713a30783945656162316143636231413730316145664142303046336238613237356133393634363634314312336469643a706561713a3078394565616231614363623141373031614566414230304633623861323735613339363436363431432a590a052369706673120b6d616368696e65446174611a4368747470733a2f2f697066732e696f2f697066732f516d597741504a7a7635435a736e417a7438617556544c76336a45396e31547a3865787a68544874564d4a643374' } ``` # Storage Operations Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/storage-operations Another basic operation peaq provides is on-chain storage, allowing data to be saved using a `key: value` structure. ## addItem(itemType, item, seed) Adds an entry pair to the blockchain. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **itemType** | `string` | Required | Required | Key that will be stored in peaq storage. Max 64 bytes. | | **item** | `object` | Required | Required | Value that will be stored in peaq storage. Max 256 bytes. | | **seed** | `string` | N/A | Required | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Add Item Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const key = "my_key"; const value = "my_value"; const tx = await sdk.storage.addItem({ itemType: key, item: value }); const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.addItem({}) return object { to: '0x0000000000000000000000000000000000000801', data: '0x257c3c030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000066d795f6b6579000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086d795f76616c7565000000000000000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000801', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x6fec47eadcba1d94e28a6d08b997aa717b69cd3c4319adb7459d21018866602a', index: 0, blockHash: '0x46a7dae54300988b4fdd42957e033d32219c2f09d683c932f00cd96d312f4f54', blockNumber: 4488814, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000800000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000010000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasUsed: 43014n, blobGasUsed: null, cumulativeGasUsed: 43014n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const key = "my_key"; const value = "my_value"; const result = await sdk.storage.addItem({ itemType: key, item: value }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.addItem({}) return object { message: 'Successfully added the storage item type my_key with item my_value for the address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', block_hash: '0xafe84e572ad2ca33cbdad683dfcf2c8ac65bb9944c34304a81010212973739fa', unsubscribe: [Function (anonymous)] } ``` ## readItem(itemType, address, wssBaseUrl) Returns the `key: value` pair of what is stored on-chain. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | **itemType** | `string` | Required | Required | Key that will be used to search peaq storage at the address. | | **address** | `string` | Required | Optional | Wallet address where the key is stored. | | **wssBaseUrl** | `string` | Required | N/A | WSS URL must be used to read from storage. EVM must explicitly set WSS (Substrate has it stored during creation). | ### Get Item Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_ADDRESS = process.env["EVM_ADDRESS"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const key = "my_key"; const pair = await sdk.storage.getItem({ itemType: key, address: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.getItem({}) return object { my_key: 'my_value' } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const key = "my_key"; const pair = await sdk.storage.getItem({ itemType: key }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.getItem({}) return object { my_key: 'my_value' } ``` ## updateItem(itemType, item, seed) Updates the value in storage for the key passed. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **itemType** | `string` | Required | Required | Key that is stored in peaq storage whose value will be updated. | | **item** | `string` | Required | Required | New value that will be stored in peaq storage. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Update Item Code Examples ```javascript EVM Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const HTTPS_BASE_URL = "https://quicknode.peaq.xyz"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const EVM_PRIVATE = process.env["EVM_PRIVATE"]; const sdk = await Sdk.createInstance({ baseUrl: HTTPS_BASE_URL, chainType: Sdk.ChainType.EVM }); const key = "my_key"; const value = "my_new_value"; const tx = await sdk.storage.updateItem({ itemType: key, item: value }); const receipt = await Sdk.sendEvmTx({ tx: tx, baseUrl: HTTPS_BASE_URL, seed: EVM_PRIVATE }); // read to see the change const pair = await sdk.storage.getItem({ itemType: key, address: EVM_ADDRESS, wssBaseUrl: WSS_BASE_URL }); ``` ```javascript EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.updateItem({}) return object { to: '0x0000000000000000000000000000000000000801', data: '0x1cd4bf090000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000066d795f6b65790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c6d795f6e65775f76616c75650000000000000000000000000000000000000000' } // sendEvmTx({}) return object TransactionReceipt { provider: JsonRpcProvider {}, to: '0x0000000000000000000000000000000000000801', from: '0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', contractAddress: null, hash: '0x2fa941ecd230d6440f2421d80582dc38c53dd0b2ae9b8f55c1980f323bd25f7f', index: 18, blockHash: '0x7467a4a72e98c69e6cde157e6fa5d56455ecc7d23aae72129c9d4d9f6512ff63', blockNumber: 4488965, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000800000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010', gasUsed: 32060n, blobGasUsed: null, cumulativeGasUsed: 15354515n, gasPrice: 100000000000n, blobGasPrice: null, type: 2, status: 1, root: undefined } ``` ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: Sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const key = "my_key"; const value = "my_new_value"; const response = await sdk.storage.updateItem({ itemType: key, item: value }); // read to see it has been changed const pair = await sdk.storage.getItem({ itemType: key }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.updateItem({}) return object { message: 'Successfully updated the storage item type my_key to the new item my_new_value for the address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', block_hash: '0xb75cf6b47421c585772220bd99df1850a50e59ce506d6302eaba49db6e029288', unsubscribe: [Function (anonymous)] } ``` ## removeItem(itemType, seed) Removes the item type and item from peaq storage. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | --- | --------- | -------------------------------------------------------------------------------------------- | | **itemType** | `string` | N/A | Required | The key representing the pair to be removed. | | **seed** | `string` | N/A | Optional | If not set at instance creation, allows user to define who sends the Substrate Transactions. | ### Remove Item Code Examples ```javascript Substrate Request theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from "@peaq-network/sdk"; const WSS_BASE_URL = "wss://quicknode.peaq.xyz"; const SUBSTRATE_SEED = process.env["SUBSTRATE_SEED"]; const sdk = await Sdk.createInstance({ baseUrl: WSS_BASE_URL, chainType: sdk.ChainType.SUBSTRATE, seed: SUBSTRATE_SEED }); const key = "my_key"; const response = await sdk.storage.removeItem({ itemType: key }); // disconnect when finished await sdk.disconnect(); ``` ```javascript Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // sdk.storage.removeItem({}) return object { message: 'Successfully removed the storage item type my_key from address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', block_hash: '0xb40d8934dc69c871b8ee36a34ef63ae5fb0b93c06b5d8d5c5df5244e001d3acb', unsubscribe: [Function (anonymous)] } ``` EVM delete storage precompile currently in development. When completed will add to the JavaScript SDK. # UMT Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/umt Universal Machine Time (UMT) is the first onchain implementation of the Precision Time Protocol (PTP), enabling robots, machines, drones, vehicles, and devices to achieve nanosecond-precise time synchronization. ## Why PTP on Blockchain? Traditional blockchain timestamps operate at second-level precision, limiting their utility for high-precision applications. By implementing PTP on peaq, we achieve: * Nanosecond-level precision timing * Distributed time synchronization * Blockchain-verified timestamps * Trustless time coordination for machine networks * Superior Network Time Accuracy: Unlike NTP, which allows for variable offset ranges, PTP provides deterministic and precise time synchronization ## Architecture ptp-image-1 ## Implementation Guide peaq's PTP implementation provides nanosecond-precise time synchronization with just a few lines of code. ### Usage This can be run on any JavaScript-based project (server or client). You would need the peaq-sdk to run it, please refer here for more information. #### subscribeToPtp(masterUrl, interval) **Parameters:** * **masterUrl** REQUIRED ``: The URL of the master clock server, which is used to send requests for synchronization. This is defined as a constant in the code, which is currently [https://ptp.peaq.xyz](https://ptp.peaq.xyz). This server is a central time source that fetches the timestamp from the network at regular block intervals using real-time updates and is responsible for synchronizing the clocks of connected systems to ensure accurate and consistent timekeeping. * **interval** OPTIONAL ``: Periodically synchronizes the clock at a fixed rate. Recorded in milliseconds, defaults to 1000. ```javascript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { Sdk } from '@peaq-network/sdk'; const MASTER_URL = 'https://ptp.peaq.xyz'; // ...contd. // Subscribe to PTP updates const unsubscribe = Sdk.subscribeToPtp( { masterUrl: MASTER_URL }, ({ offset, synchronizedTime }) => { console.log(`Clock Offset: ${offset} nanoseconds`); console.log(`Synchronized Time: ${synchronizedTime} nanoseconds`); } ); // To stop receiving updates unsubscribe(); ``` This is a simple subscription that, when subscribed, will return the results with a 1-second interval, until unsubscribed. ### Sample Output ``` Clock Offset: 1849833 nanoseconds Synchronized Time: 1740669653296849833 nanoseconds ``` The function returns a result that includes the time offset and the synchronized unix timestamp in nanoseconds based on the current local time. Both are computed in bigint format to be accuracy consistent. If an error occurs during synchronization, it returns nothing. The timestamp is in unix, you can verify it using any online unix timestamp conversion tool. ### Synchronization Result Details The synchronization result contains two key properties: * **Offset**: * Description: This represents the calculated time difference between the local clock and the master clock. * Purpose: The offset is used to adjust the local clock to align it with the master clock. A positive offset indicates that the local clock is behind the master clock, while a negative offset indicates it is ahead. * **Synchronized Time**: * Description: This is the local time adjusted by the calculated offset, effectively representing the current time as per the master clock. * Purpose: The synchronized time provides a corrected timestamp that can be used for time-sensitive operations, ensuring consistency with the master clock. ## Synchronization Process The synchronization process involves the following steps: 1. **Sync Message**: * A request is sent to the master clock server. * The server returns a timestamp, and the local timestamp is recorded immediately after receiving the response. 2. **Delay Request**: * A local timestamp is recorded, and a request is sent to the master clock server. * The server returns another timestamp. 3. **Offset Calculation**: * The offset is calculated using the formula: `offset = ((local timestamp after sync - server timestamp) - (local timestamp before delay - delay)) / 2` * The synchronized time is calculated as: `synchronized time = local timestamp after sync + offset` ## Error Handling If any error occurs during the synchronization process, such as network issues or server errors, the function logs the error to the console and returns nothing. However, the ptp server is configured to be highly resilient and capable of handling a large volume of concurrent requests while maintaining availability, through auto-scaling and load balancing. ## Conclusion The synchronization function is essential for achieving precise time synchronization with a master clock server. By calculating the time offset and adjusting the local time, it ensures that the local system time is accurate and consistent with the master clock. The synchronization result provides both the offset and the synchronized time, which are crucial for maintaining time accuracy in distributed systems. # Tier 1 Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/verify/tier-1 This page delineates a procedure to affirm the integrity and origin of machine-generated data within a blockchain network, emphasizing "Machine-Origin Authentication" as Tier 1 verification in a Decentralized Physical Infrastructure Network (DePIN). ## System Overview The process involves the following steps: * **Registration**: The machine gets registered on the blockchain using a unique identifier. * **Data Generation & Signing**: The machine generates data, signs it, and sends the hash to the blockchain. * **Data Storage**: The hash gets stored on the blockchain. * **Data Retrieval & Verification**: Any party retrieves and verifies the data's validity from the blockchain. ## Prerequisites * Node.js environment setup. * Installed `@polkadot/api`, `@polkadot/util`, `@polkadot/util-crypto`, and `@peaq-network/sdk` packages for blockchain interaction. * A seeds.json file containing the seeds for the owner's and the machine's accounts. ### 1. Registration (Machine-Origin Authentication) The machine is registered on the blockchain by the owner, creating a digital identity that serves as the foundation for Tier 1 verification. ``` // import necessary libraries import { Sdk } from "@peaq-network/sdk"; import Keyring from "@polkadot/keyring"; import fs from "fs/promises"; const register = async () => { const seeds = JSON.parse(await fs.readFile("seeds.json", "utf8")); const ownerSeed = seeds.owner; const machineSeed = seeds.machine; const sdkInstance = await Sdk.createInstance({ baseUrl: "wss://wss-async-agung.peaq.xyz", seed: ownerSeed, }); const keyring = new Keyring({ type: "sr25519" }); const machinePair = keyring.addFromUri(machineSeed); // Register the machine's DID await sdkInstance.did.create( { name: `did:peaq:${machinePair.address}`, address: machinePair.address }, (result) => { // handle result } ); }; register(); ``` ### 2. Data Generation & Signing (Direct Device Data) The machine directly generates data, signs it using its secure private key, ensuring that the data is authentic and has not been altered. This constitutes a Tier 1 level of trust. **NOTE** Data signing is an internal process of the machine. Here is a hypothetical code snippet to illustrate this step. ``` // Hypothetical snippet for data generation and signing import { generateKeyPair } from "./utils.js"; import { hexToU8a, u8aToHex } from "@polkadot/util"; import { cryptoWaitReady } from "@polkadot/util-crypto"; // Simulated machine data generation and signing const generateAndSignData = async (machineSeed) => { await cryptoWaitReady(); const machineKeypair = generateKeyPair(machineSeed); const data = "Machine-generated data"; const dataHex = u8aToHex(JSON.stringify(data)); const signature = machineKeypair.sign(hexToU8a(dataHex)); return { dataHex, signature: u8aToHex(signature) }; }; ``` ### 3. Data Storage (Immutable Ledger Entry) Signed data is stored on the blockchain, providing an immutable record that can be publicly verified, which is consistent with the principles of Tier 1 verification. ``` // import necessary libraries and functions import fs from "fs/promises"; import { generateKeyPair, makeExtrinsicCall } from "./utils.js"; import { hexToU8a, u8aToHex } from "@polkadot/util"; import { cryptoWaitReady } from "@polkadot/util-crypto"; const store = async () => { await cryptoWaitReady(); const seeds = JSON.parse(await fs.readFile("seeds.json", "utf8")); const machineSeed = seeds.machine; const machineKeypair = generateKeyPair(machineSeed); const dataHex = u8aToHex(JSON.stringify("test-data")); const signature = u8aToHex(machineKeypair.sign(hexToU8a(dataHex))); const payload = { data: dataHex, signature: signature, }; // Serialize payload into hex format for storage const payloadHex = u8aToHex(JSON.stringify(payload)); await makeExtrinsicCall( "peaqStorage", "addItem", [machineKeypair.address, payloadHex], true, machineKeypair ); }; store(); ``` ### 4. Data Retrieval & Verification (Trust Verification) Any network participant can retrieve the data and verify its authenticity using the public key associated with the machine's digital identity. This verification step confirms the Tier 1 status of the data. ``` // import necessary libraries and functions import fs from 'fs/promises'; import { hexToU8a } from '@polkadot/util'; import { signatureVerify } from '@polkadot/util-crypto'; import { getStorage, generateKeyPair } from './utils.js'; const verifyData = async (publicKey, dataHex, signatureHex) => { const dataU8a = hexToU8a(dataHex); const signatureU8a = hexToU8a(signatureHex); return signatureVerify(dataU8a, signatureU8a, publicKey).isValid; }; const verify = async () => { const seeds = JSON.parse(await fs.readFile('seeds.json', 'utf8')); const machineSeed = seeds.machine; const machineKeypair = generateKeyPair(machineSeed); const itemType = 'your_item_type_here'; const storedDataHex = await getStorage(itemType); if (!storedDataHex) { throw new Error('Data not found.'); } const { data: dataHex, signature: signatureHex } = JSON.parse(hexToU8a(storedDataHex).toString()); const isValid = await verifyData(machineKeypair.publicKey, dataHex, signatureHex); if (isValid) { console.log('Data verified successfully.'); } else { console.log('Verification failed.'); } }; verify(); ``` ## Conclusion The system described here provides a secure method for registering machines and verifying the origin and integrity of their data using cryptographic signatures. This approach aligns with Tier 1 verification in DePIN projects, ensuring the highest level of trust in the data's direct origin and secure transmission. ### Additional Considerations * Ensure that all key pairs and sensitive information are handled securely. * Conduct thorough testing in a controlled environment before deploying to production. * Always keep up to date with the SDK and API documentation from the blockchain network you're working with. Developers should refer to the official documentation of the Polkadot.js API and the specific blockchain network for detailed instructions and best practices. *** Building upon the established structure of "Machine Data Verification on Blockchain," we will now introduce the concept of Tier 2 verification, termed "Pattern Matching Validation." This tier involves validating data that may not originate directly from the device but is relevant to existing devices within the network. The verification in this tier is accomplished by comparing incoming data patterns against known patterns from devices already registered in the system. # Tier 2 Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/verify/tier-2 Tier 2 verification strengthens the data integrity framework by validating data through pattern matching. It is designed for scenarios where data does not originate directly from the device but is still crucial for the network's operations. ## Pattern Matching Validation Process * **Pattern Learning**: The system learns and stores patterns of data from registered devices. * **Data Submission**: A machine or an external source submits data to the blockchain. * **Pattern Comparison**: The submitted data is compared against learned patterns. * **Validation Outcome**: Data matching known patterns is marked as Tier 2 verified. ## Prerequisites * A pattern recognition module integrated into the blockchain network. * A dataset of known patterns from Tier 1 verified devices for comparison. ### 1. Pattern Learning The blockchain system uses historical Tier 1 verified data to learn and record data patterns associated with each registered machine. ``` // import necessary libraries and functions import { fetchTier1Data, patternRecognition } from './utils.js'; const learnPatterns = async () => { const tier1Data = await fetchTier1Data(); // Process and store patterns for each machine tier1Data.forEach((dataEntry) => { const patterns = patternRecognition(dataEntry); storePatterns(dataEntry.machineId, patterns); }); }; learnPatterns(); ``` ### 2. Data Submission Data is submitted to the blockchain, potentially originating from external sources or indirectly from machines. ``` // import necessary libraries and functions import { submitToBlockchain } from './utils.js'; const submitData = async (data) => { // Data submission can be from machines or external sources // Data is preprocessed and packaged for submission const submissionPackage = preprocessData(data); submitToBlockchain(submissionPackage); }; submitData(externalData); ``` ### 3. Pattern Comparison When new data is submitted, it is compared against the stored patterns to validate its trustworthiness. ``` // import necessary libraries and functions import { retrievePatterns, compareWithKnownPatterns } from './utils.js'; const validateData = async (submittedData) => { const knownPatterns = await retrievePatterns(submittedData.machineId); // Compare submitted data against known patterns const isValid = compareWithKnownPatterns(submittedData, knownPatterns); return isValid; }; ``` ### 4. Validation Outcome Data that matches the stored patterns is marked as Tier 2 verified, indicating a level of trustworthiness, though slightly less than Tier 1 verified data. ``` // import necessary libraries and functions import { updateDataStatus } from './utils.js'; const markAsTier2Verified = async (dataId, isValid) => { if (isValid) { // Update the data status on the blockchain to reflect Tier 2 verification updateDataStatus(dataId, 'Tier2Verified'); } else { // Handle unverified data accordingly handleUnverifiedData(dataId); } }; // Example usage markAsTier2Verified(submittedDataId, validationOutcome); ``` ## Conclusion Tier 2 verification augments the data assurance framework by leveraging pattern matching to validate data authenticity. It provides a method to establish trust in data that may not come directly from a device but still holds relevance within the network. ## Additional Considerations * Enhance the pattern recognition module to handle evolving data patterns. * Implement safeguards against false positives in pattern matching. * Continuously update the pattern database to include new data types and sources. For more detailed implementation strategies and best practices, developers should refer to the documentation specific to the pattern recognition technology used and the blockchain network's capabilities. *** This section expands the document to include Tier 2 verification, providing a logical extension to the robust verification system. It ensures that the DePIN's integrity is maintained even for data that cannot be verified at the Tier 1 level. The code snippets provided are hypothetical and meant to illustrate the proposed functionality. In a real-world scenario, the actual implementation may require more complex algorithms and integration with blockchain-specific services. Continuing the comprehensive structure for "Machine Data Verification on Blockchain," the next segment will incorporate Tier 3 verification, designated as "Oracle-Backed Authentication." This tier introduces an external validation mechanism where data is verified through oracles that serve as bridges between the blockchain and external data sources. # Tier 3 Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/verify/tier-3 Tier 3 verification introduces an extra layer of validation by employing oracles. This is particularly useful for data that requires external corroboration or originates from less secure or indirect sources. ## Oracle-Backed Verification Process 1. **Oracle Selection**: A trusted oracle is chosen to serve as an intermediary for data verification. 2. **Data Submission & Request**: Data is submitted to the blockchain, and a request is made to the oracle for verification. 3. **Oracle Verification**: The oracle verifies the data's authenticity against external sources or predefined criteria. 4. **Verification Result**: The oracle provides a verification result, which is then recorded on the blockchain. ## Prerequisites * Integration with a trusted oracle service that can provide external data verification. * Smart contracts or blockchain protocols capable of handling oracle responses. ### 1. Oracle Selection Identify and integrate a trusted oracle service into the blockchain system for external data verification. ``` // import necessary libraries and functions import { OracleService } from './oracleService.js'; const selectOracle = async () => { // Select a trusted oracle from a list of available services const oracle = new OracleService('trusted_oracle_url'); return oracle; }; const trustedOracle = selectOracle(); ``` ### 2. Data Submission & Request Data is submitted to the blockchain, and a verification request is sent to the chosen oracle. ``` // import necessary libraries and functions import { trustedOracle } from './selectOracle.js'; import { submitToBlockchain } from './utils.js'; const submitDataAndRequestVerification = async (data) => { // Data submission to the blockchain const submissionPackage = preprocessData(data); submitToBlockchain(submissionPackage); // Send a verification request to the oracle trustedOracle.requestVerification(data); }; submitDataAndRequestVerification(externalData); ``` ### 3. Oracle Verification The oracle conducts verification by comparing the submitted data against reliable external sources or predefined criteria. ``` // Assuming the oracle service provides a method to validate data import { trustedOracle } from './selectOracle.js'; const verifyDataWithOracle = async (data) => { const verificationResult = await trustedOracle.verify(data); return verificationResult; }; ``` ### 4. Verification Result The oracle's verification result is received and recorded on the blockchain, completing the Tier 3 verification process. ``` // import necessary libraries and functions import { updateDataStatusWithOracle } from './utils.js'; const recordVerificationResult = async (dataId, oracleResult) => { // Record the oracle's verification result on the blockchain updateDataStatusWithOracle(dataId, oracleResult); }; // Example usage recordVerificationResult(submittedDataId, oracleVerificationOutcome); ``` ## Conclusion Tier 3 verification ensures data authenticity by involving oracles for an added layer of external validation. This method is key for integrating data that requires external references or for which direct device-origin verification is not possible. ## Additional Considerations * Establish strict criteria for selecting oracles to ensure reliability and objectivity. * Regularly audit and monitor oracle performance to guard against any potential vulnerabilities. * Implement consensus mechanisms among multiple oracles to further bolster trust in the verification process. For practical application, developers should follow the guidelines provided by the blockchain network for integrating oracles and managing external data verification. *** This section concludes the "Machine Data Verification on Blockchain" document, providing a full spectrum of data verification tiers suitable for various trust levels within a DePIN. Tier 3 verification ensures that even data requiring external validation can be trusted within the network, thus maintaining the system's integrity. The provided code snippets are conceptual and should be adapted for specific use cases, oracle services, and blockchain network protocols. # peaq verify Source: https://docs.peaq.xyz/peaqchain/sdk-reference/javascript/verify/verify In order to establish a secure and decentralized method for verifying physical Machine data within a Decentralized Physical Infrastructure Network (DePIN), peaq works on a multi-tiered approach. The following elaborates on each tier, providing an overview for each step in the verification process. ## Tier 1 Verification: Machine-Origin Authentication * **Description**: Data originating directly from devices can be verified by a shortly released SDK or by the functions outlined in the documentation. The predominant use cases within this tier primarily center around the communication capabilities of DePINs via cellular and smart devices. * **Trust Level**: Data verified under this tier is marked as the most trusted, given its direct origin and secure transmission. * **Implementation**: The system performs device detection and signs the data using a private key, ensuring its authenticity before transmitting it to the network. Integrated software solutions, including Home Assistant, will facilitate this process, maintaining the highest level of security and integrity. ## Tier 2 Verification: Pattern Matching Validation * **Description**: Data that is not directly from the device but is relevant to existing devices on the network is validated. * **Implementation**: Validation occurs by comparing data patterns from an existing device on the network. Similar patterns indicate trustworthiness. * **Trust Level**: Tier 2 data is considered trustworthy, though less so than Tier 1. ## Tier 3 Verification: Oracle-Backed Authentication * **Description**: Data is verified through an oracle, providing an additional layer of security. * **Implementation**: The oracle confirms the legitimacy of the data source, adding an extra level of validation. * **Trust Level**: Tier 3 data is considered secure, but may still be subject to potential vulnerabilities. # Create Instance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/create-instance The [Python SDK](https://pypi.org/project/peaq-sdk/) supports **EVM** and **Substrate** transactions. The following code examples are for version `0.2.1`, with a new reference coming soon for version `1.0.0`. When installing the SDK, use the following command: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-sdk==0.2.1 ``` It can either: * Return an unsigned transaction object for manual submission, or * Automatically submit the transaction if a **private key** (EVM) or **mnemonic seed** (Substrate) is provided. Currently, the SDK supports modules for **Decentralized Identifiers (DID)**, **Storage**, **RBAC** (Role-Based Access Control), and **Transfers**. With **Machine Station Factory** and **UMT** modules coming soon. To get started, you'll need to determine: * Whether you're building for **EVM** or **Substrate**, and * Whether you want the SDK to **sign and send transactions**, or just prepare them. ## create\_instance(base\_url, chain\_type, seed) Initializes the SDK and returns an instance ready to interact with the network. | Parameter | Type | EVM | Substrate | Description | | --------------- | ----------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **base\_url** | `string` | Required | Required | HTTPS/WSS URL used to connect to the blockchain. Reference [connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq) for supported URLs. Use **HTTPS** for EVM and **WSS** for Substrate (and EVM reads). | | **chain\_type** | `ChainType` | Required | Required | Defines whether the instance will generate EVM or Substrate transactions. | | **seed** | `string` | Optional | Optional | Private key (EVM) or mnemonic phrase (Substrate) used to execute write transactions on behalf of the user. If not set, the peaq transaction object is returned to the user for manual submission. | ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, ) ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) ``` # DID Operations Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/did-operations This concept has been absorbed into peaqOS. See [peaqID](/peaqos/concepts/peaqid) for the current identity model. Using the initialized SDK instance, you can create a **Decentralized Identifier (DID)** on-chain, enabling identity-based operations within the peaq network. This operation generates a DID document and either: * Returns an unsigned transaction or extrinsic (if no signer is available), or * Submits the DID creation directly to the blockchain (if signing credentials are present). ## create(name, custom\_document\_fields, address) Enables the creation of a decentralized identity on-chain for your given entity. | Parameter | Type | EVM | Substrate | Description | | ---------------------------- | ---------------------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID to be created. | | **custom\_document\_fields** | `CustomDocumentFields` | Required | Required | Sets specific fields of the DID Document. | | **address** | `string` | Required\* | Required\* | Wallet address that is used to create the DID Document. If seed has been set, defaults to the corresponding public key address. | \* The address is required when no seed is set. ### CustomDocumentFields | Parameter | Type | EVM | Substrate | Description | | ----------------- | ---------------- | -------- | --------- | ------------------------------------------------------------------------------------------- | | **verifications** | `Verification[]` | Optional | Optional | Stores the verification method that is tied to the wallet used to create this DID Document. | | **signature** | `Signature` | Optional | Optional | Used to store an issuer's signature. | | **services** | `Services[]` | Optional | Optional | Links data, endpoints, or other metadata to the DID Document. | #### Verification | Parameter | Type | EVM | Substrate | Description | | ---------------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **type** | `string` | Optional | Optional | Allows user to manually set the type of verification to be used with the public key tied to the DID Document.

**EVM Wallets:**
- `EcdsaSecp256k1RecoveryMethod2020`
**Substrate Wallets:**
- `Ed25519VerificationKey2020`
- `Sr25519VerificationKey2020` | | **publicKeyMultibase** | `string` | Optional | Optional | Allows user to manually set the public Key MultiBase they would like to use for their verification method. | #### Signature | Parameter | Type | EVM | Substrate | Description | | ---------- | -------- | -------- | --------- | ----------------------------------------------- | | **type** | `string` | Required | Required | The algorithm used when generating a signature. | | **issuer** | `string` | Required | Required | Entity that issued the signature. | | **hash** | `string` | Required | Required | Hash value of the generated signature. | #### Services | Parameter | Type | EVM | Substrate | Description | | ------------------- | -------- | -------- | --------- | --------------------------------------------------------------- | | **id** | `string` | Required | Required | Identifier used to indicate what type of service is being used. | | **type** | `string` | Required | Required | Declares the type of service object being referenced. | | **serviceEndpoint** | `string` | Optional | Optional | URI/URL that is used to point to another data storage location. | | **data** | `string` | Optional | Optional | Data value that can be stored at this service. | ### Create DID Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) name = 'DID_NAME_001' response = sdk.did.create( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='EcdsaSecp256k1RecoveryMethod2020')], signature=Signature(type='EcdsaSecp256k1RecoveryMethod2020', issuer='0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', hash='123'), services=[Service(id='#ipfs', type='peaqStorage', data='123')] ), address=EVM_ADDRESS ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed DID create transaction for 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C of the name DID_NAME_001. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000800", "data": "0xcc4a70ca00000000000000000..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) name = 'DID_NAME_001' response = sdk.did.create( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='EcdsaSecp256k1RecoveryMethod2020')], signature=Signature(type='EcdsaSecp256k1RecoveryMethod2020', issuer='0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', hash='123'), services=[Service(id='#ipfs', type='peaqStorage', data='123')] ) ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the DID under the name DID_NAME_001 for user 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xaf146b28b2e1d8dae3200cb8e0878c67ac8614f68fb68d43905fd84539b70175", "transactionIndex": 0, "blockHash": "0x9df027467fd945434ad7c5eb90e798f66b30650a203749c3cf0c2d021cadf009", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000800", "blockNumber": 5845831, "cumulativeGasUsed": 63881, "gasUsed": 63881, "contractAddress": null, "status": 1, "effectiveGasPrice": 102000000000, "type": 2, "logs": [ { "address": "0x0000000000000000000000000000000000000800", "topics": [ "0x13aef52bc4a99da04591533072e304017e3fb76f43e7fadd25eb7f514c5ef6e5" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c...", "blockHash": "0x9df027467fd945434ad7c5eb90e798f66b30650a203749c3cf0c2d021cadf009", "blockNumber": 5845831, "transactionHash": "0xaf146b28b2e1d8dae3200cb8e0878c67ac8614f68fb68d43905fd84539b70175", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000004000000000000000000000000000000000000000000000000000000400..." } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) name = 'DID_NAME_001' response = sdk.did.create( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='Sr25519VerificationKey2020')], signature=Signature(type='Sr25519VerificationKey2020', issuer=SUBSTRATE_ADDRESS, hash='123'), services=[Service(id='#ipfs', type='peaqStorage', data='123')] ), address=SUBSTRATE_ADDRESS ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed DID create call for 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg of the name DID_NAME_001. You must sign and send externally.", "call": { "call_module": "PeaqDid", "call_function": "add_attribute", "call_args": { "did_account": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "name": "DID_NAME_001", "value": "0a396469643a70656171...", "valid_for": null } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) name = 'DID_NAME_001' response = sdk.did.create( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='Sr25519VerificationKey2020')], signature=Signature(type='Sr25519VerificationKey2020', issuer='5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', hash='123'), services=[Service(id='#ipfs', type='peaqStorage', data='123')] ) ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the DID under the name DID_NAME_001 for user 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0xf325d23c0c0531f41a22931c18b4d695956b1add7586ec2b76810fd0a7fdc80e", "block_hash": "0xd9ccec8fdb461e59d45beea52332e737a08bee268e1b29532651ad0ac1f53346", "finalized": false, "extrinsic_index": 2, "call": { "module": "PeaqDid", "function": "add_attribute", "args": { "did_account": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "name": "DID_NAME_001", "value": "0a396469643a70656171..." // Truncated hex blob } }, "events": [...], "is_success": true, "total_fee_amount": 2528404367045865 } } ``` ## read(name, address) Allows a user to read a previously created DID from the chain. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | ---------- | ---------- | ---------------------------------------------------- | | **name** | `string` | Required | Required | Name of the DID stored. | | **address** | `string` | Required\* | Required\* | Address of the wallet that created the DID Document. | \* The address is required when no seed is set. ### Read DID Code Examples ```python EVM No Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) name = 'DID_NAME_001' document = sdk.did.read(name=name, address=EVM_ADDRESS) ``` ```python EVM No Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "name": "DID_NAME_001", "value": "0a336469643a70656171...", "validity": "4294967295", "created": "1751392704000", "document": { "id": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "controller": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "verificationMethods": [ { "id": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C#keys-1", "type": "EcdsaSecp256k1RecoveryMethod2020", "controller": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "publicKeyMultibase": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C" } ], "signature": { "type": "EcdsaSecp256k1RecoveryMethod2020", "issuer": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "hash": "123" }, "services": [ { "id": "#ipfs", "type": "peaqStorage", "data": "123" } ], "authentications": [ "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C#keys-1" ] } } ``` ```python EVM Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) name = 'DID_NAME_001' document = sdk.did.read(name=name) ``` ```python EVM Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "name": "DID_NAME_001", "value": "0a336469643a706561713a307...", "validity": "4294967295", "created": "1751392704000", "document": { "id": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "controller": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "verificationMethods": [ { "id": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C#keys-1", "type": "EcdsaSecp256k1RecoveryMethod2020", "controller": "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "publicKeyMultibase": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C" } ], "signature": { "type": "EcdsaSecp256k1RecoveryMethod2020", "issuer": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "hash": "123" }, "services": [ { "id": "#ipfs", "type": "peaqStorage", "data": "123" } ], "authentications": [ "did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C#keys-1" ] } } ``` ```python Substrate No Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) name = 'DID_NAME_001' document = sdk.did.read(name=name, address=SUBSTRATE_ADDRESS) ``` ```python Substrate No Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "name": "DID_NAME_001", "value": "0a396469643a70656171...", "validity": "4294967295", "created": "1751394168000", "document": { "id": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "controller": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "verificationMethods": [ { "id": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg#keys-1", "type": "Sr25519VerificationKey2020", "controller": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "publicKeyMultibase": "z6QNpkN4bjiLRd5wzb2jYKiaMNVVyPrw3SNkcpXX19LtTEUJ" } ], "signature": { "type": "Sr25519VerificationKey2020", "issuer": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "hash": "123" }, "services": [ { "id": "#ipfs", "type": "peaqStorage", "data": "123" } ], "authentications": [ "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg#keys-1" ] } } ``` ```python Substrate Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) name = 'DID_NAME_001' document = sdk.did.read(name=name) ``` ```python Substrate Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "name": "DID_NAME_001", "value": "0a396469643a706561713a354...", "validity": "4294967295", "created": "1751394168000", "document": { "id": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "controller": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "verificationMethods": [ { "id": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg#keys-1", "type": "Sr25519VerificationKey2020", "controller": "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "publicKeyMultibase": "z6QNpkN4bjiLRd5wzb2jYKiaMNVVyPrw3SNkcpXX19LtTEUJ" } ], "signature": { "type": "Sr25519VerificationKey2020", "issuer": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "hash": "123" }, "services": [ { "id": "#ipfs", "type": "peaqStorage", "data": "123" } ], "authentications": [ "did:peaq:5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg#keys-1" ] } } ``` ## update(name, custom\_document\_fields, address) Updates an existing DID identified by name, overwriting the entire DID document with new `custom_document_fields`. Use caution, as all existing data is replaced with the newly provided fields. Recommended to use the `read` method to view the current DID document before updating. | Parameter | Type | EVM | Substrate | Description | | ---------------------------- | ---------------------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------ | | **name** | `string` | Required | Required | Name of the DID to be updated. | | **custom\_document\_fields** | `CustomDocumentFields` | Required | Required | New fields to embed in the DID Document. These fully replace the prior document. | | **address** | `string` | Required\* | Required\* | Wallet address that owns the DID Document. If seed has been set, defaults to the corresponding public key address. | \* The address is required when no seed is set. ### Update DID Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) name = 'DID_NAME_001' response = sdk.did.update( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='EcdsaSecp256k1RecoveryMethod2020')], signature=Signature(type='EcdsaSecp256k1RecoveryMethod2020', issuer='0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', hash='456'), services=[Service(id='#updated-ipfs', type='peaqStorage', data='updated-data')] ), address=EVM_ADDRESS ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed DID update transaction for 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C of the name DID_NAME_001. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000800", "data": "0x68b4b2c10000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c4449445f4e414d455f30303100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d2306133333634363936343361373036353631373133613330373833393435363536313632333136313433363336323331343133373330333136313435363634313432333033303436333336323338363133323337333536313333333933363334333633363334333134333132333336343639363433613730363536313731336133303738333934353635363136323331363134333" } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) name = 'DID_NAME_001' response = sdk.did.update( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='EcdsaSecp256k1RecoveryMethod2020')], signature=Signature(type='EcdsaSecp256k1RecoveryMethod2020', issuer='0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C', hash='456'), services=[Service(id='#updated-ipfs', type='peaqStorage', data='updated-data')] ) ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "transactionHash": "0x115cbb93e693d23b45c36016d7035d96aef01964f71b857303b2a9ade23504d1", "transactionIndex": 1, "blockHash": "0xc8dfbf2325cad44c8f3132d8a49a4e364397507bca5e720edd05b8b64039c431", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000800", "blockNumber": 5846312, "cumulativeGasUsed": 309376, "gasUsed": 49745, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000800", "topics": [ "0x018be4d5e2634aefdb51c4ddd186f33072cfb5f6baf685579c2e214995dc9e4f" ], "data": "0x0000000000000000...000000", "blockHash": "0xc8dfbf2325cad44c8f3132d8a49a4e364397507bca5e720edd05b8b64039c431", "blockNumber": 5846312, "transactionHash": "0x115cbb93e693d23b45c36016d7035d96aef01964f71b857303b2a9ade23504d1", "transactionIndex": 1, "logIndex": 4, "removed": false } ], "logsBloom": "0x0000004000000000...000000", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) name = 'DID_NAME_001' response = sdk.did.update( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='Ed25519VerificationKey2020')], signature=Signature(type='Ed25519VerificationKey2020', issuer=SUBSTRATE_ADDRESS, hash='456'), services=[Service(id='#updated-ipfs', type='peaqStorage', data='updated-data')] ), address=SUBSTRATE_ADDRESS ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "call_module": "PeaqDid", "call_function": "update_attribute", "call_args": { "did_account": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "name": "DID_NAME_001", "value": "0a39646964...736b6579732d31", "valid_for": null } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType, CustomDocumentFields, Verification, Service, Signature load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) name = 'DID_NAME_001' response = sdk.did.update( name=name, custom_document_fields= CustomDocumentFields( verifications=[Verification(type='Ed25519VerificationKey2020')], signature=Signature(type='Ed25519VerificationKey2020', issuer='5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg', hash='456'), services=[Service(id='#updated-ipfs', type='peaqStorage', data='updated-data')] ) ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated the DID under the name DID_NAME_001 for user 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0x6f72e86e2c50424866195ab7e7c9f08cd9bcb51621d8dc0cd503320a36df8135", "block_hash": "0xca84e35ef7bc07ec487a132d9967a19e8a901ec7ed9e39fe43baa3b39a671132", "finalized": false, "extrinsic_index": 2, "extrinsic": { "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "module": "PeaqDid", "function": "update_attribute", "args": { "did_account": "5Df42mkzt...", "name": "DID_NAME_001", "value": "... (truncated)", "valid_for": null }, "nonce": 1878, "tip": 0 }, "events": [...], "success": true, "total_fee": 2013819302693817, "weight": { "ref_time": 315663944, "proof_size": 6153 } } } ``` ## remove(name, address) Removes an existing on-chain DID identified by name. Once removed, the DID data is no longer accessible via subsequent reads. | Parameter | Type | EVM | Substrate | Description | | ----------- | -------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------ | | **name** | `string` | Required | Required | Name of the DID to be removed from the chain. | | **address** | `string` | Required\* | Required\* | Wallet address that owns the DID Document. If seed has been set, defaults to the corresponding public key address. | \* The address is required when no seed is set. ### Remove DID Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) name = 'DID_NAME_001' response = sdk.did.remove(name=name, address=EVM_ADDRESS) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed DID remove transaction for 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C of the name DID_NAME_001. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000800", "data": "0xe8a816900000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000c4449445f4e414d455f3030310000000000000000000000000000000000000000" } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) name = 'DID_NAME_001' response = sdk.did.remove(name=name) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully removed the DID under the name DID_NAME_001 for user 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xfe305926a1a3fbfa2ed43d8d2795d8faefcee587dac1c9726943516e081359f0", "transactionIndex": 0, "blockHash": "0x8990405a02f54447595a1339b1a9952e58267a9a86044c9e7d8d23310dc98947", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000800", "blockNumber": 5846365, "cumulativeGasUsed": 44271, "gasUsed": 44271, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000800", "topics": [ "0x8d8507f4f79e585e0cbbdeff53c51bfb6ffa6d44fbcc86e46ece2fad02b5d902" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275...", "blockHash": "0x8990405a02f54447595a1339b1a9952e58267a9a86044c9e7d8d23310dc98947", "blockNumber": 5846365, "transactionHash": "0xfe305926a1a3fbfa2ed43d8d2795d8faefcee587dac1c9726943516e081359f0", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000400000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) name = 'DID_NAME_001' response = sdk.did.remove(name=name, address=SUBSTRATE_ADDRESS) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed DID remove call for 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg of the name DID_NAME_001. You must sign and send externally.", "call": { "call_module": "PeaqDid", "call_function": "remove_attribute", "call_args": { "did_account": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "name": "DID_NAME_001" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) name = 'DID_NAME_001' response = sdk.did.remove(name=name) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully removed the DID under the name DID_NAME_001 for user 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0x554789ce0eb9c59f082867255e92a19c3e2dc6c54fff13f21c5f2892a3dd6dfd", "block_hash": "0xc936185f3cffca622b4b0e97dfd19ea3598252f133367d6ea1a8f8b3bd1b7221", "block_number": null, "finalized": false, "extrinsic_index": 2, "extrinsic": { "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "call": { "module": "PeaqDid", "function": "remove_attribute", "args": [ { "name": "did_account", "type": "AccountId", "value": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg" }, { "name": "name", "type": "BoundedVecName", "value": "DID_NAME_001" } ] }, "nonce": 1879 }, "events": [...], "success": true, "total_fee": 2387350825469955 } } ``` # Group Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/rbac-operations/group ## create\_group(group\_name, group\_id) Used to create a new group within the RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | --------------- | ----- | -------- | --------- | ----------------------------------------------------------------------------- | | **group\_name** | `str` | Required | Required | Name of the group to be created. | | **group\_id** | `str` | Optional | Optional | ID of the group. If not provided, a new ID will be generated (32 characters). | ### Create Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_name = "test_group_1" response = sdk.rbac.create_group(group_name=group_name) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create group call for the group name of test_group_1 and group id of d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x65c1e09c64303763643735352d386632662d34393766..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) group_name = "test_group_1" response = sdk.rbac.create_group(group_name=group_name) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC group under the group name of test_group_1 with the group id of 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x08a3a22c791596271d51e0c896e704efc28f360fa8912f1842ec3e6e4d2ef382", "transactionIndex": 0, "blockHash": "0x452c99c0e179f477d544eba3d9cea2566726582aaab420a0d896b2a1eed87b4a", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847190, "cumulativeGasUsed": 44942, "gasUsed": 44942, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xa02084845c66cf1021190e4778630b0a6de58f9606261968e3a6a203699cedd1" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c333...", "blockHash": "0x452c99c0e179f477d544eba3d9cea2566726582aaab420a0d896b2a1eed87b4a", "blockNumber": 5847190, "transactionHash": "0x08a3a22c791596271d51e0c896e704efc28f360fa8912f1842ec3e6e4d2ef382", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000000000000000400000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_name = "test_group_1" response = sdk.rbac.create_group(group_name=group_name) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create group call for the group name of test_group_1 and group id of 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "add_group", "call_args": { "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322", "name": "test_group_1" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) group_name = "test_group_1" response = sdk.rbac.create_group(group_name=group_name) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC group under the group name of test_group_1 with the group id of 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0x9c1b654a3d7ae7a58c8c210956ee3fdef9e0d17a3b14869dbfbb5f3e4167d1c8", "block_hash": "0xce4285fe64a7ff2d835031a0dec63b115d0617d3fa7b573148562e849baae3c4", "finalized": false, "extrinsic_index": 2, "call": { "module": "PeaqRbac", "function": "add_group", "args": { "group_id": "1801710e-b83b-4f1e-ba91-f7d53dd2", "name": "test_group_1" }, "call_hash": "0x1539dacdf787f8de1eb622d956caa7e9ccaaf1f3f005ffb25470d379481e447d" }, "events": [...], "success": true, "total_fee_amount": 2510624816339150 } } ``` ## fetch\_group(owner, group\_id) Fetches group information from the RBAC system based on the provided group ID and owner's address. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ---------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner of the group. | | **group\_id** | `str` | Required | Required | ID of the group to be fetched (32 characters). | ### Fetch Group Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" response = sdk.rbac.fetch_group(owner=EVM_ADDRESS, group_id=group_id) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "38c9ccb3-2346-4b25-ab28-51e1e2e3", "name": "test_group_1", "enabled": true } ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" response = sdk.rbac.fetch_group(owner=SUBSTRATE_ADDRESS, group_id=group_id) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "1801710e-b83b-4f1e-ba91-f7d53dd2", "name": "test_group_1", "enabled": true } ``` ## fetch\_groups(owner) Used to fetch all the groups associated with the passed owner address. | Parameter | Type | EVM | Substrate | Description | | --------- | ----- | -------- | --------- | --------------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner of all the fetched groups. | ### Fetch Groups Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) response = sdk.rbac.fetch_groups(owner=EVM_ADDRESS) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "afd2ae0e-b1db-42b9-bca1-93e9328b", "name": "test_group_1", "enabled": true }, { "id": "58a52684-3ace-4e72-a025-85085b1d", "name": "peaq-group-1", "enabled": true } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) response = sdk.rbac.fetch_groups(owner=SUBSTRATE_ADDRESS) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "06856fab-c5ff-40cd-8e83-634264c6", "name": "test_group_1", "enabled": true }, { "id": "63d6417e-1a03-4fd8-9439-295ce979", "name": "peaq-group-1", "enabled": true } ] ``` ## update\_group(group\_id, group\_name) Allows updating the name of an existing group in the RBAC system. | Parameter | Type | EVM | Substrate | Description | | --------------- | ----- | -------- | --------- | ---------------------------------------------- | | **group\_id** | `str` | Required | Required | ID of the group to be updated (32 characters). | | **group\_name** | `str` | Required | Required | New name for the group. | ### Update Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" new_group_name = "updated_group_1" response = sdk.rbac.update_group(group_id=group_id, group_name=new_group_name) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update group call for group d07cd755-8f2f-497f-ad53-9d6cf828 with name updated_group_1. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xadb32ee164303763643735352d386632662d343937662d616435332d39643663663832380000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000f757064617465645f67726f75705f310000000000000000000000000000000000" } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" new_group_name = "updated_group_1" response = sdk.rbac.update_group(group_id=group_id, group_name=new_group_name) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated group 38c9ccb3-2346-4b25-ab28-51e1e2e3 with name updated_group_1.", "receipt": { "transactionHash": "0xcffd438327fdc964477ae78f3cc5d905d9d313f2978b6dc38ed7f4ed9647ff89", "transactionIndex": 0, "blockHash": "0xf130dd47b1a2c0e96a53e498d63bdbc911a087e53fec68b40162136e230558cf", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847406, "cumulativeGasUsed": 34396, "gasUsed": 34396, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x874a12c9c30f960e54438e406d3f35a9c0b1e41f61c910516d816ec7ed9011c6" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a396..." "blockHash": "0xf130dd47b1a2c0e96a53e498d63bdbc911a087e53fec68b40162136e230558cf", "blockNumber": 5847406, "transactionHash": "0xcffd438327fdc964477ae78f3cc5d905d9d313f2978b6dc38ed7f4ed9647ff89", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" new_group_name = "updated_group_1" response = sdk.rbac.update_group(group_id=group_id, group_name=new_group_name) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update group call for group 4d56f1e4-b5bd-4a41-8766-1a02d322 with name updated_group_1. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "update_group", "call_args": { "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322", "name": "updated_group_1" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" new_group_name = "updated_group_1" response = sdk.rbac.update_group(group_id=group_id, group_name=new_group_name) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated group 1801710e-b83b-4f1e-ba91-f7d53dd2 with name updated_group_1.", "receipt": { "extrinsic_hash": "0xd93d6afe32e8a494f6e4587c3ebcc28db447eb9dcb205f2b554ccb7f23fa7720", "block_hash": "0x734644badfe943015d101c44e287fc92f4b666285490eb8ed36f6fd5dcfcb2c9", "finalized": false, "extrinsic_index": 2, "call_module": "PeaqRbac", "call_function": "update_group", "call_args": { "group_id": "1801710e-b83b-4f1e-ba91-f7d53dd2", "name": "updated_group_1" }, "events": [...], "total_fee": 2139486851158385 } } ``` ## disable\_group(group\_id) Disables a group within the RBAC system. A disabled group cannot be used for new role assignments. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ----------------------------------------------- | | **group\_id** | `str` | Required | Required | ID of the group to be disabled (32 characters). | ### Disable Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" response = sdk.rbac.disable_group(group_id=group_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable group call for group d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x47538f1264303763643735352d386632..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" response = sdk.rbac.disable_group(group_id=group_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled group 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x3d28fb7e4afb225f04c7cb2bf28e0bdc7bd8bbc57b14a37dbc4e034e2b5d0949", "transactionIndex": 0, "blockHash": "0x0c2ad855d1d9a1a52a65e76f21cb20bbdefddeaa36768d6255d5a9230739f769", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5848348, "cumulativeGasUsed": 33784, "gasUsed": 33784, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x35ffdf1d57e58fa1ceb80230b9f8a6e07c11374d095968b31d6e2fdcc4c53d7c" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c...", "blockHash": "0x0c2ad855d1d9a1a52a65e76f21cb20bbdefddeaa36768d6255d5a9230739f769", "blockNumber": 5848348, "transactionHash": "0x3d28fb7e4afb225f04c7cb2bf28e0bdc7bd8bbc57b14a37dbc4e034e2b5d0949", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000008000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" response = sdk.rbac.disable_group(group_id=group_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable group call for group 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "value": { "call_module": "PeaqRbac", "call_function": "disable_group", "call_args": { "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322" } } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" response = sdk.rbac.disable_group(group_id=group_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled group 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0xfba3fc23dd47dc26f349a998b4e26b97bbfd7ed11bd3d6cc1c64af1de3b5e9f1", "block_hash": "0x6df45a60532c4a215366f34b60fb0934ac275fc3a5244c14bb007a2ad4a3304d", "finalized": false, "events": [...], "total_fee_amount": 2138625423184026 } } ``` ## assign\_role\_to\_group(group\_id, role\_id) Assigns a role to a group in the RBAC system. This grants all members of the group the permissions associated with the role. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ------------------------------------------------------ | | **group\_id** | `str` | Required | Required | ID of the group to assign the role to (32 characters). | | **role\_id** | `str` | Required | Required | ID of the role to be assigned (32 characters). | ### Assign Role to Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" response = sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign role to group call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f and group d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x03c4d7fb30626236303431332d656264642..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned role afd2ae0e-b1db-42b9-bca1-93e9328b to group 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x72ded295dcbbef84c8cae67af567a769c778b6bc667d0d7713cc47f92b1d6f7d", "transactionIndex": 0, "blockHash": "0x46b99ed678287ed88a332131a8df6bd31c1ee53c50ffdcd8483de01c26e02eff", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847482, "cumulativeGasUsed": 42080, "gasUsed": 42080, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xd651bb257632b5e6dd788e141bf8eafe9bbaab70e59f796d0984cdb7a18c8cad" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c616...", "blockHash": "0x46b99ed678287ed88a332131a8df6bd31c1ee53c50ffdcd8483de01c26e02eff", "blockNumber": 5847482, "transactionHash": "0x72ded295dcbbef84c8cae67af567a769c778b6bc667d0d7713cc47f92b1d6f7d", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000..." "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" response = sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign role to group call for role bc3a3e43-6f2e-4e55-83d3-72d5148a and group 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "assign_role_to_group", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned role 06856fab-c5ff-40cd-8e83-634264c6 to group 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0x9bafb69fa2c5ff1de311ec6bbe3887265f52d48cc484729cfe81a7823d320090", "block_hash": "0xb34b3ba10f1eb53500d33866c660deae9ab78a6fe431e0bb136c5a9231f9f659", "finalized": false, "extrinsic_index": 2, "call": { "call_module": "PeaqRbac", "call_function": "assign_role_to_group", "call_args": [ { "name": "role_id", "type": "EntityId", "value": "0x30363835366661622d633566662d343063642d386538332d3633343236346336" }, { "name": "group_id", "type": "EntityId", "value": "0x31383031373130652d623833622d346631652d626139312d6637643533646432" } ] }, "events": [...], "total_fee_amount": 2411661613644938, "is_success": true } } ``` ## fetch\_group\_roles(owner, group\_id) Fetches all roles assigned to a specific group in the RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ---------------------------------------------------- | | **owner** | `str` | Required | Required | Address of the group owner. | | **group\_id** | `str` | Required | Required | ID of the group to fetch roles from (32 characters). | ### Fetch Group Roles Code Examples ```python EVM Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() EVM_ADDRESS = os.getenv("EVM_ADDRESS") HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" response = sdk.rbac.fetch_group_roles(owner=EVM_ADDRESS, group_id=group_id) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "role": "afd2ae0e-b1db-42b9-bca1-93e9328b", "group": "38c9ccb3-2346-4b25-ab28-51e1e2e3" } ] ``` ```python Substrate Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" response = sdk.rbac.fetch_group_roles(owner=SUBSTRATE_ADDRESS, group_id=group_id) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "role": "06856fab-c5ff-40cd-8e83-634264c6", "group": "1801710e-b83b-4f1e-ba91-f7d53dd2" } ] ``` ## unassign\_role\_to\_group(group\_id, role\_id) Removes a role assignment from a group in the RBAC system. This revokes the role's permissions from all group members. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ---------------------------------------------------------- | | **group\_id** | `str` | Required | Required | ID of the group to unassign the role from (32 characters). | | **role\_id** | `str` | Required | Required | ID of the role to be unassigned (32 characters). | ### Unassign Role to Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" response = sdk.rbac.unassign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign role from group call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f and group d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xad58943730626236303431332d6562646..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.unassign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned role afd2ae0e-b1db-42b9-bca1-93e9328b from group 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x36dd75c5b4826e9bb644f76b1729aac2f04b32bc8af579f57b67ac0ff92cbc53", "transactionIndex": 0, "blockHash": "0x5ed9f7af6fcc840cca9edc7d0e17cd6e60beff9e72bf561c8d1017e825d3a186", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847564, "cumulativeGasUsed": 39887, "gasUsed": 39887, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x6673f6cc0e410bc95e511d3c8b391c75558f5f7cb06ce317a6e05934ea60cff6" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c616...", "blockHash": "0x5ed9f7af6fcc840cca9edc7d0e17cd6e60beff9e72bf561c8d1017e825d3a186", "blockNumber": 5847564, "transactionHash": "0x36dd75c5b4826e9bb644f76b1729aac2f04b32bc8af579f57b67ac0ff92cbc53", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" response = sdk.rbac.unassign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign role from group call for role bc3a3e43-6f2e-4e55-83d3-72d5148a and group 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "unassign_role_to_group", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.unassign_role_to_group(group_id=group_id, role_id=role_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned role 06856fab-c5ff-40cd-8e83-634264c6 from group 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0x2543c6fddadcbae7ea487da0e7e04760fa309dc4c02574dea9222f09249bbaef", "block_hash": "0x344a637b2f9482fa4a23022db2a26e9898eaf165ff1c30f7c076b0ef376ee281", "finalized": false, "call": { "module": "PeaqRbac", "function": "unassign_role_to_group", "args": { "role_id": "06856fab-c5ff-40cd-8e83-634264c6", "group_id": "1801710e-b83b-4f1e-ba91-f7d53dd2" } }, "events": [...], "success": true, "total_fee": 2334736376131475 } } ``` ## assign\_user\_to\_group(user\_id, group\_id) Assigns a user to a group in the RBAC system. The user will inherit all roles and permissions assigned to the group. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | ----------------------------------------------------------- | | **user\_id** | `str` | Required | Required | ID of the user to be assigned to the group (32 characters). | | **group\_id** | `str` | Required | Required | ID of the group to assign the user to (32 characters). | ### Assign User to Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" response = sdk.rbac.assign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign user to group call for user 9e8c7866-8435-4b76-8683-709a03c9 and group d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xa76110c439653863373836362d383433352..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" response = sdk.rbac.assign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned user 9e8c7866-8435-4b76-8683-709a03c9 to group 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x03e12d10aeb79f1a46fcd0cd1b373ce6da3056659dc7752bcd3db4ac04804bee", "transactionIndex": 0, "blockHash": "0xde2e5d5fde16013727952341cc5f38c622794c9970215b642287f231f63aad9a", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847609, "cumulativeGasUsed": 40896, "gasUsed": 40896, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x09eada7abd703e77ea8611684f278dfc3e735c19b0b17b32d1a51e23e9032514" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c39...", "blockHash": "0xde2e5d5fde16013727952341cc5f38c622794c9970215b642287f231f63aad9a", "blockNumber": 5847609, "transactionHash": "0x03e12d10aeb79f1a46fcd0cd1b373ce6da3056659dc7752bcd3db4ac04804bee", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" response = sdk.rbac.assign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign user to group call for user 9e8c7866-8435-4b76-8683-709a03c9 and group 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "assign_user_to_group", "call_args": { "user_id": "9e8c7866-8435-4b76-8683-709a03c9", "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" response = sdk.rbac.assign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned user 9e8c7866-8435-4b76-8683-709a03c9 to group 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0x542e07579f5d802867505afd4adfb17ff207dade3caed8757b929500afac4e87", "block_hash": "0xec8ece52b4e616c0d1fae292eb56ff804601946045bd2dea686da02003174d97", "finalized": false, "sender": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "call": { "module": "PeaqRbac", "function": "assign_user_to_group", "user_id": "9e8c7866-8435-4b76-8683-709a03c9", "group_id": "1801710e-b83b-4f1e-ba91-f7d53dd2" }, "events": [...], "success": true, "fee": 2370129280868884 } } ``` ## fetch\_user\_groups(owner, user\_id) Fetches all groups that a specific user is assigned to in the RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | --------------------------------------------------- | | **owner** | `str` | Required | Required | Address of the group owner. | | **user\_id** | `str` | Required | Required | ID of the user to fetch groups for (32 characters). | ### Fetch User Groups Code Examples ```python EVM Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() EVM_ADDRESS = os.getenv("EVM_ADDRESS") HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.fetch_user_groups(owner=EVM_ADDRESS, user_id=user_id) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "user": "9e8c7866-8435-4b76-8683-709a03c9", "group": "38c9ccb3-2346-4b25-ab28-51e1e2e3" } ] ``` ```python Substrate Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.fetch_user_groups(owner=SUBSTRATE_ADDRESS, user_id=user_id) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "user": "9e8c7866-8435-4b76-8683-709a03c9", "group": "1801710e-b83b-4f1e-ba91-f7d53dd2" } ] ``` ## unassign\_user\_to\_group(user\_id, group\_id) Removes a user from a group in the RBAC system. The user will lose all roles and permissions associated with the group. | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | --------------------------------------------------------------- | | **user\_id** | `str` | Required | Required | ID of the user to be unassigned from the group (32 characters). | | **group\_id** | `str` | Required | Required | ID of the group to unassign the user from (32 characters). | ### Unassign User to Group Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "d07cd755-8f2f-497f-ad53-9d6cf828" response = sdk.rbac.unassign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign user from group call for user 9e8c7866-8435-4b76-8683-709a03c9 and group d07cd755-8f2f-497f-ad53-9d6cf828. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x0b9cc42839653863373836362d38343335..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" response = sdk.rbac.unassign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned user 9e8c7866-8435-4b76-8683-709a03c9 from group 38c9ccb3-2346-4b25-ab28-51e1e2e3.", "receipt": { "transactionHash": "0x60f742420c44b769d1c0184c739f9bd4552363f56e6e8619f4ca03486ba35c72", "transactionIndex": 0, "blockHash": "0x77a9c51a20451ef6cd7ee32f440a76dd1a8d9bdf3bf17ff646d081ff17a02005", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847665, "cumulativeGasUsed": 39867, "gasUsed": 39867, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x8ce274ea85050978acba6aae9ca3616564f6fd4e74829427f298d0507a609aa6" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275...", "blockHash": "0x77a9c51a20451ef6cd7ee32f440a76dd1a8d9bdf3bf17ff646d081ff17a02005", "blockNumber": 5847665, "transactionHash": "0x60f742420c44b769d1c0184c739f9bd4552363f56e6e8619f4ca03486ba35c72", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "4d56f1e4-b5bd-4a41-8766-1a02d322" response = sdk.rbac.unassign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign user from group call for user 9e8c7866-8435-4b76-8683-709a03c9 and group 4d56f1e4-b5bd-4a41-8766-1a02d322. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "unassign_user_to_group", "call_args": { "user_id": "9e8c7866-8435-4b76-8683-709a03c9", "group_id": "4d56f1e4-b5bd-4a41-8766-1a02d322" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" response = sdk.rbac.unassign_user_to_group(user_id=user_id, group_id=group_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned user 9e8c7866-8435-4b76-8683-709a03c9 from group 1801710e-b83b-4f1e-ba91-f7d53dd2.", "receipt": { "extrinsic_hash": "0x3060b1cf3c041a79cd992c9769297e1bdfc448e034723c2c1d92afc5f63f8b34", "block_hash": "0x824263ac6db7a08f1f8608b6b376e4b837b99fe15e79bec485d42dd3d8485c0f", "finalized": false, "extrinsic_idx": 2, "call": { "call_module": "PeaqRbac", "call_function": "unassign_user_to_group", "call_args": [ { "name": "user_id", "type": "EntityId", "value": "0x39653863373836362d383433352d346237362d383638332d3730396130336339" }, { "name": "group_id", "type": "EntityId", "value": "0x31383031373130652d623833622d346631652d626139312d6637643533646432" } ] }, "events": [...], "total_fee_amount": 2334013842895666 } } ``` # Permission Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/rbac-operations/permission ## create\_permission(permission\_name, permission\_id) Used to create a new permission within the RBAC (Role-Based Access Control) system. | Parameter | Type | EVM | Substrate | Description | | -------------------- | --------------- | -------- | --------- | ------------------------------------------------------------------------------------ | | **permission\_name** | `str` | Required | Required | Name of the permission to be created. | | **permission\_id** | `Optional[str]` | Optional | Optional | ID of the permission (32 characters). If not supplied one will be generated for you. | ### Create Permission Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_name = "peaq-permission-1" response = sdk.rbac.create_permission(permission_name=permission_name) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create permission call for the permission name of peaq-permission-1 and permission id of 307a081a-45e5-4ca3-b2b5-566ceb31. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x67a2e51533303761303831612d343565352d..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_name = "peaq-permission-1" response = sdk.rbac.create_permission(permission_name=permission_name) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC permission under the permission name of peaq-permission-1 with the permission id of 290506fd-b316-4a10-bd33-e8e2e047.", "receipt": { "transactionHash": "0xf79c1d4e036d3fa85671ec280760991a028f9bc36dc45a4adc1be6dab161bb63", "transactionIndex": 0, "blockHash": "0x752bf6f56b1d1a739025aa778818fd2e5fe8235b5dfd332aac057b18e602f3a8", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847866, "cumulativeGasUsed": 45011, "gasUsed": 45011, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xc6eed8c2ac8c5641c34df1a054ceb451fa58fcb301be09f2f304c01c24c7cde9" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c32393035..." "blockHash": "0x752bf6f56b1d1a739025aa778818fd2e5fe8235b5dfd332aac057b18e602f3a8", "blockNumber": 5847866, "transactionHash": "0xf79c1d4e036d3fa85671ec280760991a028f9bc36dc45a4adc1be6dab161bb63", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_name = "peaq-permission-1" response = sdk.rbac.create_permission(permission_name=permission_name) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create permission call for the permission name of peaq-permission-1 and permission id of 392a18cb-9de1-4f41-82fd-7ed1b1e8. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "add_permission", "call_args": { "permission_id": "392a18cb-9de1-4f41-82fd-7ed1b1e8", "name": "peaq-permission-1" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_name = "peaq-permission-1" response = sdk.rbac.create_permission(permission_name=permission_name) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC permission under the permission name of peaq-permission-1 with the permission id of 6e100511-7cc1-4365-b18e-d7b0f3ce.", "receipt": { "extrinsic_hash": "0x0755042c17a5cc1cebae3f1061913ac610723392600fe5c2f861670287eeea4d", "block_hash": "0x3cef644279f0835d209b83bb715c8d7d091cdfeffa5fb07ef74839909701c940", "finalized": false, "extrinsic_idx": 2, "call": { "call_module": "PeaqRbac", "call_function": "add_permission", "call_args": { "permission_id": "6e100511-7cc1-4365-b18e-d7b0f3ce", "name": "peaq-permission-1" } }, "events": [...], "total_fee_amount": 2510946098108087 } } ``` ## fetch\_permission(owner, permission\_id) Used to fetch a permission in the RBAC system by its permission ID. | Parameter | Type | EVM | Substrate | Description | | ------------------ | ----- | -------- | --------- | ------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner of the permission. | | **permission\_id** | `str` | Required | Required | ID of the permission to be fetched. | ### Fetch Permission Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" response = sdk.rbac.fetch_permission( owner=EVM_ADDRESS, permission_id=permission_id ) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "290506fd-b316-4a10-bd33-e8e2e047", "name": "peaq-permission-1", "enabled": true } ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" response = sdk.rbac.fetch_permission( owner=SUBSTRATE_ADDRESS, permission_id=permission_id ) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "6e100511-7cc1-4365-b18e-d7b0f3ce", "name": "peaq-permission-1", "enabled": true } ``` ## fetch\_permissions(owner) Used to fetch all permissions associated with the passed owner address. | Parameter | Type | EVM | Substrate | Description | | --------- | ----- | -------- | --------- | ----------------------------------------------------------------- | | **owner** | `str` | Required | Required | Address that represents the owner of all the fetched permissions. | ### Fetch Permissions Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) response = sdk.rbac.fetch_permissions(owner=EVM_ADDRESS) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "88dec9d0-354e-4ab6-9856-7a7eb45c", "name": "Permission-name-123", "enabled": true }, { "id": "ae7eda2f-a0fd-46db-bac3-1e19ba87", "name": "permission-name-123", "enabled": true } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) response = sdk.rbac.fetch_permissions(owner=SUBSTRATE_ADDRESS) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "77395752-8588-47c5-8bd5-0d2bf273", "name": "peaq-permission-new", "enabled": false }, { "id": "fc49ee05-f0ad-47ce-b336-62efdfec", "name": "peaq-permission-2", "enabled": true } ] ``` ## update\_permission(permission\_id, permission\_name) Updates the name of an existing permission. | Parameter | Type | EVM | Substrate | Description | | -------------------- | ----- | -------- | --------- | ----------------------------------------------- | | **permission\_id** | `str` | Required | Required | ID of the permission to update (32 characters). | | **permission\_name** | `str` | Required | Required | New name for the permission. | ### Update Permission Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_id = "307a081a-45e5-4ca3-b2b5-566ceb31" permission_name = "updated-permission-1" response = sdk.rbac.update_permission( permission_id=permission_id, permission_name=permission_name ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update permission call for permission 307a081a-45e5-4ca3-b2b5-566ceb31 with name updated-permission-1. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xb0c5b5ad33303761303831612d34356535..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" permission_name = "updated-permission-1" response = sdk.rbac.update_permission( permission_id=permission_id, permission_name=permission_name ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated permission 290506fd-b316-4a10-bd33-e8e2e047 with name updated-permission-1.", "receipt": { "transactionHash": "0x422c4891625ecfb872bb3dd5acaf5acd9af9e3ed6d769300c32e3e81f7f63d66", "transactionIndex": 0, "blockHash": "0x91b560fbe6d99757bb698dcb189419d2ab14a416600f70c2337052bb4f54b536", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847998, "cumulativeGasUsed": 34459, "gasUsed": 34459, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xf61e9daf2fc29e541fe654014f4239a5f1bd8f8e5bfbadf9211407e876a7a591" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641...", "blockHash": "0x91b560fbe6d99757bb698dcb189419d2ab14a416600f70c2337052bb4f54b536", "blockNumber": 5847998, "transactionHash": "0x422c4891625ecfb872bb3dd5acaf5acd9af9e3ed6d769300c32e3e81f7f63d66", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_id = "392a18cb-9de1-4f41-82fd-7ed1b1e8" permission_name = "updated-permission-1" response = sdk.rbac.update_permission( permission_id=permission_id, permission_name=permission_name ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update permission call for permission 392a18cb-9de1-4f41-82fd-7ed1b1e8 with name updated-permission-1. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "update_permission", "call_args": { "permission_id": "392a18cb-9de1-4f41-82fd-7ed1b1e8", "name": "updated-permission-1" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" permission_name = "updated-permission-1" response = sdk.rbac.update_permission( permission_id=permission_id, permission_name=permission_name ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated permission 6e100511-7cc1-4365-b18e-d7b0f3ce with name updated-permission-1.", "receipt": { "extrinsic_hash": "0x394bc58eb8dba5b8564dbe3c821b0506b06241eea701e2bf8cf746a612014670", "block_hash": "0x39c9e892ec55ac8f02fa0442c3023deaddee4fe8985df03425e556acce641bcf", "block_number": null, "finalized": false, "extrinsic_index": 2, "call": { "call_index": "0x670b", "call_function": "update_permission", "call_module": "PeaqRbac", "call_args": [ { "name": "permission_id", "type": "EntityId", "value": "0x36653130303531312d376363312d343336352d623138652d6437623066336365" }, { "name": "name", "type": "BoundedVec", "value": "updated-permission-1" } ], "call_hash": "0xb8275befb4fc4106a9afa27fde41cdbb8d73889e13e6adea2df45721724c7bf3" }, "events": [...], "is_success": true, "error_message": null, "weight": { "ref_time": 405299000, "proof_size": 3890 }, "total_fee_amount": 2139575238719276 } } ``` ## disable\_permission(permission\_id) Disables a permission within the RBAC system. A disabled permission cannot be used for new role assignments. | Parameter | Type | EVM | Substrate | Description | | ------------------ | ----- | -------- | --------- | ------------------------------------------------ | | **permission\_id** | `str` | Required | Required | ID of the permission to disable (32 characters). | ### Disable Permission Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_id = "307a081a-45e5-4ca3-b2b5-566ceb31" response = sdk.rbac.disable_permission(permission_id=permission_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable permission call for permission 307a081a-45e5-4ca3-b2b5-566ceb31. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x727e011e33303761303831612d343565352d..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" response = sdk.rbac.disable_permission(permission_id=permission_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled permission 290506fd-b316-4a10-bd33-e8e2e047.", "receipt": { "transactionHash": "0x69a7a8ee26c1ff14a848f4eeb11b7754347f729ebe61fff223b42eb773e7064d", "transactionIndex": 0, "blockHash": "0xae3e35fb404bac3f87ceb5a8de6a1b7f557c25f7a4cd7d9cc26eb3891567641a", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5848315, "cumulativeGasUsed": 33794, "gasUsed": 33794, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x8eea071fb9bda659c167469ca707aae4d94d31d2c948eda86c1d686b018e87f8" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a...", "blockHash": "0xae3e35fb404bac3f87ceb5a8de6a1b7f557c25f7a4cd7d9cc26eb3891567641a", "blockNumber": 5848315, "transactionHash": "0x69a7a8ee26c1ff14a848f4eeb11b7754347f729ebe61fff223b42eb773e7064d", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000001...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_id = "392a18cb-9de1-4f41-82fd-7ed1b1e8" response = sdk.rbac.disable_permission(permission_id=permission_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable permission call for permission 392a18cb-9de1-4f41-82fd-7ed1b1e8. You must sign and send externally.", "call": { "value": { "call_module": "PeaqRbac", "call_function": "disable_permission", "call_args": { "permission_id": "392a18cb-9de1-4f41-82fd-7ed1b1e8" } } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" response = sdk.rbac.disable_permission(permission_id=permission_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled permission 6e100511-7cc1-4365-b18e-d7b0f3ce.", "receipt": { "extrinsic_hash": "0x8f3759de5fb02d8a35d00d0d0199dd77b2ebb39fbbb12b71246ac01766ac772e", "block_hash": "0x0f961991f807a99d6cd58f4dd55e2a6682cf78ed07904a73f4f205c2988009d4", "finalized": false, "events": [...], "is_success": true, "total_fee_amount": 2138997212130607 } } ``` ## assign\_permission\_to\_role(permission\_id, role\_id) Assigns a permission to a role, granting the role the capabilities defined by the permission. | Parameter | Type | EVM | Substrate | Description | | ------------------ | ----- | -------- | --------- | ----------------------------------------------------------- | | **permission\_id** | `str` | Required | Required | ID of the permission to assign (32 characters). | | **role\_id** | `str` | Required | Required | ID of the role to assign the permission to (32 characters). | ### Assign Permission to Role Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_id = "307a081a-45e5-4ca3-b2b5-566ceb31" role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" response = sdk.rbac.assign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign permission to role call for permission 307a081a-45e5-4ca3-b2b5-566ceb31 and role 0bb60413-ebdd-44fb-ab2c-e7e0714f. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x404147f133303761303831612d343565352d3463613..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.assign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned permission 290506fd-b316-4a10-bd33-e8e2e047 to role afd2ae0e-b1db-42b9-bca1-93e9328b.", "receipt": { "transactionHash": "0x7ef5672c39ef7b46b513ee63612fdc0557bc089dbeba8766ff1d61e1ce74c573", "transactionIndex": 0, "blockHash": "0xf56a4cfbbc5fdab452cc32601c64e743495a0eb6736b1f5b8d5e9c178a0bb933", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5848033, "cumulativeGasUsed": 42101, "gasUsed": 42101, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xa02facfb294d21b16df2ffafbdd4e2052e6340881efccdbc403226b8cd2faabb" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646...", "blockHash": "0xf56a4cfbbc5fdab452cc32601c64e743495a0eb6736b1f5b8d5e9c178a0bb933", "blockNumber": 5848033, "transactionHash": "0x7ef5672c39ef7b46b513ee63612fdc0557bc089dbeba8766ff1d61e1ce74c573", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_id = "392a18cb-9de1-4f41-82fd-7ed1b1e8" role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" response = sdk.rbac.assign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign permission to role call for permission 392a18cb-9de1-4f41-82fd-7ed1b1e8 and role bc3a3e43-6f2e-4e55-83d3-72d5148a. You must sign and send externally.", "call": { "value": { "call_module": "PeaqRbac", "call_function": "assign_permission_to_role", "call_args": { "permission_id": "392a18cb-9de1-4f41-82fd-7ed1b1e8", "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a" } } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.assign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned permission 6e100511-7cc1-4365-b18e-d7b0f3ce to role 06856fab-c5ff-40cd-8e83-634264c6.", "receipt": { "extrinsic_hash": "0x6ed4828323df51efa9a7fd0f3ba2f1bbb30801bea1da497da96afcbddea0b2f5", "block_hash": "0x8a2d5c8336d8c2bd2787d4432aa7784f8716218234e83409278d32cb6ee4e4ef", "finalized": false, "extrinsic": { "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "nonce": 1902, "call": { "module": "PeaqRbac", "function": "assign_permission_to_role", "args": { "permission_id": "0x36653130303531312d376363312d343336352d623138652d6437623066336365", "role_id": "0x30363835366661622d633566662d343063642d386538332d3633343236346336" } } }, "events": [...], "success": true, "total_fee": 2412372923063492 } } ``` ## fetch\_role\_permissions(owner, role\_id) Fetches all permissions assigned to a role. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | -------------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner. | | **role\_id** | `str` | Required | Required | ID of the role to fetch permissions for (32 characters). | ### Fetch Role Permissions Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.fetch_role_permissions( owner=EVM_ADDRESS, role_id=role_id ) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "permission": "290506fd-b316-4a10-bd33-e8e2e047", "role": "afd2ae0e-b1db-42b9-bca1-93e9328b" } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.fetch_role_permissions( owner=SUBSTRATE_ADDRESS, role_id=role_id ) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "permission": "6e100511-7cc1-4365-b18e-d7b0f3ce", "role": "06856fab-c5ff-40cd-8e83-634264c6" } ] ``` ## unassign\_permission\_to\_role(permission\_id, role\_id) Removes a permission from a role, revoking the role's access to the permission's capabilities. | Parameter | Type | EVM | Substrate | Description | | ------------------ | ----- | -------- | --------- | --------------------------------------------------------------- | | **permission\_id** | `str` | Required | Required | ID of the permission to unassign (32 characters). | | **role\_id** | `str` | Required | Required | ID of the role to unassign the permission from (32 characters). | ### Unassign Permission from Role Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) permission_id = "307a081a-45e5-4ca3-b2b5-566ceb31" role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" response = sdk.rbac.unassign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign permission from role call for permission 307a081a-45e5-4ca3-b2b5-566ceb31 and role 0bb60413-ebdd-44fb-ab2c-e7e0714f. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xba427c9e33303761303831612d343..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.unassign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned permission 290506fd-b316-4a10-bd33-e8e2e047 from role afd2ae0e-b1db-42b9-bca1-93e9328b.", "receipt": { "transactionHash": "0xcfc2d4b8cf66199316189377dad8649e806359d8c68efc5edb90c03d32f5edf9", "transactionIndex": 0, "blockHash": "0x0cf3a17ddd238f091767129373d937da66362ed36fbca2b38f1de73139140bd4", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5848093, "cumulativeGasUsed": 39880, "gasUsed": 39880, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x0a5a672f8e6387c8c1f9aa25df37fd046981403d96ac60f5b4e27578486db14c" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a3...", "blockHash": "0x0cf3a17ddd238f091767129373d937da66362ed36fbca2b38f1de73139140bd4", "blockNumber": 5848093, "transactionHash": "0xcfc2d4b8cf66199316189377dad8649e806359d8c68efc5edb90c03d32f5edf9", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) permission_id = "392a18cb-9de1-4f41-82fd-7ed1b1e8" role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" response = sdk.rbac.unassign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign permission from role call for permission 392a18cb-9de1-4f41-82fd-7ed1b1e8 and role bc3a3e43-6f2e-4e55-83d3-72d5148a. You must sign and send externally.", "call": { "value": { "call_module": "PeaqRbac", "call_function": "unassign_permission_to_role", "call_args": { "permission_id": "392a18cb-9de1-4f41-82fd-7ed1b1e8", "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a" } } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.unassign_permission_to_role( permission_id=permission_id, role_id=role_id ) ``` ```python Substrate Signed Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned permission 6e100511-7cc1-4365-b18e-d7b0f3ce from role 06856fab-c5ff-40cd-8e83-634264c6.", "receipt": { "extrinsic_hash": "0xa03cf6d5e0e00f7205b4e445f1b1a2b03bbe4bd0ff3ee98755c654efb4b2896c", "block_hash": "0x2645b98668cdce99b4548724c2572ee04d7d1221634ffe50025db287902d8f3f", "finalized": false, "extrinsic_idx": 2, "extrinsic": { "extrinsic_hash": "0xa03cf6d5e0e00f7205b4e445f1b1a2b03bbe4bd0ff3ee98755c654efb4b2896c", "extrinsic_length": 170, "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "signature": { "Sr25519": "0xfad2c64fe6eca2560b491fd59b69ea2f58406109be303228abc8ae8bbecb9477f0b478178aa8df1626d5f3d4f976d1d11e614d6b3b38f12cc2650ca7f1881885" }, "era": "00", "nonce": 1903, "tip": 0, "mode": { "mode": "Disabled" }, "call": { "call_index": "0x670f", "call_function": "unassign_permission_to_role", "call_module": "PeaqRbac", "call_args": [ { "name": "permission_id", "type": "EntityId", "value": "0x36653130303531312d376363312d343336352d623138652d6437623066336365" }, { "name": "role_id", "type": "EntityId", "value": "0x30363835366661622d633566662d343063642d386538332d3633343236346336" } ], "call_hash": "0xbc4f82b157963f903724fca83e7a662752fec88b912c6582970b6efd530f9e9f" } }, "events": [...], "success": true, "total_fee": 2334479631311760 } } ``` ## fetch\_user\_permissions(owner, user\_id) Fetches all permissions assigned to a user (via roles). | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | -------------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner. | | **user\_id** | `str` | Required | Required | ID of the user to fetch permissions for (32 characters). | In order to fetch a user permission the following flow must take place: 1. Assign permission to role 2. Assign role to user 3. Fetch user permissions The following code shows this flow. ### Fetch User Permissions Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" sdk.rbac.assign_permission_to_role(permission_id=permission_id, role_id=role_id) user_id = "9e8c7866-8435-4b76-8683-709a03c9" sdk.rbac.assign_role_to_user(user_id=user_id, role_id=role_id) response = sdk.rbac.fetch_user_permissions( owner=EVM_ADDRESS, user_id=user_id ) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "763207f6-32ef-4236-969d-d1b81ac4", "name": "peaq-permission-2", "enabled": true }, { "id": "290506fd-b316-4a10-bd33-e8e2e047", "name": "updated-permission-1", "enabled": true } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" role_id = "06856fab-c5ff-40cd-8e83-634264c6" sdk.rbac.assign_permission_to_role(permission_id=permission_id, role_id=role_id) user_id = "9e8c7866-8435-4b76-8683-709a03c9" sdk.rbac.assign_role_to_user(user_id=user_id, role_id=role_id) response = sdk.rbac.fetch_user_permissions( owner=SUBSTRATE_ADDRESS, user_id=user_id ) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "6e100511-7cc1-4365-b18e-d7b0f3ce", "name": "updated-permission-1", "enabled": true }, { "id": "fc49ee05-f0ad-47ce-b336-62efdfec", "name": "peaq-permission-2", "enabled": true } ] ``` ## fetch\_group\_permissions(owner, group\_id) Fetches all permissions assigned to a group (via roles). | Parameter | Type | EVM | Substrate | Description | | ------------- | ----- | -------- | --------- | --------------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner. | | **group\_id** | `str` | Required | Required | ID of the group to fetch permissions for (32 characters). | In order to fetch a group permission the following flow must take place: 1. Assign permission to role 2. Assign role to group 3. Fetch group permissions The following code shows this flow. ### Fetch Group Permissions Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) permission_id = "290506fd-b316-4a10-bd33-e8e2e047" role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" sdk.rbac.assign_permission_to_role(permission_id=permission_id, role_id=role_id) group_id = "38c9ccb3-2346-4b25-ab28-51e1e2e3" sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) response = sdk.rbac.fetch_group_permissions( owner=EVM_ADDRESS, group_id=group_id ) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "290506fd-b316-4a10-bd33-e8e2e047", "name": "updated-permission-1", "enabled": true } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) permission_id = "6e100511-7cc1-4365-b18e-d7b0f3ce" role_id = "06856fab-c5ff-40cd-8e83-634264c6" sdk.rbac.assign_permission_to_role(permission_id=permission_id, role_id=role_id) group_id = "1801710e-b83b-4f1e-ba91-f7d53dd2" sdk.rbac.assign_role_to_group(group_id=group_id, role_id=role_id) response = sdk.rbac.fetch_group_permissions( owner=SUBSTRATE_ADDRESS, group_id=group_id ) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "6e100511-7cc1-4365-b18e-d7b0f3ce", "name": "updated-permission-1", "enabled": true } ] ``` # Role Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/rbac-operations/role Python SDK provides support for creating and managing roles in the peaq network's Role-Based Access Control (RBAC) system. ## create\_role(role\_name, role\_id) Creates a new role in the peaq network's Role-Based Access Control (RBAC) system. The role will be owned by the address derived from the seed (if provided) or must be signed externally. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------------------------------------------------------- | | **role\_name** | `string` | Required | Required | Name of the role to be created. | | **role\_id** | `string` | Optional | Optional | Unique identifier for the role (32 characters). If not provided, one will be auto-generated. | ### Create Role Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_name = "test_role_1" response = sdk.rbac.create_role(role_name=role_name) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create role call for the role name of test_role_1 and role id of 0bb60413-ebdd-44fb-ab2c-e7e0714f. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xbca838d630626236303431332d656264642d343466622d616232..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) role_name = "test_role_1" response = sdk.rbac.create_role(role_name=role_name) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC role under the role name of test_role_1 with the role id of afd2ae0e-b1db-42b9-bca1-93e9328b.", "receipt": { "transactionHash": "0x1c60eee57692882cbd3dfae35305fb07eae6d567eb4a788bcdeda67d18ff63fb", "transactionIndex": 0, "blockHash": "0xd5cd4c0c29dd7f007dc5fe60b533faaabc4c74d1d8945aaee17a0a3e88911e47", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5846791, "cumulativeGasUsed": 44939, "gasUsed": 44939, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x298ec29f9de7335421cd619739f36aa75b4b6d617ac5c411d41ebbab529593b5" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a...", "blockHash": "0xd5cd4c0c29dd7f007dc5fe60b533faaabc4c74d1d8945aaee17a0a3e88911e47", "blockNumber": 5846791, "transactionHash": "0x1c60eee57692882cbd3dfae35305fb07eae6d567eb4a788bcdeda67d18ff63fb", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_name = "test_role_1" response = sdk.rbac.create_role(role_name=role_name) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC create role call for the role name of test_role_1 and role id of bc3a3e43-6f2e-4e55-83d3-72d5148a. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "add_role", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "name": "test_role_1" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) role_name = "test_role_1" response = sdk.rbac.create_role(role_name=role_name) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the RBAC role under the role name of test_role_1 with the role id of 06856fab-c5ff-40cd-8e83-634264c6.", "receipt": { "extrinsic_hash": "0x491b16e0bb66505a93b84bdaf7b9114dbde62b4e6e56ffb90f6a005e4aea2393", "block_hash": "0x16ef5978e10c9b9b7a7aad676cf6db8531f44175ed72af43bae1384b4e5b9b0d", "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "call": { "module": "PeaqRbac", "function": "add_role", "args": { "role_id": "06856fab-c5ff-40cd-8e83-634264c6", "name": "test_role_1" } }, "events": [...], "total_fee_amount": 2510932068336512, "status": "success" } } ``` ## fetch\_role(owner, role\_id) Fetches a specific role by its ID and owner address from the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------ | -------- | -------- | --------- | ------------------------------------------------------- | | **owner** | `string` | Required | Required | Address representing the owner of the role. | | **role\_id** | `string` | Required | Required | Unique identifier of the role to fetch (32 characters). | ### Fetch Role Code Examples ```python EVM Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.fetch_role(owner=EVM_ADDRESS, role_id=role_id) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "afd2ae0e-b1db-42b9-bca1-93e9328b", "name": "test_role_1", "enabled": true } ``` ```python Substrate Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.fetch_role(owner=SUBSTRATE_ADDRESS, role_id=role_id) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "id": "06856fab-c5ff-40cd-8e83-634264c6", "name": "test_role_1", "enabled": true } ``` ## fetch\_roles(owner) Fetches all roles associated with the specified owner address from the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | --------- | -------- | -------- | --------- | ----------------------------------------------------------- | | **owner** | `string` | Required | Required | Address that represents the owner of all the fetched roles. | ### Fetch Roles Code Examples ```python EVM Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) response = sdk.rbac.fetch_roles(owner=EVM_ADDRESS) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "b68a5589-1284-49e9-8276-0359a429", "name": "test_role_1", "enabled": true }, { "id": "e09fe342-f8ee-46d1-82e2-a60a5b6e", "name": "admin_role", "enabled": true } ] ``` ```python Substrate Fetch theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) response = sdk.rbac.fetch_roles(owner=SUBSTRATE_ADDRESS) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "id": "bc3f20d5-c519-4048-8db1-4bbf48dc", "name": "test_role_1", "enabled": true }, { "id": "bb3d5a05-3a4a-452b-a96d-372832ff", "name": "admin_role", "enabled": true } ] ``` ## update\_role(role\_id, role\_name) Updates the name of an existing role in the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | -------------- | ----- | -------- | --------- | -------------------------------------------------------- | | **role\_id** | `str` | Required | Required | Unique identifier of the role to update (32 characters). | | **role\_name** | `str` | Required | Required | New name to assign to the role. | ### Update Role Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" new_role_name = "updated-peaq-role" response = sdk.rbac.update_role( role_id=role_id, role_name=new_role_name ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update role call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f with name updated-peaq-role. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x7cc0a218306262363034313..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" new_role_name = "updated-peaq-role" response = sdk.rbac.update_role( role_id=role_id, role_name=new_role_name ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated role afd2ae0e-b1db-42b9-bca1-93e9328b with name updated-peaq-role.", "receipt": { "transactionHash": "0x2e37e5f390d23b0b2d1e3b7360c6a85e6120ece97d5f5c8f4bc0f66b749d9f5d", "transactionIndex": 0, "blockHash": "0x5cd3a0cee6a138d3c37bba32719f622b68681ca778a539afe4d9c2e36dcbf15a", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5846913, "cumulativeGasUsed": 34419, "gasUsed": 34419, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0xc43fa962e2791d84b2f4ad1079b7020844acab2410952d61f00c544d837bc47f" ], "data": "0x0000000000000000000000009eeab1ac...", "blockHash": "0x5cd3a0cee6a138d3c37bba32719f622b68681ca778a539afe4d9c2e36dcbf15a", "blockNumber": 5846913, "transactionHash": "0x2e37e5f390d23b0b2d1e3b7360c6a85e6120ece97d5f5c8f4bc0f66b749d9f5d", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" new_role_name = "updated-peaq-role" response = sdk.rbac.update_role( role_id=role_id, role_name=new_role_name ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC update role call for role bc3a3e43-6f2e-4e55-83d3-72d5148a with name updated-peaq-role. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "update_role", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "name": "updated-peaq-role" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" new_role_name = "updated-peaq-role" response = sdk.rbac.update_role( role_id=role_id, role_name=new_role_name ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated role 06856fab-c5ff-40cd-8e83-634264c6 with name updated-peaq-role.", "receipt": { "extrinsic_hash": "0xb92e6221fda595175739bc9a5882edfd0dd23193e24636a2037a5d69018b3c3e", "block_hash": "0xa22f5ac2f68f587f82d3b0b6b982708e85dbcb9fce874959effe02bfc2d49d33", "extrinsic_index": 3, "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "call": { "call_index": "0x6703", "call_function": "update_role", "call_module": "PeaqRbac", "call_args": [ { "name": "role_id", "type": "EntityId", "value": "0x30363835366661622d633566662d343063642d386538332d3633343236346336" }, { "name": "name", "type": "BoundedVec", "value": "updated-peaq-role" } ], "call_hash": "0x05f9c3dc3912c2cd4315e549a3f83016ba29d05361bd73df541606064492c7e6" }, "events": [...], "total_fee": 2139430732072111 } } ``` ## disable\_role(role\_id) Disables a role in the peaq network's RBAC system, making it inactive while preserving its data. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | --------------------------------------------------------- | | **role\_id** | `str` | Required | Required | Unique identifier of the role to disable (32 characters). | ### Disable Role Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" response = sdk.rbac.disable_role(role_id=role_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable role call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x7c4a13b130626236303431332d656264642..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" response = sdk.rbac.disable_role(role_id=role_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled role afd2ae0e-b1db-42b9-bca1-93e9328b.", "receipt": { "transactionHash": "0x5de14837069c6b4992b0d7da99de6c7a0759e402cf4be2768bfa28a126be9e0e", "transactionIndex": 0, "blockHash": "0x5f1460c07e1a1dd4951fe258461706f5402b15c6b42fc68c2ba3e2f8d2f3c034", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5848370, "cumulativeGasUsed": 33783, "gasUsed": 33783, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x47808f9766351846ab3f47ddfd0cfc00fd1a4831674f2c872826932b0184d0be" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c61666432616530652d62316...", "blockHash": "0x5f1460c07e1a1dd4951fe258461706f5402b15c6b42fc68c2ba3e2f8d2f3c034", "blockNumber": 5848370, "transactionHash": "0x5de14837069c6b4992b0d7da99de6c7a0759e402cf4be2768bfa28a126be9e0e", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" response = sdk.rbac.disable_role(role_id=role_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC disable role call for role bc3a3e43-6f2e-4e55-83d3-72d5148a. You must sign and send externally.", "call": { "value": { "call_module": "PeaqRbac", "call_function": "disable_role", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a" } } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" response = sdk.rbac.disable_role(role_id=role_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully disabled role 06856fab-c5ff-40cd-8e83-634264c6.", "receipt": { "extrinsic_hash": "0x2f1dbdc1c1cdd054a0f62b091795098b510894b328605bd37508cdeb7deab84e", "block_hash": "0x347ea0edb1da99e6323cc0e2ec1424af9291d4a9a4d08b88eafadb31bf380b78", "finalized": false, "events": [...], "total_fee_amount": 2138608587458143 } } ``` ## assign\_role\_to\_user(role\_id, user\_id) Assigns a specific role to a user in the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | -------------------------------------------------------------------- | | **role\_id** | `str` | Required | Required | Unique identifier of the role to assign (32 characters). | | **user\_id** | `str` | Required | Required | Unique identifier of the user to assign the role to (32 characters). | ### Assign Role to User Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.assign_role_to_user(role_id=role_id, user_id=user_id) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign role to user call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f and user 9e8c7866-8435-4b76-8683-709a03c9. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0xae8cdd7930626236303..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.assign_role_to_user(role_id=role_id, user_id=user_id) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned role afd2ae0e-b1db-42b9-bca1-93e9328b to user 9e8c7866-8435-4b76-8683-709a03c9.", "receipt": { "transactionHash": "0xeaabe5ac83c9e7f3701e21434c1c75c4740e5f1beb9f2315536732888acea8c9", "transactionIndex": 1, "blockHash": "0xcc52548f9cdc19bbe866b98d6a2d759713a9323fb66892377dbcd3a9025fe8ec", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5846990, "cumulativeGasUsed": 297661, "gasUsed": 40890, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x484758882cb51a9d0452f6ef468ec5c3baaf0363de4265321ccc1bb959e7a445" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646...", "blockHash": "0xcc52548f9cdc19bbe866b98d6a2d759713a9323fb66892377dbcd3a9025fe8ec", "blockNumber": 5846990, "transactionHash": "0xeaabe5ac83c9e7f3701e21434c1c75c4740e5f1beb9f2315536732888acea8c9", "transactionIndex": 1, "logIndex": 4, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.assign_role_to_user(role_id=role_id, user_id=user_id) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC assign role to user call for role bc3a3e43-6f2e-4e55-83d3-72d5148a and user 9e8c7866-8435-4b76-8683-709a03c9. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "assign_role_to_user", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "user_id": "9e8c7866-8435-4b76-8683-709a03c9" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.assign_role_to_user(role_id=role_id, user_id=user_id) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully assigned role 06856fab-c5ff-40cd-8e83-634264c6 to user 9e8c7866-8435-4b76-8683-709a03c9.", "receipt": { "extrinsic_hash": "0x8e7f2aeb8e3691d788f003df50f7515002fb5408d4ff0bbb2d60b69dfa1adba6", "block_hash": "0xd92005c22c82221ef4c98047724cab91c537ecb08ddc7e0a21626a47f6753922", "finalized": false, "extrinsic_index": 2, "call": { "call_module": "PeaqRbac", "call_function": "assign_role_to_user", "call_args": { "role_id": "06856fab-c5ff-40cd-8e83-634264c6", "user_id": "9e8c7866-8435-4b76-8683-709a03c9" } }, "events": [...], "total_fee": "2369909013455249" } } ``` ## fetch\_user\_roles(owner, user\_id) Fetches all roles assigned to a specific user in the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | ----------------------------------------------------------------- | | **owner** | `str` | Required | Required | Address representing the owner of the roles. | | **user\_id** | `str` | Required | Required | Unique identifier of the user to fetch roles for (32 characters). | ### Fetch User Roles Code Examples ```python EVM Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.fetch_user_roles(owner=EVM_ADDRESS, user_id=user_id) ``` ```python EVM Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "role": "65503c15-6cd7-4320-b326-3f5c7d2b", "user": "9e8c7866-8435-4b76-8683-709a03c9" }, { "role": "afd2ae0e-b1db-42b9-bca1-93e9328b", "user": "9e8c7866-8435-4b76-8683-709a03c9" } ] ``` ```python Substrate Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.fetch_user_roles(owner=SUBSTRATE_ADDRESS, user_id=user_id) ``` ```python Substrate Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} [ { "role": "06856fab-c5ff-40cd-8e83-634264c6", "user": "9e8c7866-8435-4b76-8683-709a03c9" }, { "role": "7128759c-f869-4818-8b1d-98ce4b12", "user": "9e8c7866-8435-4b76-8683-709a03c9" } ] ``` ## unassign\_role\_to\_user(role\_id, user\_id) Removes a role assignment from a user in the peaq network's RBAC system. | Parameter | Type | EVM | Substrate | Description | | ------------ | ----- | -------- | --------- | ---------------------------------------------------------------------- | | **role\_id** | `str` | Required | Required | Unique identifier of the role to unassign (32 characters). | | **user\_id** | `str` | Required | Required | Unique identifier of the user to remove the role from (32 characters). | ### Unassign Role from User Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) role_id = "0bb60413-ebdd-44fb-ab2c-e7e0714f" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.unassign_role_to_user( role_id=role_id, user_id=user_id ) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign role from user call for role 0bb60413-ebdd-44fb-ab2c-e7e0714f and user 9e8c7866-8435-4b76-8683-709a03c9. You must sign and send externally.", "tx": { "to": "0x0000000000000000000000000000000000000802", "data": "0x7663575030626236303431332d65626..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) role_id = "afd2ae0e-b1db-42b9-bca1-93e9328b" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.unassign_role_to_user( role_id=role_id, user_id=user_id ) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned role afd2ae0e-b1db-42b9-bca1-93e9328b from user 9e8c7866-8435-4b76-8683-709a03c9.", "receipt": { "transactionHash": "0x8497af05650616bd105cf4eb88247b469f2c62c9f393dd7caaba93017c6136a1", "transactionIndex": 0, "blockHash": "0x0de592d66c9292cf8d4b5fcabcb618cacb33371f002acaeb4c0b1fb340f40eeb", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000802", "blockNumber": 5847073, "cumulativeGasUsed": 39869, "gasUsed": 39869, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000802", "topics": [ "0x0ff1a305be9c0b5a533bc46d137628af9c53f1941825d0bfe94c23c1d046dc6b" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c616664326165...", "blockHash": "0x0de592d66c9292cf8d4b5fcabcb618cacb33371f002acaeb4c0b1fb340f40eeb", "blockNumber": 5847073, "transactionHash": "0x8497af05650616bd105cf4eb88247b469f2c62c9f393dd7caaba93017c6136a1", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000000000000100000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) role_id = "bc3a3e43-6f2e-4e55-83d3-72d5148a" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.unassign_role_to_user( role_id=role_id, user_id=user_id ) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed RBAC unassign role from user call for role bc3a3e43-6f2e-4e55-83d3-72d5148a and user 9e8c7866-8435-4b76-8683-709a03c9. You must sign and send externally.", "call": { "call_module": "PeaqRbac", "call_function": "unassign_role_to_user", "call_args": { "role_id": "bc3a3e43-6f2e-4e55-83d3-72d5148a", "user_id": "9e8c7866-8435-4b76-8683-709a03c9" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) role_id = "06856fab-c5ff-40cd-8e83-634264c6" user_id = "9e8c7866-8435-4b76-8683-709a03c9" response = sdk.rbac.unassign_role_to_user( role_id=role_id, user_id=user_id ) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully unassigned role 06856fab-c5ff-40cd-8e83-634264c6 from user 9e8c7866-8435-4b76-8683-709a03c9.", "receipt": { "extrinsic_hash": "0xd1a7972b2425b07044e16265894246540b7ec90a5a5f398a71279dd5feae23a6", "block_hash": "0xae7f7e1dfe1288aa40f2cfa328d7e6e600bee95a51cb448aea9caeeeefd26396", "finalized": false, "events": [...], "is_success": true, "total_fee_amount": 2334112051296649 } } ``` # Storage Operations Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/storage-operations The peaq network allows you to store on-chain data using a simple **`key: value`** structure. ## add\_item(item\_type, item) Adds a new key-value pair to the on-chain storage. If the item is not already a string, it will be automatically JSON-serialized before storage. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ----------------------------------------------------------------------------- | | **item\_type** | `string` | Required | Required | Key used to categorize or identify the item. Max 64 bytes. | | **item** | `object` | Required | Required | Value to store. Automatically JSON-serialized if not a string. Max 256 bytes. | ### Add Item Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) item_type='my_key' item='my_value' response = sdk.storage.add_item(item_type=item_type, item=item) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed add_item tx object for peaq storage with item type my_key and item my_value. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000801", "data": "0x257c3c0300000000000000000..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) item_type='my_key' item='my_value' response = sdk.storage.add_item(item_type=item_type, item=item) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the storage item type my_key with item my_value for the address 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0x48e40a9e450c24f5d7abd055e368c1c8a80748737f4d7e8e1c29e38a160f0cea", "transactionIndex": 0, "blockHash": "0xa696c8c3620aa5cad713faa9745fb0b105d37c555e1365881bdf3ffabcd502d9", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000801", "blockNumber": 5846526, "cumulativeGasUsed": 43014, "gasUsed": 43014, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000801", "topics": [ "0x6fbdb10fd11f8487a821a58c35e587dcc19748004073777f2a8d87bc5c25535e" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f...", "blockHash": "0xa696c8c3620aa5cad713faa9745fb0b105d37c555e1365881bdf3ffabcd502d9", "blockNumber": 5846526, "transactionHash": "0x48e40a9e450c24f5d7abd055e368c1c8a80748737f4d7e8e1c29e38a160f0cea", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x00000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) item_type='my_key' item='my_value' response = sdk.storage.add_item(item_type=item_type, item=item) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed add_item call object for peaq storage with item type my_key and item my_value. You must sign and send it externally.", "call": { "call_module": "PeaqStorage", "call_function": "add_item", "call_args": { "item_type": "my_key", "item": "my_value" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) item_type='my_key' item='my_value' response = sdk.storage.add_item(item_type=item_type, item=item) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully added the storage item type my_key with item my_value for the address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0xc6f0a03731031b93b8da6fe45548f5373e4f36edbe0fad95934077e5eb1d504c", "block_hash": "0xf25b847bc9e8c2f4b36ef91c9ab8798a3aa2195c48fa1597f7a9156391620ac7", "finalized": false, "call": { "module": "PeaqStorage", "function": "add_item", "args": { "item_type": "my_key", "item": "my_value" } }, "events": [...] } } ``` ## get\_item(item\_type, address) Retrieves a stored item by its key for the specified address. Returns the decoded value as a string. **Cross-chain storage querying:** * **Substrate**: Uses the existing Substrate API connection directly to query storage. * **EVM**: Automatically uses the SDK's `base_url` to create a temporary Substrate connection. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | ---------- | ---------- | ------------------------------------------------ | | **item\_type** | `string` | Required | Required | Key used to search for the stored item. | | **address** | `string` | Required\* | Required\* | Address of the wallet that owns the stored item. | \* The address is required when no seed is set. ### Get Item Code Examples ```python EVM No Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) item_type='my_key' response = sdk.storage.get_item(item_type=item_type, address=EVM_ADDRESS) ``` ```python EVM No Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'my_key': 'my_value'} ``` ```python EVM Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) item_type='my_key' response = sdk.storage.get_item(item_type=item_type) ``` ```python EVM Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'my_key': 'my_value'} ``` ```python Substrate No Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) item_type='my_key' response = sdk.storage.get_item(item_type=item_type, address=SUBSTRATE_ADDRESS) ``` ```python Substrate No Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'my_key': 'my_value'} ``` ```python Substrate Key Read theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) item_type='my_key' response = sdk.storage.get_item(item_type=item_type) ``` ```python Substrate Key Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'my_key': 'my_value'} ``` ## update\_item(item\_type, item) Updates an existing item in on-chain storage by replacing its value with a new one. The item will be automatically JSON-serialized if it's not already a string. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | ------------------------------------------------------------------ | | **item\_type** | `string` | Required | Required | Key of the existing item to update. | | **item** | `object` | Required | Required | New value to store. Automatically JSON-serialized if not a string. | ### Update Item Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) item_type='my_key' item='my_new_value' response = sdk.storage.update_item(item_type=item_type, item=item) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed update_item tx object for peaq storage with item type my_key and item my_new_value. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000801", "data": "0x1cd4bf09000000000..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) item_type='my_key' item='my_new_value' response = sdk.storage.update_item(item_type=item_type, item=item) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated the storage item type my_key with item my_new_value for the address 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xf477fa1b5255fd623b5a25757d42cd82ede5b287e20fe170d407d85f9a0ea412", "transactionIndex": 0, "blockHash": "0x33bd10eead5f94b2dd330625c93b17038b8a35c32dda5ef9f730bfd01bd39504", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000801", "blockNumber": 5846617, "cumulativeGasUsed": 32060, "gasUsed": 32060, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000801", "topics": [ "0x73cf4bb2cd2563692444e276f8a1db29257d4f8caabbd3443cf369e461df95ba" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b...", "blockHash": "0x33bd10eead5f94b2dd330625c93b17038b8a35c32dda5ef9f730bfd01bd39504", "blockNumber": 5846617, "transactionHash": "0xf477fa1b5255fd623b5a25757d42cd82ede5b287e20fe170d407d85f9a0ea412", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) item_type='my_key' item='my_new_value' response = sdk.storage.update_item(item_type=item_type, item=item) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed update_item call object for peaq storage with item type my_key and item my_new_value. You must sign and send it externally.", "call": { "call_module": "PeaqStorage", "call_function": "update_item", "call_args": { "item_type": "my_key", "item": "my_new_value" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) item_type='my_key' item='my_new_value' response = sdk.storage.update_item(item_type=item_type, item=item) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully updated the storage item type my_key with item my_new_value for the address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0xd6a2930a6de697cc0c5d31b41fd53bd1e7e80aa95ac4f2feb59539a7ef02310c", "block_hash": "0x61394cf43727841ed98c5487a2c46101e7b432bd830dc76f7c40b5bd1dcc20c0", "finalized": false, "extrinsic_index": 2, "call": { "module": "PeaqStorage", "function": "update_item", "args": { "item_type": "my_key", "item": "my_new_value" } }, "events": [...], "is_success": true, "total_fee": 1954699326844373 } } ``` ## remove\_item(item\_type) Removes an existing key-value pair from on-chain storage. | Parameter | Type | EVM | Substrate | Description | | -------------- | -------- | -------- | --------- | -------------------------------------------- | | **item\_type** | `string` | Required | Required | The key representing the pair to be removed. | ### Remove Item Code Examples ```python EVM Unsigned Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM ) item_type='my_key' response = sdk.storage.remove_item(item_type=item_type) ``` ```python EVM Unsigned Tx Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed remove_item tx object for peaq storage with item type my_key. You must sign and send it externally.", "tx": { "to": "0x0000000000000000000000000000000000000801", "data": "0x2c2decc1000000000000000000000000000000000..." } } ``` ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) item_type='my_key' response = sdk.storage.remove_item(item_type=item_type) ``` ```python EVM Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully removed the storage item type my_key for the address 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xe5c923db4b45567c6673bfc33e87ff24676bac44656e869c06aea507e49dace0", "transactionIndex": 0, "blockHash": "0x2abc1e8dad4ad39689a4a338a2d8a60ea71e541ddcec39872f27cedc8f221816", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000801", "blockNumber": 5846657, "cumulativeGasUsed": 42572, "gasUsed": 42572, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000801", "topics": [ "0x9a7468a61900ddc322ec242b1778095d326b907aca7e792ae8a9b862903a0fa8" ], "data": "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c00000000000000000000000000000000000...", "blockHash": "0x2abc1e8dad4ad39689a4a338a2d8a60ea71e541ddcec39872f27cedc8f221816", "blockNumber": 5846657, "transactionHash": "0xe5c923db4b45567c6673bfc33e87ff24676bac44656e869c06aea507e49dace0", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate Unsigned Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE ) item_type='my_key' response = sdk.storage.remove_item(item_type=item_type) ``` ```python Substrate Unsigned Call Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Constructed remove_item call object for peaq storage with item type my_key. You must sign and send it externally.", "call": { "call_module": "PeaqStorage", "call_function": "remove_item", "call_args": { "item_type": "my_key" } } } ``` ```python Substrate Write Call theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) item_type='my_key' response = sdk.storage.remove_item(item_type=item_type) ``` ```python Substrate Write Response theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Successfully removed the storage item type my_key for the address 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg.", "receipt": { "extrinsic_hash": "0x03dff995ad423247b1096c63fe0fdcc16940f2b6eba2b602ac43ae5153026680", "block_hash": "0x6140c316ca255d036cfb607bfb2707da6ae4e45bcb0bfd2e58745859194235c4", "finalized": false, "extrinsic_index": 2, "address": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "call": { "module": "PeaqStorage", "function": "remove_item", "args": { "item_type": "my_key" } }, "events": [...], "success": true, "total_fee": "2342748778674540" } } ``` # Transfer Operations Source: https://docs.peaq.xyz/peaqchain/sdk-reference/python/transfer The peaq network provides methods to transfer tokens across supported chains (peaq and agung). This includes native token transfers, ERC-20 token transfers, and ERC-721 (NFT) transfers. ## native(to, amount) Transfers the native token from the signer to a target address. | Parameter | Type | EVM | Substrate | Description | | ---------- | -------------------------------- | -------- | --------- | ------------------------------------------------ | | **to** | `str` | Required | Required | The recipient address (either SS58 or EVM H160). | | **amount** | `int \| float \| str \| Decimal` | Required | Required | Human-readable token amount (e.g., 1.5). | ### Native Token Transfer Code Examples ```python EVM to EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) amount = 1.5 response = sdk.transfer.native(to=EVM_ADDRESS, amount=amount) ``` ```python EVM to EVM receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Sent 1.5 native-token from 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C to 0x3e3FF16083Bf0a444B8fF86C7156eB3368e3cefB.", "receipt": { "transactionHash": "0xeb349f98c31e414b76a674ed7b435005e9e717bc6459156d736c66999bec5728", "transactionIndex": 0, "blockHash": "0xa28e6d76a5b2aea0c24c9f4e39c548c1ddcaeebcffdf6273a4e07b3db36730c0", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x3e3FF16083Bf0a444B8fF86C7156eB3368e3cefB", "blockNumber": 5848490, "cumulativeGasUsed": 21000, "gasUsed": 21000, "contractAddress": null, "logs": [], "logsBloom": "0x000000000000000000000000000000000000000000000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python EVM to Substrate theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) amount = 1.5 response = sdk.transfer.native(to=SUBSTRATE_ADDRESS, amount=amount) ``` ```python EVM to Substrate receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Sent 1.5 native-token from 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C to 5FEw7aWmqcnWDaMcwjKyGtJMjQfqYGxXmDWKVfcpnEPmUM7q.", "receipt": { "transactionHash": "0x006077a3dbc93d2ebaa3ed20e4764ba41de122edd2eaa338f4e593750dcd6d6f", "transactionIndex": 0, "blockHash": "0xb64e5c258440264ff9f354700a56e96102fddedfcabd937f20081ad40843965e", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x0000000000000000000000000000000000000809", "blockNumber": 5848515, "cumulativeGasUsed": 30423, "gasUsed": 30423, "contractAddress": null, "logs": [ { "address": "0x0000000000000000000000000000000000000809", "topics": [ "0xa1f949b36d49ecc12f7df90e9e1c2dfc2091941dbc70b29ad1b995253a47853b", "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c", "0x8c81c81b633fae0cc5713dd9d7ac2ae9fe5d63112b396554c0d8e19b199daf1d" ], "data": "0x00000000000000000000000000000000000000000000000014d1120d7b160000", "blockHash": "0xb64e5c258440264ff9f354700a56e96102fddedfcabd937f20081ad40843965e", "blockNumber": 5848515, "transactionHash": "0x006077a3dbc93d2ebaa3ed20e4764ba41de122edd2eaa338f4e593750dcd6d6f", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x000000000000000000000000080000000080000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` ```python Substrate to EVM theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") EVM_ADDRESS = os.getenv("EVM_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) amount = 1.5 response = sdk.transfer.native(to=EVM_ADDRESS, amount=amount) ``` ```python Substrate to EVM receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Sent 1.5 native-token from 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg to 0x3e3FF16083Bf0a444B8fF86C7156eB3368e3cefB.", "receipt": { "extrinsic_hash": "0x18097b6e184090630d0a136c477a37b80460b8cf9e79074289e2e00b20549e07", "block_hash": "0x8616cb83c69ff2795b4cea5eab7cd3ed5dcd3c634f6dbdc3f373ecff9a96e8fc", "from": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "to": "5Gt6N8WZRfWDuULHrQyVd1qbcHiuVjMTq5MyKhDss7s2mCUF", "amount": 1500000000000000000, "fee": 1972642001704138, "events": [ ... { "event": "Transfer", "from": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "to": "5Gt6N8WZRfWDuULHrQyVd1qbcHiuVjMTq5MyKhDss7s2mCUF", "amount": 1500000000000000000 }, ... ], "success": true } } ``` ```python Substrate to Substrate theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() WSS_PEAQ_URL = os.getenv("WSS_PEAQ_URL") SUBSTRATE_SEED = os.getenv("SUBSTRATE_SEED") SUBSTRATE_ADDRESS = os.getenv("SUBSTRATE_ADDRESS") sdk = Sdk.create_instance( base_url=WSS_PEAQ_URL, chain_type=ChainType.SUBSTRATE, seed=SUBSTRATE_SEED ) amount = 1.5 response = sdk.transfer.native(to=SUBSTRATE_ADDRESS, amount=amount) ``` ```python Substrate to Substrate receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Sent 1.5 native-token from 5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg to 5FEw7aWmqcnWDaMcwjKyGtJMjQfqYGxXmDWKVfcpnEPmUM7q.", "receipt": { "extrinsic_hash": "0xbe07335455ba0dfb33812499a42e8b9c16df65932281ed162696559c5de73494", "block_hash": "0x4a015f85f4a858f89977c981b0b165af0cfa110534a6f89a041843ab6946733d", "from": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "to": "5FEw7aWmqcnWDaMcwjKyGtJMjQfqYGxXmDWKVfcpnEPmUM7q", "amount": 1500000000000000000, "fee": 1972642001704138, "events": [ ... { "event": "Transfer", "from": "5Df42mkztLtkksgQuLy4YV6hmhzdjYvDknoxHv1QBkaY12Pg", "to": "5FEw7aWmqcnWDaMcwjKyGtJMjQfqYGxXmDWKVfcpnEPmUM7q", "amount": 1500000000000000000 }, ... ], "success": true } } ``` ## erc20(erc\_20\_address, recipient\_address, amount, token\_decimals) Transfers ERC-20 tokens from the signer to a recipient address. | Parameter | Type | EVM | Substrate | Description | | ---------------------- | -------------------------------- | -------- | --------- | -------------------------------------------------- | | **erc\_20\_address** | `str` | Required | N/A | The address of the ERC-20 contract. | | **recipient\_address** | `str` | Required | N/A | The recipient's address. | | **amount** | `int \| float \| str \| Decimal` | Required | N/A | Human-readable token amount. | | **token\_decimals** | `int \| float \| str \| Decimal` | Optional | N/A | Number of decimals for the token (defaults to 18). | ### ERC-20 Transfer Code Examples ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) ERC_20_ADDRESS = os.getenv("ERC_20_ADDRESS") EVM_ADDRESS = os.getenv("EVM_ADDRESS") amount = 1.5 token_decimals = 18 response = sdk.transfer.erc20( erc_20_address=ERC_20_ADDRESS, recipient_address=EVM_ADDRESS, amount=amount, token_decimals=token_decimals ) ``` ```python EVM Write Tx Receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Transferred 1.5 of the erc-20 at address 0xFF78784737e8e124cb38603eD3DA78f071f50c2a to the new owner of 0x3e3FF16083Bf0a444B8fF86C7156eB3368e3cefB from the owner 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xc1b8bd9a02a59bcd21aa71200c0e4c7c38117636262cdeae462bd029e24538fc", "transactionIndex": 0, "blockHash": "0xef507bcb8f5fb76fb28d24573597d86618d0d5e4965f3d352bd48fd21c2178ce", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0xFF78784737e8e124cb38603eD3DA78f071f50c2a", "blockNumber": 5848651, "cumulativeGasUsed": 34492, "gasUsed": 34492, "status": 1, "effectiveGasPrice": 102000000000, "type": 2, "logs": [ { "address": "0xFF78784737e8e124cb38603eD3DA78f071f50c2a", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c", "0x0000000000000000000000003e3ff16083bf0a444b8ff86c7156eb3368e3cefb" ], "data": "0x00000000000000000000000000000000000000000000000014d1120d7b160000", "blockHash": "0xef507bcb8f5fb76fb28d24573597d86618d0d5e4965f3d352bd48fd21c2178ce", "blockNumber": 5848651, "transactionHash": "0xc1b8bd9a02a59bcd21aa71200c0e4c7c38117636262cdeae462bd029e24538fc", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ] } } ``` ## erc721(erc\_721\_address, recipient\_address, token\_id) Transfers an ERC-721 token (NFT) from the signer to a recipient address using the `safeTransferFrom` method. | Parameter | Type | EVM | Substrate | Description | | ---------------------- | ----- | -------- | --------- | ------------------------------------ | | **erc\_721\_address** | `str` | Required | N/A | The address of the ERC-721 contract. | | **recipient\_address** | `str` | Required | N/A | The recipient's address. | | **token\_id** | `int` | Required | N/A | The ID of the token to transfer. | ### ERC-721 Transfer Code Examples ```python EVM Write Tx theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_sdk import Sdk from peaq_sdk.types import ChainType load_dotenv() HTTPS_PEAQ_URL = os.getenv("HTTPS_PEAQ_URL") EVM_PRIVATE_KEY = os.getenv("EVM_PRIVATE_KEY") sdk = Sdk.create_instance( base_url=HTTPS_PEAQ_URL, chain_type=ChainType.EVM, seed=EVM_PRIVATE_KEY ) ERC_721_ADDRESS = os.getenv("ERC_721_ADDRESS") EVM_ADDRESS = os.getenv("EVM_ADDRESS") TOKEN_ID = 1 response = sdk.transfer.erc721( erc_721_address=ERC_721_ADDRESS, recipient_address=EVM_ADDRESS, token_id=TOKEN_ID ) ``` ```python EVM Write Tx Receipt theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "message": "Transferred NFT at address 0x3319176b32a4c1e68c02ef454c148ee9b5363c33 to the new owner of 0x3e3FF16083Bf0a444B8fF86C7156eB3368e3cefB from the owner 0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C.", "receipt": { "transactionHash": "0xd483ec3f28326c27d824b2d42fc434ee16c2f25a900d87c4a6a3dd383e87547d", "transactionIndex": 0, "blockHash": "0xb71f47ee73a11a8653397bb7e3ea9e40d8bbdc5a2382db72aae854027f5e62c3", "from": "0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C", "to": "0x3319176b32A4c1e68C02Ef454C148eE9b5363C33", "blockNumber": 5848729, "cumulativeGasUsed": 89772, "gasUsed": 89772, "contractAddress": null, "logs": [ { "address": "0x3319176b32A4c1e68C02Ef454C148eE9b5363C33", "topics": [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000009eeab1accb1a701aefab00f3b8a275a39646641c", "0x0000000000000000000000003e3ff16083bf0a444b8ff86c7156eb3368e3cefb", "0x000000000000000000000000000000000000000000000000000000000000c29d" ], "data": "0x", "blockHash": "0xb71f47ee73a11a8653397bb7e3ea9e40d8bbdc5a2382db72aae854027f5e62c3", "blockNumber": 5848729, "transactionHash": "0xd483ec3f28326c27d824b2d42fc434ee16c2f25a900d87c4a6a3dd383e87547d", "transactionIndex": 0, "logIndex": 0, "transactionLogIndex": "0x0", "removed": false } ], "logsBloom": "0x0000000000000000000000000000000000800000...", "status": 1, "effectiveGasPrice": 102000000000, "type": 2 } } ``` # Permissions Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/access/permissions Define and assign permissions to roles. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() # Create a permission p1 = sdk.access.create_permission("ROBOT_CONTROL", "Full Robot Control Access") print("create_permission:", p1) # Assign permission to a role arp = sdk.access.assign_permission_to_role("ROBOT_CONTROL", "ROBOT_OPERATOR") print("assign_permission_to_role:", arp) # Read permission (may be unavailable depending on chain) print(sdk.access.read_permission("ROBOT_CONTROL")) ``` Expected output (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} create_permission: 0x7795a3cff2c33e60... assign_permission_to_role: 0x9b060b42f3d206e9... {'exists': False, 'permission_name': 'ROBOT_CONTROL', 'permission_id': '0x524f424f545f434f4e54524f4c30303030303030303030303030303030303030'} ``` # Roles Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/access/roles Create and manage roles for machines. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() # Create a role r1 = sdk.access.create_role("ROBOT_OPERATOR", "Advanced Robot Operator") print("create_role:", r1) # Grant the role to a user identifier gr = sdk.access.grant_role("ROBOT_OPERATOR", "USER_001") print("grant_role:", gr) # Read back role details rr = sdk.access.read_role("ROBOT_OPERATOR") print(rr) ``` Expected output (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} create_role: 0x394fcf222b439369... grant_role: 0x7f6c67dba35ae5fc... {'role_name': 'ROBOT_OPERATOR', 'role_id': '0x524f424f545f4f50455241544f5230303030303030303030303030303030', 'data': {'id': 'ROBOT_OPERATOR000000000000000000', 'name': 'Advanced Robot Operator', 'enabled': True}, 'exists': True, 'read_status': 'success'} ``` # Create Instance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/create-instance Create a `PeaqRobot` instance to access wallet, identity, and storage modules. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() # 2) Use keystore (recommended) or explicit mnemonic # Keystore path can be set via env PEAQ_ROBOT_KEYSTORE # 3) Choose network (testnet by default). To override: sdk = PeaqRobot(network="agung", mnemonic="your 12/24 words ...") # or "peaq" / custom wss URL ``` Quick check: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} print(sdk.address) print(sdk.balance) # float ``` Properties: * `sdk.address`: SS58 address of the active keypair * `sdk.balance`: free balance in AGUNG units (float) * `sdk.id`: identity module * `sdk.store`: storage module # Options & Callbacks Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/examples/options-test FAST vs FINAL confirmation using the PyPI package. Save as `options_test.py` and run with a funded account. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import asyncio, time from peaq_robot import PeaqRobot from peaq_robot.types import TxOptions, ConfirmationMode sdk = PeaqRobot() # FAST (string tx-hash) tx1 = sdk.store.add_data(f"OPT_FAST_{int(time.time())}", {"t": int(time.time())}) print("FAST:", tx1) # FINAL (awaitable finalize) def on_status(s): try: print(s.model_dump()) except Exception: print({"status": str(s.status), "hash": s.hash}) res = sdk.store.add_data( f"OPT_FINAL_{int(time.time())}", {"t": int(time.time())}, tx_options=TxOptions(mode=ConfirmationMode.FINAL), on_status=on_status, ) final_receipt = asyncio.run(res.finalize) print("FINALIZED:", str(final_receipt)[:180]) ``` Install first: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install -U peaq-robotics-sdk ``` PyPI: [peaq-robotics-sdk](https://pypi.org/project/peaq-robotics-sdk/) Expected output (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} FAST: 0x3b7f...e1 {'status': , 'hash': '0x3b7f...e1', ...} {'status': , 'hash': '0x3b7f...e1', ...} {'status': , 'hash': '0x3b7f...e1', ...} FINALIZED: {'extrinsic_hash': '0x3b7f...e1', 'success': True, 'events': [...]} ``` # Wallet Demo Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/examples/wallet-demo End‑to‑end sample using the PyPI package. Save as `wallet_demo.py` and run with a funded account. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os, time, asyncio from peaq_robot import PeaqRobot from peaq_robot.types import TxOptions, ConfirmationMode sdk = PeaqRobot() print("Address:", sdk.address) print("Balance:", sdk.balance) # Optional: fund via an env mnemonic (demo only) mnemonic = os.getenv("PEAQ_ROBOT_FUND_MNEMONIC", "") if mnemonic: from substrateinterface.keypair import Keypair kp = Keypair.create_from_mnemonic(mnemonic) _ = sdk.wallet.send_transaction( module='Balances', function='transfer_allow_death', params={'dest': sdk.address, 'value': int(1 * 10**18)}, keypair=kp, ) # Create DID (idempotent) and store a small JSON payload try: _ = sdk.id.create_identity() except Exception: pass tx = sdk.store.add_data("DEMO_DATA_001", {"robot": "demo", "status": "active"}) print("Storage tx:", tx) res = sdk.store.read_data("DEMO_DATA_001") print("Read:", res.get("data")) ``` Install first: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install -U peaq-robotics-sdk ``` PyPI: [peaq-robotics-sdk](https://pypi.org/project/peaq-robotics-sdk/) Expected output (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Address: 5F...abc Balance: ... Storage tx: 0x4a8b...9f Read: {'robot': 'demo', 'status': 'active'} ``` # Introduction Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/home The peaq Robotics SDK provides a clean Python interface to onboard robots to peaq. It wraps peaq runtime native Machine Functions and exposes simple modules: * [Identity (`id`)](/peaqchain/sdk-reference/robotics-sdk/python/identity/create-and-read): create and read identity documents * [Access (`access`)](/peaqchain/sdk-reference/robotics-sdk/python/access/roles): roles, permissions, and grants * [Storage (`store`)](/peaqchain/sdk-reference/robotics-sdk/python/storage/add-and-read): write and read compact JSON data Coming soon: * Verification * Time * Payment > Note: The current SDK version runs on any Unitree model with Python support, including industrial-grade robots, as well as on hobbyist-oriented robots by TurtleBot and HiWonder. Quickstart: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() print(sdk.address) print(sdk.balance) # Create DID and store data sdk.id.create_identity() sdk.store.add_data("DEMO", {"ok": True}) ``` What you’ll learn here: * Installation and environment setup * Creating an instance * Reading/writing identity and storage data * Transaction confirmation modes and callbacks * End‑to‑end runnable examples # Create and Read Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/identity/create-and-read This concept has been absorbed into peaqOS. See [peaqID](/peaqos/concepts/peaqid) for the current identity model. Create an identity document and read it back from chain. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() # Minimal (defaults id/controller to your address) try: tx = sdk.id.create_identity() print(tx) except Exception: # Identity may already exist pass # Optional: create with a custom name (idempotent) try: _ = sdk.id.create_identity(name=f"did:peaq:{sdk.address}") except Exception: pass doc = sdk.id.read_identity() print(doc["decoded_data"]) # parsed document if available ``` Response example: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "exists": true, "read_status": "success", "did_account": "5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "name": "identity:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "value": "0x0a3f...", "decoded_data": { "id": "did:peaq:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "controller": "did:peaq:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "verificationMethods": [ { "id": "did:peaq:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8#keys-1", "type": "Sr25519VerificationKey2020", "controller": "did:peaq:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "publicKeyMultibase": "6c97e334e9199cd21944c6882f6222a1b8f5be2ff3b860021f4a074ad4d8d802" } ], "authentications": [ "did:peaq:5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8#keys-1" ], "services": [], "signature": { "type": "Sr25519VerificationKey2020", "issuer": "5EX69aaLFLyDxNcbxVd3Y4FVxjyHTCXtLDx9scg3uQSXF3r8", "hash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" } } } ``` Fields: * `exists`, `read_status` * `name`, `value` (encoded string or raw) * `decoded_data` with `verificationMethods`, `authentications`, `services`, `signature` # Customize Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/identity/customize-document This concept has been absorbed into peaqOS. See [peaqID](/peaqos/concepts/peaqid) for the current identity model. You can fully customize the identity document. All fields are optional; sensible defaults are applied if omitted. Accepted options and types: * `id`: string (default: generated from your address) * `controller`: string (default: same as id) * `verificationMethods`: array of objects `{ id: string, type: string, controller: string, publicKeyMultibase: string }` * `authentications`: array of strings (verification method references) * `services`: array of objects `{ id: string, type: string, serviceEndpoint?: string, data?: string }` * `signature`: object `{ type: string, issuer: string, hash: string }` * `name`: string (stored on-chain; default: `did:peaq:
`) * `tx_options`, `on_status`: transaction options/callback Example (custom fields): ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot from peaq_robot.types import TxOptions, ConfirmationMode sdk = PeaqRobot() res = sdk.id.create_identity( name=f"identity:{sdk.address}", id=f"identity:{sdk.address}", controller=f"identity:{sdk.address}", verificationMethods=[{ "id": f"identity:{sdk.address}#keys-1", "type": "Sr25519VerificationKey2020", "controller": f"identity:{sdk.address}", "publicKeyMultibase": sdk.id.keypair.public_key.hex(), }], authentications=[f"identity:{sdk.address}#keys-1"], services=[ {"id": "#telemetry", "type": "RobotService", "data": "v1"} ], signature={ "type": "Sr25519VerificationKey2020", "issuer": sdk.address, "hash": "demo", }, tx_options=TxOptions(mode=ConfirmationMode.FAST), ) print(res) # Read back the final on-chain document (decoded if available) doc = sdk.id.read_identity() print(doc["decoded_data"]) # includes id, controller, verificationMethods, authentications, services, signature ``` Notes: * If you only want the minimal document, you can omit all fields; defaults are applied. * Services support either serviceEndpoint or compact data for small payloads. * For FINAL confirmation and status logs, pass tx\_options and on\_status. # Installation Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/installation Install the published package from PyPI. Python 3.8–3.12 are supported. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install -U peaq-robotics-sdk ``` PyPI page: [peaq-robotics-sdk](https://pypi.org/project/peaq-robotics-sdk/) ## Verify ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() print(sdk.address) ``` Expected output: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 5F...abc ``` # Network Selection Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/network-selection Choose the network to connect to using a simple string or a full WebSocket URL. Recommended values: * `"peaq"` (mainnet) * `"agung"` (testnet, default) * Custom: any `wss://...` endpoint Tip: See [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq) for WSS URLs. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot # Defaults to Agung testnet sdk = PeaqRobot() # Explicit selections (mainnet first) sdk = PeaqRobot(network="peaq") sdk = PeaqRobot(network="agung") sdk = PeaqRobot(network="wss://your-endpoint") ``` # Add and Read Data Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/storage/add-and-read Write compact JSON data and read it back. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot sdk = PeaqRobot() # Write tx = sdk.store.add_data("TELEMETRY_001", {"battery": 87.3}) print(tx) # Read res = sdk.store.read_data("TELEMETRY_001") print(res["data"]) # decoded JSON if possible ``` Response example (read): ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "exists": true, "read_status": "success", "address": "5F...abc", "data_type": "TELEMETRY_001", "raw": "0x7b2262617474657279223a38372e337d", "data": { "battery": 87.3 } } ``` Limits: * `data_type` ≤ 64 chars * serialized `data` ≤ 256 chars # Telemetry and Configuration Helpers Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/storage/telemetry-and-config Helpers for common data types. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Telemetry compaction keeps payloads within chain limits sdk.store.add_telemetry("G1", { "timestamp": 1710001111, "battery_level": 87.3, "status": "ok", }) # Device configuration sdk.store.add_configuration("G1", {"mode": "autonomous", "interval": 5}) ``` # Confirmation Modes Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/transactions/confirmation-modes Choose how writes return: * FAST (default): returns a string tx-hash immediately after submission * FINAL: waits for finalization and returns a structured result ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot from peaq_robot.types import TxOptions, ConfirmationMode import asyncio sdk = PeaqRobot() # FAST tx = sdk.store.add_data("FAST_DEMO", {"ok": True}) print(tx) # FINAL res = sdk.store.add_data( "FINAL_DEMO", {"ok": True}, tx_options=TxOptions(mode=ConfirmationMode.FINAL) ) final_receipt = asyncio.run(res.finalize) print(final_receipt) ``` Outputs: * FAST returns a string tx-hash like `0x4a8b...9f`. * FINAL status progression (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'status': 'TransactionStatus.BROADCAST', 'hash': '0x4a8b...9f'} {'status': 'TransactionStatus.IN_BLOCK', 'hash': '0x4a8b...9f'} {'status': 'TransactionStatus.FINALIZED', 'hash': '0x4a8b...9f'} ``` Final receipt (shape): ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "extrinsic_hash": "0x4a8b...9f", "success": true, "events": [ { "event": "ExtrinsicSuccess", "phase": "ApplyExtrinsic" }, { "event": "System.ExtrinsicSuccess", "phase": "Finalization" } ] } ``` # Status Callbacks Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/transactions/status-callbacks Receive status updates during submission/finalization. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot from peaq_robot.types import TxOptions, ConfirmationMode, TransactionStatusCallback import asyncio def on_status(s: TransactionStatusCallback): print(s.model_dump()) sdk = PeaqRobot() res = sdk.store.add_data( "FINAL_CB", {"ok": True}, tx_options=TxOptions(mode=ConfirmationMode.FINAL), on_status=on_status, ) final_receipt = asyncio.run(res.finalize) ``` Possible `status`: `BROADCAST`, `IN_BLOCK`, `FINALIZED`. Sample callback prints (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} {'status': 'TransactionStatus.BROADCAST', 'hash': '0x3b7f...e1', 'total_confirmations': 0} {'status': 'TransactionStatus.IN_BLOCK', 'hash': '0x3b7f...e1', 'total_confirmations': 1} {'status': 'TransactionStatus.FINALIZED', 'hash': '0x3b7f...e1', 'total_confirmations': 10} ``` # Wallet Basics Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/python/wallet-and-keystore Create an SDK instance and access your address and balance. Secrets can be provided explicitly if needed. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_robot import PeaqRobot # Default instance sdk = PeaqRobot() print(sdk.address) # Optional: provide secrets explicitly PeaqRobot(mnemonic="...") PeaqRobot(private_key="0x...") ``` Tip: The examples repo uses a local keystore via `examples/.env`: ```ini theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PEAQ_ROBOT_KEYSTORE=.data/wallet.json PEAQ_ROBOT_FUND_MNEMONIC="" ``` # Access Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/access Govern robot permissions using ROS 2 services that wrap the peaq RBAC pallets. Role-based access control (RBAC) is exposed in ROS 2 through `core_node`. The services map one-to-one with the Robotics Python SDK but run under lifecycle management so you can gate access policies alongside mission logic. ## Capabilities * Create roles that encapsulate operational capabilities (`operator`, `maintenance`) * Define permissions (e.g., `store_telemetry`, `emit_intent`) * Assign permissions to roles and grant roles to robot or user DIDs * Query existing policies for audit trails or dashboards ## Service Reference | Service | Type | Purpose | | ------------------------------------------ | ------------------------------------------------- | ----------------------------------- | | `/peaq_core_node/access/create_role` | `peaq_ros2_interfaces/srv/AccessCreateRole` | Add a new role identifier | | `/peaq_core_node/access/create_permission` | `peaq_ros2_interfaces/srv/AccessCreatePermission` | Register a permission definition | | `/peaq_core_node/access/assign_permission` | `peaq_ros2_interfaces/srv/AccessAssignPermToRole` | Attach a permission to a role | | `/peaq_core_node/access/grant_role` | `peaq_ros2_interfaces/srv/AccessGrantRole` | Grant a role to a robot or user DID | All services accept simple JSON payloads and return transaction hashes so you can follow progress via `peaq/tx_status`. ## Example Workflow ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # 1. Create role ros2 service call /peaq_core_node/access/create_role \ peaq_ros2_interfaces/srv/AccessCreateRole \ '{role_id: "operator"}' # 2. Create permission ros2 service call /peaq_core_node/access/create_permission \ peaq_ros2_interfaces/srv/AccessCreatePermission \ '{permission_id: "telemetry:write"}' # 3. Assign permission to role ros2 service call /peaq_core_node/access/assign_permission \ peaq_ros2_interfaces/srv/AccessAssignPermToRole \ '{role_id: "operator", permission_id: "telemetry:write"}' # 4. Grant role to robot DID ros2 service call /peaq_core_node/access/grant_role \ peaq_ros2_interfaces/srv/AccessGrantRole \ '{user_did: "did:peaq:5G...", role_id: "operator"}' ``` Responses include transaction hashes for each step. Combine them with `ros2 topic echo /peaq/tx_status` to confirm finalization. ## Automation Pattern * Trigger RBAC provisioning from CI whenever a new robot identity is created. * Use a ROS 2 Node (Python/C++) that batches service calls and verifies outcomes before declaring a robot operational. * Persist granted roles using your fleet management system for quick audits. ## Best Practices * Prefix permissions with domain context (`telemetry:write`, `mission:cancel`) to avoid collisions. * Use different roles for humans vs. robots even if they share capabilities—this keeps grants revocable per actor type. * Log RBAC service responses for compliance; they already include block hashes and timestamps. Next, wire RBAC with secure data flows by configuring [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage). # Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/identity Manage robot DIDs through lifecycle ROS 2 services with full transaction visibility. This concept has been absorbed into peaqOS. See [peaqID](/peaqos/concepts/peaqid) for the current identity model. peaq ROS 2 brings the Robotics Python SDK’s identity APIs into the ROS 2 ecosystem. `core_node` exposes fully lifecycle-managed services so every robot can create and query decentralized identities (DIDs) without leaving ROS tooling. | Service | Type | Purpose | | --------------------------------- | ----------------------------------------- | ------------------------------------------------- | | `/peaq_core_node/identity/create` | `peaq_ros2_interfaces/srv/IdentityCreate` | Create a DID with optional metadata | | `/peaq_core_node/identity/read` | `peaq_ros2_interfaces/srv/IdentityRead` | Return the DID document linked to the node wallet | ## When to Use It * Provision new robots on the peaq network directly from ROS launch flows * Attach metadata to robots for fleet discovery and access control * Audit DID state via ROS services before orchestrating missions ## Launch Prerequisites ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core core.launch.py \ network:=agung \ config_yaml:=/work/peaq_ros2_examples/config/peaq_robot.yaml ros2 lifecycle set /peaq_core_node configure ros2 lifecycle set /peaq_core_node activate ``` The core node must be active to serve identity calls. Ensure the wallet in `peaq_robot.yaml` holds sufficient testnet funds. ## Create a DID ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/identity/create \ peaq_ros2_interfaces/srv/IdentityCreate \ '{metadata_json: "{\"fleet\": \"warehouse_drivers\"}"}' ``` Service response (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} transaction_hash: 0xabc123... status: PENDING message: "Identity creation submitted" ``` Monitor progress via `peaq/tx_status`: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 topic echo /peaq/tx_status ``` You will see phases such as `PENDING → IN_BLOCK → FINALIZED`. The node publishes the DID to `/tmp/core_node.log` once finalized. ## Read the DID Document ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/identity/read \ peaq_ros2_interfaces/srv/IdentityRead ``` Example output: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} document_json: '{"id":"did:peaq:5G...","metadata":{"fleet":"warehouse_drivers"}}' exists: true network: agung ``` Use this service to confirm metadata before handing off to mission control nodes. ## Integrate in Launch Files Embed identity creation in automated provisioning flows: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # launch/identity_provision.launch.py from launch import LaunchDescription from launch_ros.actions import LifecycleNode def generate_launch_description(): return LaunchDescription([ LifecycleNode( package='peaq_ros2_core', executable='core_node', name='peaq_core_node', parameters=[{'config_yaml': '/work/configs/peaq_robot.yaml'}] ) ]) ``` After activation, issue a `ros2 service call` or include a lightweight automation node that wraps the service call to guarantee every robot registers itself when it boots. ## Operational Tips * Store metadata (e.g., `robot_model`, `location`) to simplify downstream access control rules. * Use `FINAL` confirmation mode for production identity provisioning by setting `PEAQ_ROBOT_CONFIRMATION_MODE=FINAL`. * Automate faucet funding in CI pipelines to avoid manual intervention when spinning up new test robots. Continue to [Access Control](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/access) for managing roles once identities exist. # Node Info Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/node-info Inspect network, wallet, and config details exposed by `/info`. Use the info service to confirm how the core node is configured before running sensitive operations. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/info \ peaq_ros2_interfaces/srv/GetNodeInfo {} ``` Example response (abridged): ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "network": "agung", "wallet_address": "5F...xyz", "confirmation_mode": "FAST", "events_enabled": true, "version": "1.3.0", "build_timestamp": "2025-10-30T09:55:12Z" } ``` The service returns: * Current network + RPC endpoint * Wallet address used for signing transactions * Default confirmation mode * Event streaming status * Package version and build metadata ### Operational Uses * Validate you are pointing at the correct network before funding wallets * Capture node metadata in deployment runbooks or monitoring dashboards * Compare versions across fleet deployments to ensure consistency Pair this with [Event Streams](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/event-streams) to monitor transaction outcomes in real time. # Service Index Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/overview All ROS 2 services exposed by the peaq core workspace with request/response context. `peaq_ros2_core` exposes the full robotics SDK as ROS 2 services. Use this index as a quick reference before diving into the detailed guides that follow. ## Service Catalog | Namespace | Service | Type | Purpose | | ------------ | ------------------- | ------------------------------------------------- | -------------------------------------------------------------------- | | `~/identity` | `create` | `peaq_ros2_interfaces/srv/IdentityCreate` | Provision a DID for the robot wallet with optional metadata | | `~/identity` | `read` | `peaq_ros2_interfaces/srv/IdentityRead` | Fetch the current DID document | | `~/storage` | `add` | `peaq_ros2_interfaces/srv/StoreAddData` | Submit compact JSON data or file pointers to the peaq storage pallet | | `~/storage` | `read` | `peaq_ros2_interfaces/srv/StoreReadData` | Retrieve stored data and IPFS CIDs | | `~/access` | `create_role` | `peaq_ros2_interfaces/srv/AccessCreateRole` | Define a new RBAC role | | `~/access` | `create_permission` | `peaq_ros2_interfaces/srv/AccessCreatePermission` | Register a permission identifier | | `~/access` | `assign_permission` | `peaq_ros2_interfaces/srv/AccessAssignPermToRole` | Link a permission to a role | | `~/access` | `grant_role` | `peaq_ros2_interfaces/srv/AccessGrantRole` | Grant a role to a robot or user DID | | `~/` | `info` | `peaq_ros2_interfaces/srv/GetNodeInfo` | Return node metadata, network, wallet address, and config summary | All services are lifecycle-aware: start the node, configure, and activate it before sending requests. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core core.launch.py ros2 lifecycle set /peaq_core_node configure ros2 lifecycle set /peaq_core_node activate ``` Next explore the focused guides: * [Identity](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/identity) * [Access Control](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/access) * [Storage Services](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/storage) * [Node Info](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/node-info) # Storage Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/storage Use `/storage/add` and `/storage/read` to move data between ROS 2 and the peaq storage pallet. Write and read compact JSON data (or file references) via lifecycle ROS 2 services. For high-throughput telemetry with IPFS, see [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage). ## Write data (`/storage/add`) ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/storage/add \ peaq_ros2_interfaces/srv/StoreAddData \ '{key: "DEMO", value_json: "{\"ok\": true}", confirmation_mode: "FAST"}' ``` Expected output (abridged): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} status: submitted message: "Storage transaction initiated" tx_hash: 0xabc123... ``` ## Read data (`/storage/read`) ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/storage/read \ peaq_ros2_interfaces/srv/StoreReadData \ '{key: "DEMO"}' ``` Example response: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} exists: true value_json: '{"ok": true}' cid: Qm... ``` Subsections * Add: submit JSON data with `/storage/add` * Read: retrieve JSON data with `/storage/read` * Telemetry & Configuration: see [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage) for IPFS setup and retry policies Tips * Pair service calls with `ros2 topic echo /peaq/tx_status` to monitor confirmation phases (`PENDING`, `IN_BLOCK`, `FINALIZED`). * Use `confirmation_mode: FINAL` in production for stronger guarantees. * For files, publish via the `peaq/storage/ingest` topic handled by the storage bridge. # Example Launches Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/examples/overview Validate end-to-end ROS 2 + peaq pipelines with production-ready launch files. `peaq_ros2_examples` packages launch files that orchestrate the core services, storage bridge, and helper scripts. Use them to validate your setup or as templates for project-specific launches. ## `demo_core.launch.py` Launches the core node and events node together: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_examples demo_core.launch.py \ network:=agung \ confirmation_mode:=FAST \ log_level:=INFO ``` * Starts `core_node` + `events_node` * Applies configuration from `peaq_ros2_examples/config/peaq_robot.yaml` * Suitable for identity, access, and storage service testing * Emits rich telemetry on `peaq/tx_status` and `peaq/events` so monitors light up immediately ## `storage_bridge.launch.py` Included with `peaq_ros2_core`, this launch is often combined with `demo_core` to exercise storage flows: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core storage_bridge.launch.py ``` * Subscribes to `peaq/storage/ingest` * Publishes `peaq/storage/status` * Honours retry policies defined in the shared configuration file * Demonstrates how RBAC-protected storage flows finalize on-chain with streaming feedback ## `e2e_test.launch.py` Runs a full end-to-end scenario used by the testing guide: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_examples e2e_test.launch.py \ network:=agung \ confirmation_mode:=FINAL ``` * Spins up core services, storage bridge, and test utilities * Validates DID creation, storage writes, and event streaming automatically Each launch exposes the same arguments (`network`, `confirmation_mode`, `log_level`, `config_yaml`). Extend them with additional nodes or parameters as needed for your deployment. Continue with the [example scripts](/peaqchain/sdk-reference/robotics-sdk/ros2/examples/scripts) to trigger specific blockchain operations. # Example Scripts Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/examples/scripts Run guided scripts that exercise ROS 2 services, storage bridge, and event streaming end to end. The examples package ships Python scripts that call into ROS 2 services and topics. They are great starting points for automated tests or operator tooling. ## `create_identity.py` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_examples create_identity ``` * Prompts for optional metadata and creates a DID via `/identity/create` * Streams transaction updates until finalized * Prints the resulting DID document when available ## `send_store_add_data.py` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_examples send_store_add_data ``` * Requests a key, payload, and confirmation mode interactively * Publishes telemetry through the storage bridge and waits for completion * Reads the stored data back to verify the flow Embed the client class (`StorageClient`) in your own scripts to reuse retry handling and logging. ## `emit_intent.py` Even without humanoid adapters, this script is useful for broadcasting structured events and verifying event streaming: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_examples emit_intent ``` * Provides menus for locomotion, posture, and gesture intents * Publishes events onto `peaq/events` so you can observe payloads and transaction status updates ## `user_quickstart.py` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_examples user_quickstart ``` * Chains identity creation, storage operations, and access control into a single guided flow * Ideal for validating that wallet funding and configuration are correct before deploying robots ## Utilities * `inspect_sdk.py`: prints the active configuration and SDK status * `print_wallet_addr.py`: displays the wallet address associated with the current keystore * `retry_failed_storage.py`: replays failed storage submissions recorded by the storage bridge All scripts expect the workspace to be sourced and the relevant nodes running. Combine them with the [example launches](/peaqchain/sdk-reference/robotics-sdk/ros2/examples/overview) to exercise complete robotics pipelines without writing additional code and to showcase ROS 2 support as a first-class citizen of the peaq robotics platform. # Configuration Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/getting-started/configuration Provide network, wallet, and storage settings for ROS 2 nodes through environment variables or YAML. The packages read configuration from either environment variables or a YAML file referenced in launch parameters. Use whichever is easier for your deployment and keep sensitive data (wallet passwords, managed IPFS JWTs) outside of version control. ## Environment Variables ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Network and confirmation mode export PEAQ_ROBOT_NETWORK=agung export PEAQ_ROBOT_CONFIRMATION_MODE=FAST # Wallet keystore export PEAQ_ROBOT_KEYSTORE=$HOME/.peaq_robot/wallet.json export PEAQ_ROBOT_KEY_PASSWORD="super-secret" # Event forwarding and logging export PEAQ_ROBOT_EVENTS_ENABLED=true export PEAQ_ROBOT_LOG_LEVEL=INFO export PEAQ_ROBOT_LOG_FORMAT=human ``` All nodes (`core_node`, `storage_bridge_node`, `events_node`) consume these variables through the shared configuration module. When unset, defaults come from the YAML configuration or each node’s internal defaults. ## YAML Configuration Create a copy of the example file and fill in required fields: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cp peaq_ros2_examples/config/peaq_robot.example.yaml \ peaq_ros2_examples/config/peaq_robot.yaml ``` Example snippet: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} network: agung # or "peaq" for mainnet wallet: path: /work/peaq_wallet.json auto_generate: true default_confirmation_mode: FAST events: enabled: true storage_bridge: storage: mode: pinata pinata: jwt: "" gateway_url: "https://YOUR-GATEWAY.mypinata.cloud/ipfs" ``` Launch files pass this YAML through `config.yaml_path` parameters so that every node reads a single source of truth. Set `auto_generate: true` during testing to create a wallet automatically; switch to a managed keystore in production. ### Secret Management * Keep `PEAQ_ROBOT_KEY_PASSWORD` in an `.env` file or secret manager and source it before launching nodes. * Do not commit the populated `peaq_robot.yaml` to version control—copy it per environment instead. * Rotate managed gateway credentials regularly and store them in the same secure channel as other secrets. Proceed to [launch the core services](/peaqchain/sdk-reference/robotics-sdk/ros2/getting-started/launch-core-services) once configuration is in place. # Installation Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/getting-started/installation Set up the peaq ROS 2 workspace with Docker or a native Humble environment. The ROS 2 packages live in the [`peaq-robotics-ros2`](https://github.com/peaqnetwork/peaq-robotics-ros2) repository. Clone the workspace and choose either a Docker-based workflow or a native ROS 2 Humble setup. ## Docker Workflow (Recommended) ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 # Build the development image docker build -t peaq-ros2:latest . # Start a container with workspace mounted docker run -it --rm \ --name peaq-ros2-dev \ -v "$(pwd)":/work \ -w /work \ -p 5001:5001 -p 8080:8080 \ peaq-ros2:latest # Inside the container source /opt/ros/humble/setup.bash colcon build source install/setup.bash ``` The Docker image installs ROS 2 Humble, Python dependencies, and build tooling. Exposed ports (`5001`, `8080`) let IPFS nodes or optional managed gateways run locally when using the storage bridge. ## Native Humble Setup ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 # Install Python requirements pip install -r requirements.txt # Ensure ROS 2 Humble is installed and sourced source /opt/ros/humble/setup.bash # (Optional) install additional system dependencies sudo rosdep init 2>/dev/null || true rosdep update rosdep install --from-paths . --ignore-src -r -y || true # Build the workspace colcon build source install/setup.bash ``` ### IPFS (with Optional Managed Pinning) The storage bridge relies on an IPFS node. Follow the repository guide to install Kubo, run `ipfs daemon`, and optionally supply managed gateway credentials (e.g., Pinata, web3.storage) in the configuration file when required. Continue with [configuration](/peaqchain/sdk-reference/robotics-sdk/ros2/getting-started/configuration) once the workspace builds successfully. # Launch Core Services Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/getting-started/launch-core-services Run lifecycle-managed ROS 2 nodes for peaq identity, access, and storage operations. After configuring the workspace, start the lifecycle nodes that provide blockchain connectivity. Source both the ROS 2 distro and the workspace overlay before launching. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} source /opt/ros/humble/setup.bash source install/setup.bash # Launch core services with default parameters ros2 launch peaq_ros2_core core.launch.py ``` The launch file spins up `core_node` and `events_node` with lifecycle management enabled. Key launch arguments: | Parameter | Default | Description | | ------------------- | ------------------------------------------- | --------------------------------------------------- | | `network` | `agung` | peaq network to target (`peaq` for mainnet) | | `confirmation_mode` | `FAST` | Transaction confirmation strategy (`FAST`, `FINAL`) | | `log_level` | `INFO` | Logging level across nodes | | `config_yaml` | `peaq_ros2_examples/config/peaq_robot.yaml` | Path to configuration file | Override them inline: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core core.launch.py \ network:=peaq \ confirmation_mode:=FINAL \ log_level:=DEBUG \ config_yaml:=/work/configs/robot.yaml ``` ### Lifecycle Management The nodes start in the *unconfigured* state. Use standard lifecycle commands to configure and activate them: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 lifecycle set /peaq_core_node configure ros2 lifecycle set /peaq_core_node activate ros2 lifecycle get /peaq_core_node ``` Repeat the lifecycle commands for `/peaq_events_node` if you need to pause or resume event streaming independently. Nodes publish status logs to `/tmp/core_node.log` and `/tmp/events_node.log` by default. ### Storage Bridge Launch the storage bridge when you want IPFS-backed storage flows (managed pinning optional): ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core storage_bridge.launch.py \ config_yaml:=/work/peaq_ros2_examples/config/peaq_robot.yaml ``` The bridge consumes the same configuration file, handles retry logic, and exposes ROS 2 interfaces documented in [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage). For a full demo, continue with the [example launches](/peaqchain/sdk-reference/robotics-sdk/ros2/examples/overview). # Introduction Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/home Enterprise-grade ROS 2 integration for the peaq robotics stack. The peaq Robotics SDK for ROS 2 brings blockchain-native identity, access, and storage flows to ROS applications. It exposes simple modules you can use across robot fleets: * [Identity (`id`)](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/identity): create and read identity documents * [Access (`access`)](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/access): roles, permissions, and grants * [Storage (`store`)](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/storage): write and read compact JSON data * [Tether WDK (`tether`)](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/overview): create EVM addresses, read USDT balances, and transfer USDT on peaq EVM Coming soon: * Verification * Time > Note: The current ROS 2 integration targets Humble (or later) and works on the same robot classes as the Robotics Python SDK—from Unitree models to TurtleBot/HiWonder—whenever ROS 2 is available on the device. Quickstart: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Clone and build git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 pip install -r requirements.txt source /opt/ros/humble/setup.bash colcon build && source install/setup.bash # Launch core services ros2 launch peaq_ros2_core core.launch.py # Configure and activate lifecycle ros2 lifecycle set /peaq_core_node configure ros2 lifecycle set /peaq_core_node activate # Create an identity ros2 service call /peaq_core_node/identity/create \ peaq_ros2_interfaces/srv/IdentityCreate \ '{metadata_json: "{\"type\": \"robot\"}"}' ``` What you’ll learn here: * Installation and environment setup * Configuration and launching core services * Reading/writing identity and storage data * Access control (roles, permissions, grants) * Event streams and verifiable storage pipelines * End‑to‑end runnable examples # Event Streams Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/event-streams Subscribe to blockchain events and transaction lifecycles from ROS 2. The ROS 2 event pipeline keeps operators and autonomy stacks informed about blockchain activity in real time. `events_node` converts SDK callbacks into ROS topics so you can react to confirmations, failures, or custom pallet events without polling. ## Topics | Topic | Type | Description | | ---------------- | ----------------------------------- | -------------------------------------------------------------------------- | | `peaq/tx_status` | `peaq_ros2_interfaces/msg/TxStatus` | Transaction lifecycle updates (`PENDING`, `IN_BLOCK`, `FINALIZED`, errors) | | `peaq/events` | `peaq_ros2_interfaces/msg/Event` | Raw blockchain events emitted by pallets (identity, storage, access, etc.) | Both topics are QoS-configured for reliability in distributed robot fleets. ## Launch Events Node The events node starts automatically with `core.launch.py`. To run it on its own: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_core events_node --ros-args \ -p config_yaml:=/work/peaq_ros2_examples/config/peaq_robot.yaml ros2 lifecycle set /peaq_events_node configure ros2 lifecycle set /peaq_events_node activate ``` ## Monitor Transactions ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 topic echo /peaq/tx_status ``` Example output: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} phase: IN_BLOCK hash: 0x9b060b42... details: confirmation_mode: FAST endpoint: identity.create submitted_at: "2025-10-30T10:05:31Z" ``` Use the metadata to drive dashboards, emit alerts, or trigger retries in application logic. ## Subscribe to Events ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 topic echo /peaq/events --qos-reliability reliable ``` Events include pallet name, method, and payload JSON. Example for a DID creation: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pallet: identity method: IdentityCreated payload: '{"owner":"did:peaq:5G...","metadata":{"fleet":"warehouse_drivers"}}' ``` ## Integrate with Autonomy Stacks * Feed `peaq/tx_status` into behavior trees to wait for blockchain confirmation before executing moves. * Use `peaq/events` to trigger ROS actions (e.g., start storage ingestion once a DID is acknowledged). * Attach event subscribers to logging pipelines for compliance and analytics. ## Troubleshooting * If events stop flowing, check lifecycle state: `ros2 lifecycle get /peaq_events_node`. * Review `/tmp/events_node.log` for connection or authentication issues. * Ensure `PEAQ_ROBOT_EVENTS_ENABLED=true` is set when relying on environment variables. With event streaming in place you have an end-to-end feedback loop across identity, access, and storage operations. Pair this with the [Messaging Index](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/topics) and [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage) to cover every messaging pathway. # Messaging Index Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/topics ROS 2 topics exposed by the peaq robotics workspace and how to use them. The ROS 2 workspace exposes several pub/sub channels in addition to the service APIs. Use this index to stitch blockchain events and storage flows into your autonomy stack. ## Command & Telemetry Topics | Topic | Type | Direction | Purpose | | --------------------- | ---------------------------------------- | --------- | ----------------------------------------------------- | | `peaq/storage/ingest` | `peaq_ros2_interfaces/msg/StorageIngest` | Publish | Send telemetry or file CIDs to the storage bridge | | `peaq/storage/status` | `peaq_ros2_interfaces/msg/StorageResult` | Subscribe | Track success/failure for each storage ingest | | `peaq/tx_status` | `peaq_ros2_interfaces/msg/TxStatus` | Subscribe | Transaction lifecycle updates for all peaq operations | | `peaq/events` | `peaq_ros2_interfaces/msg/Event` | Subscribe | Raw events emitted by peaq pallets | ### StorageIngest Schema ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} string key bool is_file string file_path string content string content_type string metadata_json ``` * Use `is_file=true` with `file_path` when pointing to local files that the bridge should upload to IPFS * Provide structured payloads via `content` (JSON) plus an optional `metadata_json` envelope for robot state, firmware versions, or mission identifiers ### StorageResult Schema ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} string key string cid string ipfs_url string tx_hash string status string error ``` * `status` transitions through `PENDING`, `RETRYING`, `SUCCESS`, or `FAILED` * Failed entries are also written to `/tmp/storage_bridge_failures.jsonl` so you can replay them later See [Verifiable Storage](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage) to add DID-backed authentication on top of these topics. # Verifiable Storage Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/verifiable-storage Cryptographically prove that every ROS 2 data point came from the right robot. **Verifiable Storage** is more than a storage utility—it is the trust anchor that guarantees on-chain data really originated from your robot. By combining ROS 2 lifecycle management, DID-backed authentication, and IPFS persistence (with optional managed pinning), the bridge turns raw telemetry into signed, auditable evidence. ## Why It Matters * **Authenticity first** – every ingest request is checked against the robot’s DID and RBAC grants before it ever touches the blockchain. * **Tamper proof** – payloads are hashed, pinned to IPFS, and referenced on-chain so downstream consumers can verify integrity independently (managed pinning services remain optional). * **Production ready** – lifecycle nodes, retries, and detailed status topics keep operators informed without leaving ROS tools. > **Big picture:** ROS 2 fleets can now publish verifiable data streams that regulators, partners, or marketplaces can trust instantly. ## Architecture | Stage | What Happens | Verification Hooks | | ----------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | 1. Ingest | `StorageIngest` message arrives on `peaq/storage/ingest` | Bridge loads the robot wallet, derives its DID, and checks it exists on-chain when `require_did` is enabled | | 2. Encode | Payload hashed, optional files pinned to IPFS (local or managed) | Hash and DID are logged together; RBAC rules (from the Access module) can gate who triggers ingest | | 3. Submit | Transaction sent via peaq storage pallet | Transaction is signed by the robot’s keystore and tracked through `peaq/tx_status` | | 4. Finalize | On-chain record references IPFS CID | Any consumer can fetch the CID, recompute the hash, and confirm the signing DID | ## Launch the Bridge ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_core storage_bridge.launch.py \ config_yaml:=/work/peaq_ros2_examples/config/peaq_robot.yaml \ log_level:=INFO ros2 lifecycle set /peaq_storage_bridge configure ros2 lifecycle set /peaq_storage_bridge activate ``` ### Verification-Centric Configuration ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} storage_bridge: robot: require_did: true # verify robot DID is registered on peaq storage: mode: both # local_ipfs | pinata | both pinata: # placeholder name used by the default config jwt: "" # works with any compatible managed gateway gateway_url: "https://your-gateway.example.com/ipfs" local_ipfs: api_url: "http://127.0.0.1:5001" gateway_url: "http://127.0.0.1:8080/ipfs" retry: max_attempts: 3 delay_seconds: 5.0 signature: algorithm: sr25519 # bridge signs payloads with the robot wallet ``` Secrets for managed gateways (JWT/API keys) and wallet passwords must stay out of source control—load them via environment variables before launch. ## IPFS Setup ### Local IPFS (Kubo) ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Install Kubo wget https://dist.ipfs.tech/kubo/v0.38.1/kubo_v0.38.1_linux-amd64.tar.gz tar -xzf kubo_v0.38.1_linux-amd64.tar.gz sudo bash kubo/install.sh # Initialize and start ipfs init ipfs daemon ``` Update the config YAML to point at your node: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} storage_bridge: storage: mode: local_ipfs local_ipfs: api_url: http://127.0.0.1:5001 gateway_url: http://127.0.0.1:8080/ipfs ``` Optional quality-of-life settings: * `local_ipfs.save_dir`: cache directory for downloaded blobs * `local_ipfs.pin_results=true`: keep data pinned locally for quick replays ### Managed Gateway (Optional) Use a third-party IPFS pinning/gateway provider (e.g., Pinata, web3.storage, NFT.storage) only if you need off-device persistence or public access. Example environment variables: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export PEAQ_ROBOT_IPFS_JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." export PEAQ_ROBOT_IPFS_GATEWAY="https://your-gateway.example.com/ipfs" ``` Reference them in the YAML (the `pinata` block name is historical—you can still point it at any managed gateway): ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} storage_bridge: storage: mode: pinata pinata: jwt: "${PEAQ_ROBOT_IPFS_JWT}" gateway_url: "${PEAQ_ROBOT_IPFS_GATEWAY}" pin: true mode: upload ``` Using both local IPFS and a managed gateway provides redundancy—set `mode: both` to mirror uploads. ### Access Control & Wallets * Ensure the wallet referenced in `wallet.path` has enough balance on the target network (fund via faucet for Agung). * Combine with RBAC by allow-listing roles under `storage_bridge.robot.allowlist_roles` when you want to restrict which robots can publish telemetry. ## Publishing Verifiable Data ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 topic pub --once /peaq/storage/ingest \ peaq_ros2_interfaces/msg/StorageIngest \ '{key: "robot:telemetry", content: "{\"battery\": 0.87}", is_file: false}' ros2 topic echo /peaq/storage/status ``` The storage status stream surfaces the CID, IPFS URL, transaction hash, and success state for each submission. Combine it with `peaq/tx_status` to see the confirmation phases tied back to the robot DID in logs. ## Reading & Verifying Downstream ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaq_core_node/storage/read \ peaq_ros2_interfaces/srv/StoreReadData \ '{key: "robot:telemetry"}' ``` The response includes the payload, IPFS CID, and the originating DID. Consumers can recompute the hash against the IPFS artifact and ensure it matches the on-chain record. ### Manual Verification Checklist 1. Fetch the CID from the service response. 2. Retrieve the payload: `ipfs cat ` or `curl /`. 3. Recompute the hash and compare with the value logged in storage bridge outputs. 4. Confirm DID ownership using `/peaq_core_node/identity/read`. ## Automated Attestation * Attach mission metadata via the `metadata_json` field in `StorageIngest` so every record includes firmware versions or profile IDs. * Pair with the [Access Control](/peaqchain/sdk-reference/robotics-sdk/ros2/core-services/access) guides to revoke publishing rights instantly. * Leverage `/tmp/storage_bridge_failures.jsonl` and the replay scripts to prove that no data was dropped—even during outages. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python3 scripts/check_storage_failures.py --details python3 scripts/retry_failed_storage.py --key robot:telemetry ``` ## Observability & Audit * Switch to JSON logs (`PEAQ_ROBOT_LOG_FORMAT=json`) for ingestion into SIEM or compliance tooling. * Track wallet-derived DID and CID pairs in your log pipeline to detect impersonation attempts. * Use `ros2 lifecycle get /peaq_storage_bridge` in your health probes; the node exports ready/active states so Kubernetes or fleet managers can react quickly. ### Dashboard Pointers * `peaq/storage/status`: success vs failure counts * `/tmp/storage_bridge_failures.jsonl`: monitor size/age to detect backlogs * `peaq/tx_status`: confirmation latency per network With verifiable telemetry in place, your ROS 2 fleet can supply zero-trust data to marketplaces, regulators, or partners. Continue with [Event Streams](/peaqchain/sdk-reference/robotics-sdk/ros2/messaging/event-streams) to surface confirmations to autonomy stacks. # Service Index Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/tether/overview All address-only Tether WDK services exposed by `peaq_tether_node` with request/response context. `peaq_ros2_tether` adds an optional ROS 2 node (`peaq_tether_node`) that integrates **Tether WDK** (EVM wallet module) so robots can: * Create an **EVM wallet address** * Query **USDT (ERC-20)** balance on peaq EVM * Transfer **USDT** on peaq EVM This integration is **address-only**: * ROS APIs use **only EVM addresses** (`address`, `from_address`, `to_address`) * Wallet secrets (mnemonic) are stored locally on the robot/machine in **one shared registry file** * No private keys/mnemonics are ever sent over ROS services ## Service Catalog | Service | Type | Purpose | | --------------------------------- | ----------------------------------------------- | -------------------------------------------------------------- | | `/peaq_tether_node/wallet/create` | `peaq_ros2_interfaces/srv/TetherCreateWallet` | Create a new EVM wallet address (mnemonic stored locally) | | `/peaq_tether_node/usdt/balance` | `peaq_ros2_interfaces/srv/TetherGetUsdtBalance` | Read USDT balance for an address | | `/peaq_tether_node/usdt/transfer` | `peaq_ros2_interfaces/srv/TetherTransferUsdt` | Transfer USDT from one local wallet address to another address | Use this index as a quick reference, then dive into the focused guides: * [Wallet](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/wallet) * [USDT Balance](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/usdt-balance) * [USDT Transfer](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/usdt-transfer) * [Wallet Registry](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/wallet-registry) ## Launch Prerequisites Install JS dependencies once (Node.js required where the node runs): ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cd peaq_ros2_tether/js npm ci ``` Start the node via `ros2 run`. Notes: * If you run multiple ROS 2 environments on the same host, isolate them with `ROS_DOMAIN_ID` to prevent service collisions. * When running from a **source checkout**, the node prefers a CLI path where Node can resolve `node_modules/`. The simplest approach is to run from the workspace root. You can also override the CLI path explicitly with `PEAQ_TETHER_CLI_PATH`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" # Point this to your clone path export REPO_DIR="" cd "$REPO_DIR" export PEAQ_CFG="$REPO_DIR/peaq_ros2_examples/config/peaq_robot.yaml" # IMPORTANT: set this explicitly. If REPO_DIR is empty, the default becomes "/peaq_ros2_tether/..." export PEAQ_TETHER_CLI_PATH="$REPO_DIR/peaq_ros2_tether/js/peaq_tether_cli.mjs" # Option A (recommended): tmux detach (keeps the node running after SSH disconnects) tmux new -s tether # inside tmux: ros2 run peaq_ros2_tether tether_node --ros-args -p config.yaml_path:="$PEAQ_CFG" # detach without stopping: Ctrl-b then d # # After detaching, the node keeps running and ROS services continue to work from any terminal # (as long as you source your ROS environment and use the same ROS_DOMAIN_ID). # Option B: background + disown # ros2 run peaq_ros2_tether tether_node --ros-args -p config.yaml_path:="$PEAQ_CFG" \ # > /tmp/tether_node.log 2>&1 & disown ``` Confirm the node is running and services are registered: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" # In a new terminal you must source ROS so `ros2` is available: # source /opt/ros//setup.bash # source /install/setup.bash ros2 node list | grep peaq_tether_node ros2 service list | grep peaq_tether_node ``` Stop the node: * **tmux option**: * Re-attach: `tmux attach -t tether` * Stop inside tmux: `Ctrl-c` * (Or kill the session): `tmux kill-session -t tether` * **background/disown option**: * Try: * `pkill -f "ros2 run peaq_ros2_tether tether_node" || true` * `pkill -f "peaq_ros2_tether/tether_node" || true` * If the node was started as another user (or with `sudo`), you may need `sudo pkill ...`. * Verify it’s gone: * `pgrep -af "peaq_ros2_tether.*tether_node" || echo "tether node stopped"` If `ros2 node list` still shows stale entries right after stopping, restart the ROS CLI daemon and try again: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 daemon stop || true ros2 daemon start || true ``` # USDT Balance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/tether/usdt-balance Read USDT (ERC-20) balances on peaq EVM using address-only ROS 2 services. Use this service to query USDT balances for any EVM address on peaq EVM. The call is read-only and does not require the address to exist in the local registry. ## Service Reference | Service | Type | Purpose | | -------------------------------- | ----------------------------------------------- | ---------------------------------- | | `/peaq_tether_node/usdt/balance` | `peaq_ros2_interfaces/srv/TetherGetUsdtBalance` | Return USDT balance for an address | ## Launch Prerequisites Start `peaq_tether_node` once, then call services as needed. * To start/stop the node (tmux + background options), follow the steps in [Service Index](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/overview). * In any new terminal where you run `ros2` commands, make sure you’ve sourced your ROS/workspace environment and set the same `ROS_DOMAIN_ID` as the node: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # source /opt/ros//setup.bash # source /install/setup.bash export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ``` ## Check Balance ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ros2 service call /peaq_tether_node/usdt/balance \ peaq_ros2_interfaces/srv/TetherGetUsdtBalance \ "{address: '0x...'}" ``` Response fields: * `balance_raw`: base-10 string (uint256) * `balance_formatted`: formatted using configured decimals (default 6 for peaq USDT) ## Tips * If you hit provider rate limits, retry after a short delay or use a higher-throughput RPC endpoint. * Keep the USDT contract address in config in quotes (YAML) to avoid type parsing issues. # USDT Transfer Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/tether/usdt-transfer Dry-run and broadcast USDT transfers on peaq EVM using address-only ROS 2 services. Use this service to transfer USDT from a locally-managed wallet address to any recipient address. The service is **address-only**: * `from_address` identifies which local wallet to use for signing (mnemonic loaded from the local registry) * `to_address` is the destination address ## Service Reference | Service | Type | Purpose | | --------------------------------- | --------------------------------------------- | ---------------------------------------------------------------- | | `/peaq_tether_node/usdt/transfer` | `peaq_ros2_interfaces/srv/TetherTransferUsdt` | Transfer USDT from a local wallet address to a recipient address | ## Launch Prerequisites Start `peaq_tether_node` once, then call services as needed. * To start/stop the node (tmux + background options), follow the steps in [Service Index](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/overview). * In any new terminal where you run `ros2` commands, make sure you’ve sourced your ROS/workspace environment and set the same `ROS_DOMAIN_ID` as the node: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # source /opt/ros//setup.bash # source /install/setup.bash export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ``` ## Dry-run (Quote Only) Use `dry_run: true` to estimate and validate the transfer without broadcasting: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ros2 service call /peaq_tether_node/usdt/transfer \ peaq_ros2_interfaces/srv/TetherTransferUsdt \ "{from_address: '0xFROM', to_address: '0xTO', amount: '0.05', dry_run: true}" ``` ## Real Transfer (Broadcast) ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ros2 service call /peaq_tether_node/usdt/transfer \ peaq_ros2_interfaces/srv/TetherTransferUsdt \ "{from_address: '0xFROM', to_address: '0xTO', amount: '0.05', dry_run: false}" ``` ## Common Failure Modes * `INSUFFICIENT_FUNDS`: the `from_address` lacks native token for gas (even if USDT amount is 0). * `Unknown address`: the `from_address` is not present in the local registry (no mnemonic available for signing). * Provider rate limits: retry after a short delay, or use a higher-throughput RPC provider. # Wallet Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/tether/wallet Create EVM wallet addresses from ROS 2 without ever passing secrets over services. Use the wallet service to create a new EVM address that the robot can use for USDT operations on peaq EVM. The mnemonic is stored locally in the shared registry file and is never sent over ROS by default. ## Service Reference | Service | Type | Purpose | | --------------------------------- | --------------------------------------------- | --------------------------------------------------------- | | `/peaq_tether_node/wallet/create` | `peaq_ros2_interfaces/srv/TetherCreateWallet` | Create a new EVM wallet address (mnemonic stored locally) | ## Launch Prerequisites Start `peaq_tether_node` once, then call services as needed. * To start/stop the node (tmux + background options), follow the steps in [Service Index](/peaqchain/sdk-reference/robotics-sdk/ros2/tether/overview). * In any new terminal where you run `ros2` commands, make sure you’ve sourced your ROS/workspace environment and set the same `ROS_DOMAIN_ID` as the node: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # source /opt/ros//setup.bash # source /install/setup.bash export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ``` ## Create a Wallet Address ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" ros2 service call /peaq_tether_node/wallet/create \ peaq_ros2_interfaces/srv/TetherCreateWallet \ "{label: 'robot_001', export_mnemonic: false}" ``` Response fields: * `address`: checksummed EVM address (0x...) * `mnemonic`: empty unless **both** of the following are true: * You call the service with `export_mnemonic: true` * You explicitly allow unsafe export in config (`tether.wallet_registry.unsafe_export_mnemonic: true`) ## Unsafe Mnemonic Export (Development Only) Mnemonic export is disabled by default. To enable it temporarily for local testing: 1. Set the config flag in `peaq_robot.yaml`: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tether: wallet_registry: unsafe_export_mnemonic: true ``` 2. Request export in the service call: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-94}" # Restart the node after enabling unsafe export in config (stop + start using your chosen method). # Example stop command (background mode): # pkill -f "peaq_ros2_tether.*tether_node" 2>/dev/null || true ros2 service call /peaq_tether_node/wallet/create \ peaq_ros2_interfaces/srv/TetherCreateWallet \ "{label: 'robot_001', export_mnemonic: true}" ``` > Warning: Never enable mnemonic export in production. Treat exported mnemonics like root credentials. ## Operational Tips * Treat the returned `address` as the only identifier used by the tether node APIs. * Do not enable mnemonic export in production. * Back up the registry file securely if the address controls real funds. # Wallet Registry Source: https://docs.peaq.xyz/peaqchain/sdk-reference/robotics-sdk/ros2/tether/wallet-registry Understand how `peaq_tether_node` stores wallet secrets locally and why ROS remains address-only. `peaq_tether_node` stores wallet secrets locally in a single shared registry file on the robot/machine. This design keeps ROS APIs **address-only** while allowing the node to sign EVM transactions when you call transfer services. ## Why a Registry Exists * ROS services must never receive mnemonics or private keys. * Transfers must still be signed somewhere; the node signs locally by loading the mnemonic from disk. * A single file registry fits the “single robot / single host” assumption and simplifies operations. ## Configuration In `peaq_robot.yaml`: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tether: wallet_registry: path: "~/.peaq_robot/tether_wallets.json" unsafe_export_mnemonic: false ``` ### Enabling mnemonic export (unsafe) Mnemonic export is **disabled by default**. If you explicitly enable: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tether: wallet_registry: unsafe_export_mnemonic: true ``` then `wallet/create` will return a mnemonic **only when** the request sets `export_mnemonic: true`. ## Security Invariants * The EVM `address` is the only identifier used by services. * Mnemonics are stored locally and are never emitted over ROS by default. * Keep the registry file restricted (e.g., mode `0600`) and treat it like a keystore. ## Operational Tips * Back up the registry if addresses control real funds (secure storage only). * If you rotate machines, migrate the registry file with care. * If multiple ROS 2 environments run on one host, isolate with `ROS_DOMAIN_ID` to prevent service collisions. * To keep `peaq_tether_node` running without tying up your terminal, start it in `tmux` (detach with `Ctrl-b` then `d`) or run it in the background and `disown` it. # Cancel Contract Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/cancelContract ## `cnft.cancelContract(CancelContract)` Cancel a Contract NFT draft. This sends a transaction from the contract controller and removes the draft from storage. ### CancelContract Type Parameters | Parameter | Type | Required | Description | | ---------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------- | | **contractController** | `Signer` | Required | Contract controller signer authorized to cancel the contract. Must be connected to a provider. | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractId** | `string` | Required | Contract ID to cancel. | ### Returns | Field | Type | Description | | --------------- | -------------------- | ---------------------------------------------------- | | **status** | `cancelled` | Status of the operation. | | **contractNft** | `string` | Contract NFT contract address. | | **contractId** | `string` | Contract ID that was cancelled (from emitted event). | | **cancelledBy** | `string` | Controller address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt for the cancellation call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Contract controller const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 3. Cancel the contract const result = await rwa_sdk.cnft.cancelContract({ contractController: alice, contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Contract controller and counterparties const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 3. Cancel the contract const result = await rwa_sdk.cnft.cancelContract({ contractController: alice, contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'cancelled', contractNft: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a', contractId: '1234567890', cancelledBy: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', receipt: ContractTransactionReceipt { ... } } ``` Note: Once cancelled, `getDraft` for the same `contractId` will revert with `Not found`. # Create Contract Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/createContract ## `cnft.createContract(CreateContract)` Create a Contract NFT draft by initializing the contract with counterparties and content, and paying the setup fee (ERC20). This sends a transaction from the contract controller. ### CreateContract Type Parameters | Parameter | Type | Required | Description | | ---------------------- | ---------- | -------- | ----------------------------------------------------------------------------------------------- | | **contractController** | `Signer` | Required | Signer that controls the contract and submits the transaction. Must be connected to a provider. | | **erc20** | `string` | Required | ERC20 token address used to pay the setup fee. | | **tokenDecimals** | `number` | Required | ERC20 token decimals for human-readable fee amounts. | | **counterparties** | `string[]` | Required | Array of counterparty EOA addresses that must sign the contract. | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractHash** | `string` | Required | Content hash (e.g., `keccak256` of the contract content). | | **url** | `string` | Required | URL pointing to the contract content/metadata. | ### Returns | Field | Type | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | **status** | `created` | Status of the operation. | | **contractNft** | `string` | Contract NFT contract address. | | **contractId** | `string` | Contract ID for the draft. | | **contractController** | `string` | Controller EOA address. | | **counterparties** | `string[]` | Counterparty addresses included in the draft. | | **content** | `{ hash: string; url: string }` | Content hash and URL. | | **fee** | `{ token: string; tokenDecimals: number; setupAmount: bigint; balanceBefore: bigint; balanceAfter: bigint; humanTokenDelta: string }` | Fee and ERC20 balance details for the setup. | | **receipt** | `TransactionReceipt` | Transaction receipt for the creation call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Contract controller (submits tx and pays ERC20 fee) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Counterparties const bob = process.env.BOB_PUBLIC_ADDRESS!; const charlie = process.env.CHARLIE_PUBLIC_ADDRESS!; // 3. Prepare content const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const url = "https://example.com"; const content = `This is a test contract ${Date.now()}`; const contractHash = keccak256(toUtf8Bytes(content)); // 4. Create contract draft const result = await rwa_sdk.cnft.createContract({ contractController: alice, erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, counterparties: [bob, charlie], contractNft: contractNft, contractHash: contractHash, url: url }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Contract controller (submits tx and pays ERC20 fee) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Counterparties const bob = process.env.BOB_PUBLIC_ADDRESS; const charlie = process.env.CHARLIE_PUBLIC_ADDRESS; // 3. Prepare content const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const url = "https://example.com"; const content = `This is a test contract ${Date.now()}`; const contractHash = keccak256(toUtf8Bytes(content)); // 4. Create contract draft const result = await rwa_sdk.cnft.createContract({ contractController: alice, erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, counterparties: [bob, charlie], contractNft: contractNft, contractHash: contractHash, url: url }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'created', contractNft: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a', contractId: '1234567890', contractController: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', counterparties: [ '0xbA9274C766A5961C40bB4a3e0e107699EE9Dab9C', '0x68af027F5AaE3b1B6ff770b87aB7ac360b54ad40' ], content: { hash: '0x...', url: 'https://example.com' }, fee: { token: '0x...', tokenDecimals: 18, setupAmount: 1000000000000000000n, balanceBefore: 1000000000000000000n, balanceAfter: 0n, humanTokenDelta: '1.0' }, receipt: ContractTransactionReceipt { ... } } ``` Note: the contract ID is deterministic based on initiator, counterparties, content hash, and URL. # Get Contract Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/getContract ## `cnft.getContract(GetContract)` Fetch a Contract NFT by contract ID. This is a read-only call. ### GetContract Type Parameters | Parameter | Type | Required | Description | | --------------- | -------- | -------- | ------------------------------ | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractId** | `string` | Required | Contract ID to fetch. | ### Returns | Field | Type | Description | | ------------ | ---------- | ---------------------------------------- | | **contract** | `Contract` | Contract tuple returned by the contract. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 2. Fetch contract (after all counterparties sign) const result = await rwa_sdk.cnft.getContract({ contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 2. Fetch contract (after all counterparties sign) const result = await rwa_sdk.cnft.getContract({ contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { contract: [ '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', [ '0xbA9274C766A5961C40bB4a3e0e107699EE9Dab9C', '0x68af027F5AaE3b1B6ff770b87aB7ac360b54ad40' ], 1234567890n, 'https://example.com' ] } ``` # Get Draft Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/getDraft ## `cnft.getDraft(GetDraft)` Fetch a Contract NFT draft by contract ID. This is a read-only call. ### GetDraft Type Parameters | Parameter | Type | Required | Description | | --------------- | -------- | -------- | ----------------------------------- | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractId** | `string` | Required | Contract ID to fetch the draft for. | ### Returns | Field | Type | Description | | --------- | --------------- | ------------------------------------- | | **draft** | `ContractDraft` | Draft tuple returned by the contract. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 2. Fetch draft const result = await rwa_sdk.cnft.getDraft({ contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Get contract draft const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = "1234567890" // 2. Fetch draft const result = await rwa_sdk.cnft.getDraft({ contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { draft: [ [ '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', [ '0xbA9274C766A5961C40bB4a3e0e107699EE9Dab9C', '0x68af027F5AaE3b1B6ff770b87aB7ac360b54ad40' ], 1234567890n, 'https://example.com' ], [ '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2' ] ] } ``` # Is Blocked Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/isBlocked ## `cnft.isBlocked(IsBlocked)` Check whether a Contract NFT contract is blocked. This is a read-only call. ### IsBlocked Type Parameters | Parameter | Type | Required | Description | | --------------- | -------- | -------- | ------------------------------ | | **contractNft** | `string` | Required | Contract NFT contract address. | ### Returns | Field | Type | Description | | ----------- | --------- | ------------------------------------- | | **blocked** | `boolean` | `true` if blocked, `false` otherwise. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Check blocked state const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const result = await rwa_sdk.cnft.isBlocked({ contractNft }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Check blocked state const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const result = await rwa_sdk.cnft.isBlocked({ contractNft }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { blocked: true } ``` # Is Contract ID Available Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/isContractIdAvailable ## `cnft.isContractIdAvailable(IsContractIdAvailable)` Check whether a Contract ID is available for a Contract NFT. This is a read-only call. ### IsContractIdAvailable Type Parameters | Parameter | Type | Required | Description | | --------------- | -------- | -------- | ------------------------------ | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractId** | `string` | Required | Contract ID to check. | ### Returns | Field | Type | Description | | ------------- | --------- | --------------------------------------------- | | **available** | `boolean` | `true` if available, `false` if already used. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Check availability const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const result = await rwa_sdk.cnft.isContractIdAvailable({ contractNft, contractId: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Check availability const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const result = await rwa_sdk.cnft.isContractIdAvailable({ contractNft, contractId: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { available: true } ``` # Set Blocked Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/setBlocked ## `cnft.setBlocked(SetBlocked)` Set a Contract NFT contract to blocked or unblocked. This sends a transaction from the Contract NFT owner. ### SetBlocked Type Parameters | Parameter | Type | Required | Description | | --------------------- | --------- | -------- | ------------------------------------------------------------------------------------------- | | **contractNftSigner** | `Signer` | Required | Contract NFT owner signer authorized to set blocked state. Must be connected to a provider. | | **contractNft** | `string` | Required | Contract NFT contract address. | | **blocked** | `boolean` | Required | `true` to block, `false` to unblock. | ### Returns | Field | Type | Description | | --------------- | -------------------- | -------------------------------------------- | | **status** | `set` | Status of the operation. | | **contractNft** | `string` | Contract NFT contract address. | | **blocked** | `boolean` | The resulting block state. | | **setBy** | `string` | Address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the update operation. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Contract NFT owner signer const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Contract NFT address const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; // 3. Block the contract const result = await rwa_sdk.cnft.setBlocked({ contractNftSigner: admin, contractNft: contractNft, blocked: true }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Contract NFT owner signer const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Contract NFT address const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; // 3. Block the contract const result = await rwa_sdk.cnft.setBlocked({ contractNftSigner: admin, contractNft: contractNft, blocked: true }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'set', contractNft: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a', blocked: true, setBy: '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', receipt: ContractTransactionReceipt { ... } } ``` # Sign Contract Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/cnft/signContract ## `cnft.signContract(SignContract)` Sign a Contract NFT as a counterparty. This sends a transaction from the counterparty signer. If the final signature is collected, the contract becomes completed. ### SignContract Type Parameters | Parameter | Type | Required | Description | | ---------------------- | -------- | -------- | ------------------------------------------------------------------------------------- | | **counterpartySigner** | `Signer` | Required | Counterparty signer authorized to sign the contract. Must be connected to a provider. | | **contractNft** | `string` | Required | Contract NFT contract address. | | **contractId** | `string` | Required | Contract ID to sign. | ### Returns | Field | Type | Description | | ---------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | **status** | `completed` or `signed` or `mined_unknown` | Status of the signing operation. | | **contractId** | `string` | Contract ID that was signed (from emitted event when available). | | **counterpartySigner** | `string` | Counterparty address that signed (from emitted event when available). | | **receipt** | `TransactionReceipt` | Transaction receipt for the signing call. | | **progress** | `{ collected: number; total: number }` | Optional signature progress when `status` is `signed`. Total includes the initiator (+1). | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Contract controller and counterparties const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); const bob = new Wallet(process.env.BOB_PRIVATE_KEY!, provider); const charlie = new Wallet(process.env.CHARLIE_PRIVATE_KEY!, provider); // 2. Get information from contract creation const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = '1234567890' // 3. Counterparty signs const result = await rwa_sdk.cnft.signContract({ counterpartySigner: bob, contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet, keccak256, toUtf8Bytes } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Contract counterparty const bob = new Wallet(process.env.BOB_PRIVATE_KEY, provider); // 2. Get information from contract creation const contractNft = "0x35D67095A5a6f00CBE288cF744b3efC48de3699a"; const contractId = '1234567890' // 3. Counterparty signs const result = await rwa_sdk.cnft.signContract({ counterpartySigner: bob, contractNft: contractNft, contractId: contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs Signed (more signatures required): ``` Result { status: 'signed', contractId: '1234567890', counterpartySigner: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a', receipt: ContractTransactionReceipt { ... }, progress: { collected: 2, total: 3 } } ``` Completed (final signature collected): ``` Result { status: 'completed', contractId: '1234567890', counterpartySigner: '0x68af027F5AaE3b1B6ff770b87aB7ac360b54ad40', receipt: ContractTransactionReceipt { ... } } ``` Note: If emitted events are not detected, the SDK returns `status: 'mined_unknown'` with `contractId` and `counterpartySigner` set to `'unknown'`. # Add Claim to Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/addClaimToIdentity ## `onchainid.addClaimToIdentity(AddClaimToIdentity)` Add a signed claim to an ONCHAINID identity (calls the identity contract's `addClaim`). ### AddClaimToIdentity Type Parameters | Parameter | Type | Required | Description | | ---------------------- | -------- | -------- | ------------------------------------------------------------------------ | | **subjectIdentity** | `string` | Required | ONCHAINID identity contract address that will receive the claim. | | **identityController** | `Signer` | Required | Signer/wallet that controls the identity, connected to a provider. | | **claim** | `IClaim` | Required | Encoded claim payload: `{ identity, issuer, topic, scheme, data, uri }`. | | **claimSignature** | `string` | Required | `0x`-prefixed hex signature over the claim by the claim issuer. | ### Returns | Field | Type | Description | | ----------- | -------------------- | -------------------------------------------------------------------------------- | | **status** | `added` or `updated` | `'added'` when the claim is new, `'updated'` when it replaces an existing claim. | | **claimId** | `string` | Claim ID emitted by the identity contract. | | **receipt** | `TransactionReceipt` | Transaction receipt of the `addClaim` call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit, type GetIdentity, type IssueKycClaim, type AddClaimToIdentity } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Claim Issuer admin wallet const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY!, provider); // 2. Get User to KYC const getIdentity: GetIdentity = { subject: process.env.ALICE_PUBLIC_ADDRESS! }; const alice = await rwa_sdk.onchainid.getIdentity(getIdentity); // 3. Create claim + signature const issueKycClaim: IssueKycClaim = { claimIssuerSigner: claimIssuer, claimIssuerContract: process.env.CLAIM_ISSUER_CONTRACT_ADDRESS!, subjectIdentity: alice.identity, name: 'Alice', lastName: 'Doe', dateOfBirth: '1990-01-01', placeOfBirth: 'New York', uri: 'https://example.com/kyc' } const { claim, signature } = await rwa_sdk.onchainid.issueKycClaim(issueKycClaim); // 4. Identity owner signs and submits addClaim const aliceSigner = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); const addClaimToIdentity: AddClaimToIdentity = { subjectIdentity: alice.identity, identityController: aliceSigner, claim: claim, claimSignature: signature, } const { receipt, status, claimId } = await rwa_sdk.onchainid.addClaimToIdentity(addClaimToIdentity); console.log('Add claim result:', { status, claimId, receipt }); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Claim Issuer admin wallet const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY, provider); // 2. Get User to KYC const alice = await rwa_sdk.onchainid.getIdentity({ subject: process.env.ALICE_PUBLIC_ADDRESS }); // 3. Create claim + signature const { claim, signature } = await rwa_sdk.onchainid.issueKycClaim({ claimIssuerSigner: claimIssuer, claimIssuerContract: process.env.CLAIM_ISSUER_CONTRACT_ADDRESS, subjectIdentity: alice.identity, name: 'Alice', lastName: 'Doe', dateOfBirth: '1990-01-01', placeOfBirth: 'New York', uri: 'https://example.com/kyc' }); // 4. Identity owner signs and submits addClaim const aliceSigner = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); const { receipt, status, claimId } = await rwa_sdk.onchainid.addClaimToIdentity({ subjectIdentity: alice.identity, identityController: aliceSigner, claim: claim, claimSignature: signature, }); console.log('Add claim result:', { status, claimId, receipt }); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` { status: 'added', claimId: '0x29753f23d65eadcfc30f6988fa876cef5069d80f61802576d029c1272a2c9c4e', receipt: TransactionReceipt { ... hash: '0xabccaf471ad0afa2f059747baeb7f79be3d41ecdaae1beed0bd3d903348b302a', status: 1 } } ``` Notes: * Ensure `identityController` controls the ONCHAINID at `subjectIdentity`. * `claimSignature` must match the exact `claim` payload and issuer. # Create Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/createIdentity ## `onchainid.createIdentity(CreateIdentity)` Create (or fetch if already exists) an ONCHAINID identity for a given EOA (Externally Owned Account where the user controls the keys). If an identity is already associated with a `subject`, it returns that identity address with `status: 'exists'`. ### CreateIdentity Type Parameters | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ------------------------------------------------------------------- | | **idFactoryAdmin** | `Signer` | Required | ID Factory Signer authorized to create identities. | | **subject** | `string` | Required | EOA of the identity the deployed ONCHAINID will be associated with. | | **deploymentSalt** | `string` | Required | Arbitrary string used for deterministic deployment. | ### Returns | Field | Type | Description | | ------------ | --------------------- | ---------------------------------------------------------------------------- | | **status** | `created` or `exists` | `'created'` when a new identity was deployed, `'exists'` if already present. | | **identity** | `string` | ONCHAINID identity contract address that is bound to their EOA. | | **receipt** | `TransactionReceipt` | Transaction receipt when created. Only present when `status` is `'created'`. | ### Usage #### TypeScript ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit, type CreateIdentity } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Admin wallet const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Get Alice public address const alice = process.env.ALICE_PUBLIC_ADDRESS! // 3. Create ONCHAINID Identity params const createIdentity: CreateIdentity = { idFactoryAdmin: admin, subject: alice, deploymentSalt: "identity-" + Date.now().toString() } const result = await rwa_sdk.onchainid.createIdentity(createIdentity); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Admin wallet const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Get Alice public address const alice = process.env.ALICE_PUBLIC_ADDRESS // 3. Create ONCHAINID Identity const result = await rwa_sdk.onchainid.createIdentity({ idFactoryAdmin: admin, subject: alice, deploymentSalt: "identity-" + Date.now().toString() }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs Created: ``` Result { status: 'created', identity: '0x1e747251c5F1A4cDC4CD667536db2949A93aB110', receipt: ContractTransactionReceipt { ... } } ``` Already exists: ``` Result { status: 'exists', identity: '0x1e747251c5F1A4cDC4CD667536db2949A93aB110' } ``` # Get Claim Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/getClaim ## `onchainid.getClaim(GetClaim)` Fetch a claim from an ONCHAINID identity by `claimId`. This is a read-only call to the identity contract. ### GetClaim Type Parameters | Parameter | Type | Required | Description | | ------------------- | -------- | -------- | ------------------------------------------------------------------------- | | **subjectIdentity** | `string` | Required | The ONCHAINID identity contract address to read from. | | **claimId** | `string` | Required | The claim identifier, computed as `keccak256(abi.encode(issuer, topic))`. | Note: The `claimId` is derived from the issuer contract address and the claim topic to uniquely identify a claim on an identity. In Solidity this is `keccak256(abi.encode(_issuer, _topic))`. In ethers you can compute it via `keccak256(new AbiCoder().encode(["address","uint256"], [issuer, topic]))`. ### Returns | Field | Type | Description | | --------- | ------- | ------------------------------------------------------- | | **claim** | `Claim` | The full claim payload read from the identity contract. | ### Usage #### TypeScript ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics, type SDKInit, type GetIdentity, type GetClaim } from '@peaq-network/rwa'; import { JsonRpcProvider, AbiCoder, keccak256 } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Resolve the identity for an EOA (or use a known identity address) const alice = process.env.ALICE_PUBLIC_ADDRESS!; const identityRes = await rwa_sdk.onchainid.getIdentity({ subject: alice } as GetIdentity); if (identityRes.status !== 'found') throw new Error('Identity not found'); const identity = identityRes.identity; // 2. Compute claimId = keccak256(abi.encode(issuer, topic)) const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS!; const topic = ClaimTopics.CT_KYC_APPROVED; const abiCoder = new AbiCoder(); const claimId = keccak256(abiCoder.encode(["address", "uint256"], [issuerContract, topic])); // 3. Fetch claim const result = await rwa_sdk.onchainid.getClaim({ subjectIdentity: identity, claimId } as GetClaim); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics } from "@peaq-network/rwa"; import { JsonRpcProvider, AbiCoder, keccak256 } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Resolve the identity for an EOA (or use a known identity address) const alice = process.env.ALICE_PUBLIC_ADDRESS; const identityRes = await rwa_sdk.onchainid.getIdentity({ subject: alice }); if (identityRes.status !== 'found') throw new Error('Identity not found'); const identity = identityRes.identity; // 2. Compute claimId = keccak256(abi.encode(issuer, topic)) const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; const topic = ClaimTopics.CT_KYC_APPROVED; const abiCoder = new AbiCoder(); const claimId = keccak256(abiCoder.encode(["address", "uint256"], [issuerContract, topic])); // 3. Fetch claim const result = await rwa_sdk.onchainid.getClaim({ subjectIdentity: identity, claimId: claimId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { claim: { topic: ClaimTopics.CT_KYC_APPROVED, scheme: 1, issuer: '0x187EB39e1aF4B9a79936635dBa52984af98464a9', signature: '0x4bab329329ea682e4ac675d9ebab2b8ec597124b98b5085a4e8f83104ce064d4728771567752a75db8ad75552579f0f805913f1f756947f25a8e85d67ce3055e1b', data: '0x252ec8044814d556905cc1587f4a375a2acfe3f84a17d7d104accd32ee25b3b6', uri: 'https://example.com/kyc' } } ``` # Get Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/getIdentity ## `onchainid.getIdentity(GetIdentity)` Fetch the ONCHAINID identity contract address associated with a given EOA (Externally Owned Account). This is a read-only query against the ID Factory; no transaction is sent. ### GetIdentity Type Parameters | Parameter | Type | Required | Description | | ----------- | -------- | -------- | -------------------------------------------------- | | **subject** | `string` | Required | EOA to check for an associated ONCHAINID identity. | ### Returns | Field | Type | Description | | ------------ | ---------------------- | ------------------------------------------------------------------------------- | | **status** | `found` or `not_found` | `'found'` when an identity is associated with the EOA, otherwise `'not_found'`. | | **identity** | `string` | The identity address when found. Only present when `status` is `'found'`. | ### Usage #### TypeScript ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit, type GetIdentity } from '@peaq-network/rwa'; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Alice public address const alice = process.env.ALICE_PUBLIC_ADDRESS!; // 2. Query ONCHAINID Identity const params: GetIdentity = { subject: alice }; const result = await rwa_sdk.onchainid.getIdentity(params); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Alice public address const alice = process.env.ALICE_PUBLIC_ADDRESS // 2. Query ONCHAINID Identity const result = await rwa_sdk.onchainid.getIdentity({ subject: alice }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs Found: ``` Result { status: 'found', identity: '0xF16b0871271C2135b4Ffc374676e74a16aaDC2c9' } ``` Not found: ``` Result { status: 'not_found' } ``` # Issue KYC Claim Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/issueKycClaim ## `onchainid.issueKycClaim(IssueKycClaim)` Generate and sign a KYC claim for an ONCHAINID identity. This does not broadcast a transaction; it returns the encoded claim payload and the issuer's signature that can be submitted or verified off-chain/on-chain by downstream contracts. ### IssueKycClaim Type Parameters | Parameter | Type | Required | Description | | ----------------------- | -------- | -------- | ------------------------------------------------------------------------------------ | | **claimIssuerSigner** | `Signer` | Required | Claim Issuer signer authorized to issue KYC claims. Must be connected to a provider. | | **claimIssuerContract** | `string` | Required | EVM address of the ClaimIssuer contract. | | **subjectIdentity** | `string` | Required | ONCHAINID identity contract address of the subject being KYCed. | | **name** | `string` | Required | First name of the identity owner. | | **lastName** | `string` | Required | Last name of the identity owner. | | **dateOfBirth** | `string` | Required | Date of birth in ISO format `YYYY-MM-DD`. | | **placeOfBirth** | `string` | Required | Place of birth. | | **uri** | `string` | Optional | Optional URI pointing to KYC evidence/metadata. | ### Returns | Field | Type | Description | | ------------- | -------- | ------------------------------------------------------------------------ | | **claim** | `IClaim` | Encoded claim payload: `{ identity, issuer, topic, scheme, data, uri }`. | | **signature** | `string` | Signature over the claim by `claimIssuer`. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit, type GetIdentity, type IssueKycClaim } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Alice EOA const aliceEoa = process.env.ALICE_PUBLIC_ADDRESS // 2. Get Alice identity const getIdentity: GetIdentity = { subject: aliceEoa! }; const alice = await rwa_sdk.onchainid.getIdentity(getIdentity); console.log("Alice Identity", alice); // 3. Get Claim Issuer Admin const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY!, provider); // 4. Get Issuer Contract const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; // 5. Issue signed KYC claim const issueKycClaim: IssueKycClaim = { claimIssuerSigner: claimIssuer, claimIssuerContract: issuerContract!, subjectIdentity: alice.identity, name: "Alice", lastName: "Doe", dateOfBirth: "1990-01-01", placeOfBirth: "New York", uri: "https://example.com/kyc" } const { claim, signature } = await rwa_sdk.onchainid.issueKycClaim(issueKycClaim); console.log("Result", { claim, signature }); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Alice EOA const aliceEoa = process.env.ALICE_PUBLIC_ADDRESS // 2. Get Alice identity const alice = await rwa_sdk.onchainid.getIdentity({ subject: aliceEoa }); console.log("Alice Identity", alice); // 3. Get Claim Issuer Admin const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY, provider); // 4. Get Issuer Contract const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; // 5. Issue signed KYC claim const result = await rwa_sdk.onchainid.issueKycClaim({ claimIssuerSigner: claimIssuer, claimIssuerContract: issuerContract, subjectIdentity: alice.identity, name: "Alice", lastName: "Doe", dateOfBirth: "1990-01-01", placeOfBirth: "New York", uri: "https://example.com/kyc" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Alice Identity { status: 'found', identity: '0x1d0FDE95e971c5c78B6f9c745a8e2791Fe0c962C' } Result { claim: { identity: '0x1d0FDE95e971c5c78B6f9c745a8e2791Fe0c962C', issuer: '0x842d57632954943304441258E94f3f089235022c', topic: ClaimTopics.CT_KYC_APPROVED, scheme: 1, data: '0x252ec8044814d556905cc1587f4a375a2acfe3f84a17d7d104accd32ee25b3b6', uri: 'https://example.com/kyc' }, signature: '0xffd77807790d0e764bcdd2bc7661c6a3e1e016758100f4afc5d6a5f3552455791ddcfd2bd5b705c27eaa66c45ca1c0bf7b53f5a3a533b6023b8bf3f2132a363a1b' } ``` Note: `topic` and `scheme` are set according to the RWA KYC specification. Ensure your `claimIssuer` and `issuerContract` are recognized by your registry/verification flow for successful validation. # Issue Role Claim Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/issueRoleClaim ## `onchainid.issueRoleClaim(IssueRoleClaim)` Generate and sign a Role claim (Machine Regulator or Machine Issuer) for an ONCHAINID identity. This does not broadcast a transaction; it returns the encoded claim payload and the issuer's signature that can be submitted or verified off-chain/on-chain by downstream contracts. ### IssueRoleClaim Type Parameters | Parameter | Type | Required | Description | | ----------------------- | -------- | -------- | ------------------------------------------------------------------------------------- | | **claimIssuerSigner** | `Signer` | Required | Claim Issuer signer authorized to issue Role claims. Must be connected to a provider. | | **claimIssuerContract** | `string` | Required | EVM address of the ClaimIssuer contract. | | **subjectIdentity** | `string` | Required | ONCHAINID identity contract address of the subject. | | **roleTopic** | `number` | Required | Role topic identifier. | | **roleDescription** | `string` | Required | Human-readable role description. | ### Returns | Field | Type | Description | | ------------- | -------- | ------------------------------------------------------------------------ | | **claim** | `IClaim` | Encoded claim payload: `{ identity, issuer, topic, scheme, data, uri }`. | | **signature** | `string` | Signature over the claim by `claimIssuerSigner`. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics, type SDKInit, type GetIdentity, type IssueRoleClaim } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Alice EOA const aliceEoa = process.env.ALICE_PUBLIC_ADDRESS // 2. Get Alice identity const getIdentity: GetIdentity = { subject: aliceEoa! }; const alice = await rwa_sdk.onchainid.getIdentity(getIdentity); console.log("Alice Identity", alice); // 3. Get Claim Issuer Admin const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY!, provider); // 4. Get Issuer Contract const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; // 5. Issue signed Role claim const issueRoleClaim: IssueRoleClaim = { claimIssuerSigner: claimIssuer, claimIssuerContract: issuerContract!, subjectIdentity: alice.identity, roleTopic: ClaimTopics.CT_MNFT_ISSUER, roleDescription: "Machine Issuer" } const { claim, signature } = await rwa_sdk.onchainid.issueRoleClaim(issueRoleClaim); console.log("Result", { claim, signature }); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Alice EOA const aliceEoa = process.env.ALICE_PUBLIC_ADDRESS // 2. Get Alice identity const alice = await rwa_sdk.onchainid.getIdentity({ subject: aliceEoa }); console.log("Alice Identity", alice); // 3. Get Claim Issuer Admin const claimIssuer = new Wallet(process.env.CLAIM_ISSUER_PRIVATE_KEY, provider); // 4. Get Issuer Contract const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; // 5. Issue signed Role claim const result = await rwa_sdk.onchainid.issueRoleClaim({ claimIssuerSigner: claimIssuer, claimIssuerContract: issuerContract, subjectIdentity: alice.identity, roleTopic: ClaimTopics.CT_MNFT_ISSUER, roleDescription: "Machine Issuer" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Alice Identity { status: 'found', identity: '0x1d0FDE95e971c5c78B6f9c745a8e2791Fe0c962C' } Result { claim: { identity: '0x1d0FDE95e971c5c78B6f9c745a8e2791Fe0c962C', issuer: '0x842d57632954943304441258E94f3f089235022c', topic: 7, scheme: 1, data: '0x252ec8044814d556905cc1587f4a375a2acfe3f84a17d7d104accd32ee25b3b6', uri: 'https://issuer-regulator-provider.com/user/verification' }, signature: '0xffd77807790d0e764bcdd2bc7661c6a3e1e016758100f4afc5d6a5f3552455791ddcfd2bd5b705c27eaa66c45ca1c0bf7b53f5a3a533b6023b8bf3f2132a363a1b' } ``` Note: `roleTopic` and `roleDescription` are encoded into the claim data according to the RWA Role specification. Ensure your `claimIssuerSigner` and `claimIssuerContract` are recognized by your registry/verification flow for successful validation. # Remove Claim from Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/identity/removeClaimFromIdentity ## `onchainid.removeClaimFromIdentity(RemoveClaimFromIdentity)` Remove a claim from an ONCHAINID identity by `claimId`. This sends a transaction to the identity contract and requires the identity owner’s signer. ### RemoveClaimFromIdentity Type Parameters | Parameter | Type | Required | Description | | ---------------------- | -------- | -------- | ------------------------------------------------------------------------- | | **subjectIdentity** | `string` | Required | The ONCHAINID identity contract address to modify. | | **identityController** | `Signer` | Required | The signer authorized to remove claims from the identity (controller). | | **claimId** | `string` | Required | The claim identifier, computed as `keccak256(abi.encode(issuer, topic))`. | Note: The `claimId` uniquely identifies a claim for an identity. It is derived from the issuer contract address and the claim topic: in Solidity `keccak256(abi.encode(_issuer, _topic))`. In ethers this can be reproduced with `keccak256(new AbiCoder().encode(["address","uint256"], [issuer, topic]))`. ### Returns | Field | Type | Description | | ----------- | -------------------- | ------------------------------------------------- | | **status** | `removed` | Status of the removal. | | **claimId** | `string` | Claim ID removed from the identity. | | **receipt** | `TransactionReceipt` | The transaction receipt confirming claim removal. | ### Usage #### TypeScript ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics, type SDKInit, type GetIdentity, type RemoveClaimFromIdentity } from '@peaq-network/rwa'; import { JsonRpcProvider, AbiCoder, keccak256, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Resolve the identity for an EOA (or use a known identity address) const alice = process.env.ALICE_PUBLIC_ADDRESS!; const identityRes = await rwa_sdk.onchainid.getIdentity({ subject: alice } as GetIdentity); if (identityRes.status !== 'found') throw new Error('Identity not found'); const identity = identityRes.identity; // 2. Identity owner signer const identityOwner = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 3. Compute claimId = keccak256(abi.encode(issuer, topic)) const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS!; const topic = ClaimTopics.CT_KYC_APPROVED; const abiCoder = new AbiCoder(); const claimId = keccak256(abiCoder.encode(["address", "uint256"], [issuerContract, topic])); // 4. Remove claim from identity const result = await rwa_sdk.onchainid.removeClaimFromIdentity({ subjectIdentity: identity, identityController: identityOwner, claimId } as RemoveClaimFromIdentity); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, ClaimTopics } from "@peaq-network/rwa"; import { JsonRpcProvider, AbiCoder, keccak256, Wallet } from "ethers"; async function main() { // 0. Create rwa_sdk instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Resolve the identity for an EOA (or use a known identity address) const alice = process.env.ALICE_PUBLIC_ADDRESS; const identityRes = await rwa_sdk.onchainid.getIdentity({ subject: alice }); if (identityRes.status !== 'found') throw new Error('Identity not found'); const identity = identityRes.identity; // 2. Identity owner signer const identityOwner = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 3. Compute claimId = keccak256(abi.encode(issuer, topic)) const issuerContract = process.env.CLAIM_ISSUER_CONTRACT_ADDRESS; const topic = ClaimTopics.CT_KYC_APPROVED; const abiCoder = new AbiCoder(); const claimId = keccak256(abiCoder.encode(["address", "uint256"], [issuerContract, topic])); // 4. Remove claim from identity const result = await rwa_sdk.onchainid.removeClaimFromIdentity({ subjectIdentity: identity, identityController: identityOwner, claimId: claimId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` { status: 'removed', claimId: '0x8b8a9d2d3a4d1b0f9d1e3f8c1a6d58e4c1b6d95a2a4a9f2e6d0d7d2b8b1c2d3a', receipt: TransactionReceipt { ... hash: '0xbfb964e21d0a8f227e62752a1a5b7cca95aff0cd992430a6213d06e6ea548b9c', status: 1 } } ``` # Installation & Initialization Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/initialize Install the RWA SDK from npm and set up your project. ## Set up TS ENV ### 1. New Project ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm init -y npm pkg set type=module ``` ### 2. Install packages ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @peaq-network/rwa npm i -D typescript tsx @types/node npm i dotenv ``` ### 3. Add a TypeScript config ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} touch tsconfig.json ``` Add the following into the file: ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": true, "resolveJsonModule": true, "verbatimModuleSyntax": true, "sourceMap": true, "noEmit": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] } ``` ### 4. Update scripts in `package.json`. The `src/index.ts` is the executable file. ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} "scripts": { "dev": "tsx watch src/index.ts", "start": "tsx src/index.ts", "typecheck": "tsc -p tsconfig.json --noEmit" }, ``` ### 5. Create source file ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} mkdir src touch src/index.ts ``` ## Set up JS ENV ### 1. Initialize node env ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm init -y npm pkg set type=module ``` ### 2. Install packages ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @peaq-network/rwa npm i dotenv ``` ### 3. Create source file ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} mkdir src touch src/index.js ``` ## Execute For both TS/JS environments your next steps are: ### Create .env file Next we will create the `.env` file where we store `VARIABLES_LIKE_THIS`. Make sure you add the corresponding variables named in the sdk code into this same file. We also add a gitignore to make sure you don't accidentally post secrets. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} touch .env touch .gitignore ``` Add to `.env`: If you would like to be able to follow the full example walkthrough, please define your `.env` as: ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} HTTPS_BASE_URL="" // PEAQ OWNER Admin ADMIN_PUBLIC_ADDRESS="" ADMIN_PRIVATE_KEY="" // Claim Issuer CLAIM_ISSUER_PUBLIC_ADDRESS="" CLAIM_ISSUER_PRIVATE_KEY="" CLAIM_ISSUER_CONTRACT_ADDRESS="" CLAIM_ISSUER_IDENTITY_ADDRESS="" // Machine Regulator MACHINE_REGULATOR_PUBLIC_ADDRESS="" MACHINE_REGULATOR_PRIVATE_KEY="" // Machine Issuer MACHINE_ISSUER_PUBLIC_ADDRESS="" MACHINE_ISSUER_PRIVATE_KEY="" // Alice ALICE_PUBLIC_ADDRESS="" ALICE_PRIVATE_KEY="" // Bob BOB_PUBLIC_ADDRESS="" BOB_PRIVATE_KEY="" // Charlie CHARLIE_PUBLIC_ADDRESS="" CHARLIE_PRIVATE_KEY="" ``` Add to `.gitignore`: ``` .env ``` ### TS Execution Copy one of the examples with the proper authority into `src/index.ts` for execution and run with: ``` npm start ``` ### JS Execution Copy one of the examples with the proper authority into `src/index.js` for execution and run with: ``` node src/index.js ``` ## `new RWA(opts)` Initialize the peaq RWA SDK for a specific chain. The instance wires module addresses for that chain and exposes module entry points. A Provider is required at initialization and is used for read operations; write operations require an explicit Signer. ### Parameters | Parameter | Type | Required | Description | | ------------ | ---------- | -------- | ------------------------------------------------------------------------------- | | **chainId** | `Chain` | Required | The Implementation Target network. `Chain.AGUNG` (9990) or `Chain.PEAQ` (3338). | | **provider** | `Provider` | Required | An ethers Provider used for read-only calls across modules. | ### Usage #### TypeScript ```Typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL!); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); console.log(rwa_sdk); ``` #### ESM JavaScript Default setup in JS guide with `"type": "module",` set via cmd: `npm pkg set type=module` ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); console.log(rwa_sdk); ``` #### Common JavaScript Optional setup to use **cjs**. Remove `"type": "module",` from your `package.json`. ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const { RWA, Chain } = require('@peaq-network/rwa'); const { JsonRpcProvider } = require('ethers'); const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); console.log(rwa_sdk); ``` # Ensure Machine NFT Allowance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/mnft/ensureMachineNftAllowance ## `mnft.ensureMachineNftAllowance(EnsureMachineNftAllowance)` Ensure the ERC20 allowance for the Machine NFT registration account is sufficient to cover the registration fee for N machines of a given value. If the allowance is insufficient, it submits an approval transaction. ### EnsureMachineNftAllowance Type Parameters | Parameter | Type | Required | Description | | --------------------- | -------- | -------- | -------------------------------------------------------------------- | | **machineController** | `Signer` | Required | Signer that will pay the ERC20 fee. Must be connected to a provider. | | **machineNft** | `string` | Required | Machine NFT contract address. | | **machineValueHuman** | `string` | Required | Machine value in human-readable units (e.g., `"10"`). | | **erc20** | `string` | Required | ERC20 token address used to pay the fee. | | **tokenDecimals** | `number` | Required | ERC20 token decimals. | | **machineCount** | `number` | Required | Number of machines to register. | ### Returns | Field | Type | Description | | --------------------- | ---------------------------------- | -------------------------------------------- | | **status** | `approved` or `already_sufficient` | Whether an approval was sent. | | **machineNft** | `string` | Machine NFT contract address. | | **feeToken** | `string` | ERC20 token address used for fees. | | **feePerMachine** | `bigint` | Fee per machine in token units. | | **requiredAllowance** | `bigint` | Total allowance required for `machineCount`. | | **currentAllowance** | `bigint` | Current allowance after the check/approval. | | **receipt** | `TransactionReceipt` | Only present when `status` is `'approved'`. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine controller (pays ERC20 fee via allowance) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Ensure allowance const result = await rwa_sdk.mnft.ensureMachineNftAllowance({ machineController: alice, machineNft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", machineValueHuman: "10", erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, machineCount: 2 }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine controller (pays ERC20 fee via allowance) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Ensure allowance const result = await rwa_sdk.mnft.ensureMachineNftAllowance({ machineController: alice, machineNft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", machineValueHuman: "10", erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, machineCount: 2 }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'approved', machineNft: '0x5Ca3db1f292f913DDdA8C5385E5391438665463c', feeToken: '0x...', feePerMachine: 10000000000000000000n, requiredAllowance: 20000000000000000000n, currentAllowance: 20000000000000000000n, receipt: ContractTransactionReceipt { ... } } ``` # Get Machine DID Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/mnft/getMachineDid ## `mnft.getMachineDid(GetMachineDid)` Read and deserialize a DID document from a Machine NFT. This is a read-only call. ### GetMachineDid Type Parameters | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | **machineNft** | `string` | Required | Machine NFT contract address. | | **tokenId** | `string` | Required | Machine NFT token ID. | ### Returns | Field | Type | Description | | --------------- | -------- | ------------------------------------------ | | **didDocument** | `Object` | Deserialized DID document for the machine. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get a known Machine NFT and tokenId const machineNft = "0x5Ca3db1f292f913DDdA8C5385E5391438665463c"; const tokenId = "tokenId"; // 2. Read DID document const result = await rwa_sdk.mnft.getMachineDid({ machineNft, tokenId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Get a known Machine NFT and tokenId const machineNft = "0x5Ca3db1f292f913DDdA8C5385E5391438665463c"; const tokenId = "tokenId"; // 2. Read DID document const result = await rwa_sdk.mnft.getMachineDid({ machineNft, tokenId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { didDocument: { id: 'did:peaq:...', controller: '0x...', verification_methods: [ ... ], services: [ ... ], authentications: [ ... ], verifiable_credential: { id: '...', type: 'MachineNft', issuer: '0x...', issuance_date: '...', credential_subject: { ... } } } } ``` # Register Machine NFT Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/mnft/registerMachineNft ## `mnft.registerMachine(RegisterMachine)` Register one or more Machine NFTs to a designated controller. This sends transactions from the Machine Issuer and charges the ERC20 fee from the controller. **The Machine NFT must be funded prior to execution.** ### RegisterMachine Type Parameters | Parameter | Type | Required | Description | | ------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------ | | **machineIssuer** | `Signer` | Required | Machine Issuer signer that submits the mint transactions. Must be connected to a provider. | | **machineNft** | `string` | Required | Machine NFT contract address. | | **machineValueHuman** | `string` | Required | Machine value in human-readable units (e.g., `"10"`). | | **machineControllerAddr** | `string` | Required | EOA address that will control/own the machines. | | **erc20** | `string` | Required | ERC20 token address used to pay the fee. | | **tokenDecimals** | `number` | Required | ERC20 token decimals. | | **salt** | `number` | Required | Salt used to derive the DID document. | | **count** | `number` | Required | Number of machines to register. | ### Returns | Field | Type | Description | | --------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- | | **status** | `issued` | Status of the operation. | | **machineNft** | `string` | Machine NFT contract address. | | **machineIssuer** | `string` | Machine Issuer address that submitted the transaction(s). | | **machineController** | `string` | Machine controller/owner address. | | **machineValue** | `{ human: string; units: bigint; tokenDecimals: number; feeToken: string }` | Machine value details. | | **count** | `number` | Number of machines issued. | | **machines** | `{ machineId: string; did?: string; receipt?: TransactionReceipt }[]` | Per-machine results and receipts. | | **feesPaid** | `bigint` | ERC20 fees paid by the controller. | | **startingBalance** | `bigint` | Controller ERC20 balance before issuance. | | **endingBalance** | `bigint` | Controller ERC20 balance after issuance. | | **humanTokenDelta** | `string` | Human-readable fee delta. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine Issuer wallet const machineIssuer = new Wallet(process.env.MACHINE_ISSUER_PRIVATE_KEY!, provider); // 2. Machine controller (receives NFT and pays ERC20 fee via allowance) const alice = process.env.ALICE_PUBLIC_ADDRESS!; // 3. Register MachineNFT(s) for Alice const result = await rwa_sdk.mnft.registerMachine({ machineIssuer: machineIssuer, machineNft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", machineValueHuman: "10", machineControllerAddr: alice, erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, salt: Math.floor(Math.random() * 10000), count: 2 }); console.log('Result', result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine Issuer wallet const machineIssuer = new Wallet(process.env.MACHINE_ISSUER_PRIVATE_KEY, provider); // 2. Machine controller (receives NFT and pays ERC20 fee via allowance) const alice = process.env.ALICE_PUBLIC_ADDRESS; // 3. Register MachineNFT(s) for Alice const result = await rwa_sdk.mnft.registerMachine({ machineIssuer: machineIssuer, machineNft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", machineValueHuman: "10", machineControllerAddr: alice, erc20: rwa_sdk.getAddresses().erc20.peaq, tokenDecimals: 18, salt: Math.floor(Math.random() * 10000), count: 2 }); console.log('Result', result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'issued', machineNft: '0x5Ca3db1f292f913DDdA8C5385E5391438665463c', machineIssuer: '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', machineController: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', machineValue: { human: '10', units: 10000000000000000000n, tokenDecimals: 18, feeToken: '0x...' }, count: 2, machines: [ { machineId: '1', did: 'did:peaq:...', receipt: ContractTransactionReceipt { ... } }, { machineId: '2', did: 'did:peaq:...', receipt: ContractTransactionReceipt { ... } } ], feesPaid: 20000000000000000000n, startingBalance: 100000000000000000000n, endingBalance: 80000000000000000000n, humanTokenDelta: '20.0' } ``` # Add Machine Issuer Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/addMachineIssuer ## `rwanft.addMachineIssuer(AddMachineIssuer)` Add a Machine Issuer to the PeaqRwaNft contract. This sends a transaction signed by a Machine Regulator. ### AddMachineIssuer Type Parameters | Parameter | Type | Required | Description | | -------------------------- | -------- | -------- | --------------------------------------------------------------------------------------------- | | **machineRegulatorSigner** | `Signer` | Required | Machine Regulator signer authorized to add a Machine Issuer. Must be connected to a provider. | | **newMachineIssuer** | `string` | Required | EOA address of the Machine Issuer to add. | ### Returns | Field | Type | Description | | ----------------- | -------------------- | ---------------------------------------------------------------- | | **status** | `added` | Status of the operation. | | **peaqRwaNft** | `string` | PeaqRwaNft contract address. | | **machineIssuer** | `string` | Machine Issuer address that was added. | | **machineNft** | `string` | Machine NFT contract address associated with the Machine Issuer. | | **addedBy** | `string` | Machine Regulator address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the add operation. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Machine Issuer address to add const machineIssuer = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS!; // 3. Add Machine Issuer const result = await rwa_sdk.rwanft.addMachineIssuer({ machineRegulatorSigner: machineRegulator, newMachineIssuer: machineIssuer }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.MACHINE_REGULATOR_PRIVATE_KEY, provider); // 2. Machine Issuer address to add const machineIssuer = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS; // 3. Add Machine Issuer const result = await rwa_sdk.rwanft.addMachineIssuer({ machineRegulatorSigner: machineRegulator, newMachineIssuer: machineIssuer }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'added', peaqRwaNft: '0x9a7e2c5B4F9bE3C1dA7b5a6F8e2D3c4B5A6F7E8D', machineIssuer: '0x3c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c9D', machineNft: '0x1A2b3C4d5E6f7A8b9C0d1E2f3A4b5C6D7e8F9A0b', addedBy: '0x8F7e6D5c4B3a2D1c0B9a8E7f6D5c4B3A2D1C0b9A', receipt: ContractTransactionReceipt { ... } } ``` Note: the Machine Issuer address typically must already have the appropriate Role claim before being added. # Find Contract NFT Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/findContractNft ## `rwanft.findContractNft(FindContractNft)` Find a ContractNft address with available storage for a given contract ID. This is a read-only call. ### FindContractNft Type Parameters | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ---------------------------------- | | **contractId** | `string` | Required | Contract ID (stringified integer). | ### Returns | Field | Type | Description | | --------------- | -------- | ------------------------------------------------------------------------ | | **contractNft** | `string` | ContractNft address with available storage for the provided contract ID. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Find contract NFT by contract ID const contractId = "1234567890"; const result = await rwa_sdk.rwanft.findContractNft({ contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Find contract NFT by contract ID const contractId = "1234567890"; const result = await rwa_sdk.rwanft.findContractNft({ contractId }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { contractNft: '0x35D67095A5a6f00CBE288cF744b3efC48de3699a' } ``` Note: if the contract ID is already in use, the call will revert with a message similar to `Not available, please contact owner`. # Get Machine Issuers Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/getMachineIssuers ## `rwanft.getMachineIssuers()` Fetch the list of Machine Issuer addresses registered in the PeaqRwaNft contract. This is a read-only call. ### Parameters None. ### Returns | Field | Type | Description | | ------------------ | ---------- | ---------------------------------- | | **machineIssuers** | `string[]` | Array of Machine Issuer addresses. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Machine Issuers const result = await rwa_sdk.rwanft.getMachineIssuers(); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Machine Issuers const result = await rwa_sdk.rwanft.getMachineIssuers(); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { machineIssuers: [ '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2' ] } ``` # Get Machine Regulators Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/getMachineRegulators ## `rwanft.getMachineRegulators()` Fetch the list of Machine Regulator addresses registered in the PeaqRwaNft contract. This is a read-only call. ### Parameters None. ### Returns | Field | Type | Description | | --------------------- | ---------- | ------------------------------------- | | **machineRegulators** | `string[]` | Array of Machine Regulator addresses. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Get Machine Regulators const result = await rwa_sdk.rwanft.getMachineRegulators(); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider: provider }); // 1. Get Machine Regulators const result = await rwa_sdk.rwanft.getMachineRegulators(); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { machineRegulators: [ '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0' ] } ``` # Remove Machine Issuer Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/removeMachineIssuer ## `rwanft.removeMachineIssuer(RemoveMachineIssuer)` Remove a Machine Issuer from the PeaqRwaNft contract. This sends a transaction signed by a Machine Regulator. ### RemoveMachineIssuer Type Parameters | Parameter | Type | Required | Description | | -------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------ | | **machineRegulatorSigner** | `Signer` | Required | Machine Regulator signer authorized to remove a Machine Issuer. Must be connected to a provider. | | **machineIssuer** | `string` | Required | EOA address of the Machine Issuer to remove. | ### Returns | Field | Type | Description | | ----------------- | -------------------- | --------------------------------------------------------- | | **status** | `removed` | Status of the operation. | | **peaqRwaNft** | `string` | PeaqRwaNft contract address. | | **machineIssuer** | `string` | Machine Issuer address that was removed. | | **removedBy** | `string` | Machine Regulator address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the remove operation. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.MACHINE_REGULATOR_PRIVATE_KEY!, provider); // 2. Machine Issuer address to remove const machineIssuer = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS!; // 3. Remove Machine Issuer const result = await rwa_sdk.rwanft.removeMachineIssuer({ machineRegulatorSigner: machineRegulator, machineIssuer: machineIssuer }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.MACHINE_REGULATOR_PRIVATE_KEY, provider); // 2. Machine Issuer address to remove const machineIssuer = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS; // 3. Remove Machine Issuer const result = await rwa_sdk.rwanft.removeMachineIssuer({ machineRegulatorSigner: machineRegulator, machineIssuer: machineIssuer }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'removed', peaqRwaNft: '0x9a7e2c5B4F9bE3C1dA7b5a6F8e2D3c4B5A6F7E8D', machineIssuer: '0x3c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c9D', removedBy: '0x8F7e6D5c4B3a2D1c0B9a8E7f6D5c4B3A2D1C0b9A', receipt: ContractTransactionReceipt { ... } } ``` # Set Machine NFT Block State Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/rwanft/setMachineNftBlockState ## `rwanft.setMachineNftBlockState(SetMachineNftBlockState)` Set the block state of a Machine Issuer address or a Machine NFT contract address in the PeaqRwaNft contract. This sends a transaction signed by a Machine Regulator. ### SetMachineNftBlockState Type Parameters | Parameter | Type | Required | Description | | -------------------------- | --------- | -------- | ----------------------------------------------------------------------------------------------- | | **machineRegulatorSigner** | `Signer` | Required | Machine Regulator signer authorized to update the block state. Must be connected to a provider. | | **issuerOrContractNft** | `string` | Required | EOA address of a Machine Issuer or Machine NFT contract address. | | **blocked** | `boolean` | Required | `true` to block, `false` to unblock. | ### Returns | Field | Type | Description | | -------------- | -------------------- | --------------------------------------------------------- | | **status** | `updated` | Status of the operation. | | **peaqRwaNft** | `string` | PeaqRwaNft contract address. | | **target** | `string` | The address whose block state was updated. | | **blocked** | `boolean` | The resulting block state. | | **updatedBy** | `string` | Machine Regulator address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the update operation. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.MACHINE_REGULATOR_PRIVATE_KEY!, provider); // 2. Target Machine Issuer or Machine NFT contract const target = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS!; // 3. Block target const result = await rwa_sdk.rwanft.setMachineNftBlockState({ machineRegulatorSigner: machineRegulator, issuerOrContractNft: target, blocked: true }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from '@peaq-network/rwa'; import { JsonRpcProvider, Wallet } from 'ethers'; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine Regulator signer const machineRegulator = new Wallet(process.env.MACHINE_REGULATOR_PRIVATE_KEY, provider); // 2. Target Machine Issuer or Machine NFT contract const target = process.env.MACHINE_ISSUER_PUBLIC_ADDRESS; // 3. Block target const result = await rwa_sdk.rwanft.setMachineNftBlockState({ machineRegulatorSigner: machineRegulator, issuerOrContractNft: target, blocked: true }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'updated', peaqRwaNft: '0x9a7e2c5B4F9bE3C1dA7b5a6F8e2D3c4B5A6F7E8D', target: '0x3c1D2e3F4a5B6c7D8e9F0a1B2c3D4e5F6a7B8c9D', blocked: true, updatedBy: '0x8F7e6D5c4B3a2D1c0B9a8E7f6D5c4B3A2D1C0b9A', receipt: ContractTransactionReceipt { ... } } ``` # Approve Vault as Operator Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/approveVaultAsOperator ## `vault.nftApproval(NftApproval)` Approve a Vault as operator for specific Machine/Contract NFT token IDs by calling `approve` per token. This sends transactions from the NFT owner/controller. ### NftApproval Type Parameters | Parameter | Type | Required | Description | | --------------------- | ---------- | -------- | ------------------------------------------------------------------------------- | | **machineController** | `Signer` | Required | Owner/controller signer that grants approvals. Must be connected to a provider. | | **nft** | `string` | Required | Machine NFT or Contract NFT contract address. | | **vault** | `string` | Required | Vault address to approve as operator. | | **tokenIds** | `string[]` | Required | Token IDs to approve. | ### Returns | Field | Type | Description | | ------------------------- | ---------------------- | ----------------------------------------- | | **status** | `approved` | Status of the operation. | | **nft** | `string` | NFT contract address. | | **vault** | `string` | Vault address approved for the token IDs. | | **newlyApprovedTokenIds** | `string[]` | Token IDs approved in this call. | | **receipts** | `TransactionReceipt[]` | Transaction receipts (one per token ID). | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Machine Controller const controller = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Approve the vault for specific MNFT token IDs const result = await rwa_sdk.vault.nftApproval({ machineController: controller, nft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", tokenIds: [ "1262843802665614120367007478296348432923457422026", "880598419457374294774049460835571533031091411284" ] }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Machine Controller const controller = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Approve the vault for specific MNFT token IDs const result = await rwa_sdk.vault.nftApproval({ machineController: controller, nft: "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", tokenIds: [ "1262843802665614120367007478296348432923457422026", "880598419457374294774049460835571533031091411284" ] }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'approved', nft: '0x5Ca3db1f292f913DDdA8C5385E5391438665463c', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', newlyApprovedTokenIds: [ '1262843802665614120367007478296348432923457422026', '880598419457374294774049460835571533031091411284' ], receipts: [ ContractTransactionReceipt { ... }, ContractTransactionReceipt { ... } ] } ``` Notes: * This approves specific token IDs (not `setApprovalForAll`). * For Contract NFTs, pass the Contract NFT address and its token IDs. # Claim Yield Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/claimYield ## `vault.claimYield(ClaimYield)` Claim yield from a vault’s reward distributor. Yield is sent to the claimer. ### ClaimYield Type Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | ---------------------------------------------------------- | | **claimerSigner** | `Signer` | Required | Signer that claims yield. Must be connected to a provider. | | **vault** | `string` | Required | Vault address. | ### Returns | Field | Type | Description | | --------------------- | -------------------- | ------------------------------------------------------------ | | **status** | `claimed` | Status of the operation. | | **vault** | `string` | Vault address. | | **rewardDistributor** | `string` | Reward distributor contract address. | | **claimer** | `string` | Address that submitted the transaction (recipient of yield). | | **receipt** | `TransactionReceipt` | Transaction receipt for the claim call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Claimer signer const bob = new Wallet(process.env.BOB_PRIVATE_KEY!, provider); // 2. Claim yield const result = await rwa_sdk.vault.claimYield({ claimerSigner: bob, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Claimer signer const bob = new Wallet(process.env.BOB_PRIVATE_KEY, provider); // 2. Claim yield const result = await rwa_sdk.vault.claimYield({ claimerSigner: bob, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'claimed', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', rewardDistributor: '0x...', claimer: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', receipt: ContractTransactionReceipt { ... } } ``` # Claim Yield To Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/claimYieldTo ## `vault.claimYieldTo(ClaimYieldTo)` Claim yield from a vault’s reward distributor and send it to a specified address. ### ClaimYieldTo Type Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | --------------------------------------------------------------- | | **claimerSigner** | `Signer` | Required | Signer that submits the claim. Must be connected to a provider. | | **vault** | `string` | Required | Vault address. | | **to** | `string` | Required | Recipient address for the claimed yield. | ### Returns | Field | Type | Description | | --------------------- | -------------------- | --------------------------------------- | | **status** | `claimed` | Status of the operation. | | **vault** | `string` | Vault address. | | **rewardDistributor** | `string` | Reward distributor contract address. | | **claimer** | `string` | Address that submitted the transaction. | | **recipient** | `string` | Address that received the yield. | | **receipt** | `TransactionReceipt` | Transaction receipt for the claim call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Claimer signer const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Recipient of yield const charlie = process.env.CHARLIE_PUBLIC_ADDRESS! // 3. Claim yield to Charlie const result = await rwa_sdk.vault.claimYieldTo({ claimerSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", to: charlie }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Claimer signer const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Recipient of yield const charlie = process.env.CHARLIE_PUBLIC_ADDRESS // 3. Claim yield to Charlie const result = await rwa_sdk.vault.claimYieldTo({ claimerSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", to: charlie }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'claimed', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', rewardDistributor: '0x...', claimer: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', recipient: '0xbA9274C766A5961C40bB4a3e0e107699EE9Dab9C', receipt: ContractTransactionReceipt { ... } } ``` # Create Vault and Token Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/createVaultAndToken ## `vault.createVault(CreateVault)` Create a new Vault and its associated security token using a Vault Factory. This sends a transaction from the vault deployer. ### CreateVault Type Parameters | Parameter | Type | Required | Description | | ----------------------- | ---------- | -------- | ----------------------------------------------------------------------------- | | **vaultDeployer** | `Signer` | Required | Deployer signer authorized to create vaults. Must be connected to a provider. | | **vaultController** | `string` | Required | EOA address that will control the vault. | | **vaultFactory** | `string` | Required | Vault Factory contract address. | | **infoDesk** | `string` | Required | InfoDesk contract address. | | **trustedClaimIssuers** | `string[]` | Required | Addresses of trusted Claim Issuer contracts. | | **tokenName** | `string` | Required | Security token name. | | **tokenSymbol** | `string` | Required | Security token symbol. | | **payoutToken** | `string` | Required | ERC20 token address used for payouts. | ### Returns | Field | Type | Description | | --------------- | -------------------- | ------------------------------------------ | | **status** | `created` | Status of the operation. | | **vault** | `string` | Deployed Vault address. | | **token** | `string` | Deployed security token address. | | **distributor** | `string` | Deployed reward distributor address. | | **receipt** | `TransactionReceipt` | Transaction receipt for the creation call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Vault deployer signer const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Vault controller const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 3. Create Vault const result = await rwa_sdk.vault.createVault({ vaultDeployer: admin, vaultController: alice.address, vaultFactory: "0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6", infoDesk: "0x9f2bF4e338cCC48D1b7021494377907ea4a593F2", trustedClaimIssuers: [process.env.CLAIM_ISSUER_CONTRACT_ADDRESS!], tokenName: "Test Token ABC", tokenSymbol: "ABC", payoutToken: rwa_sdk.getAddresses().erc20.peaq }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Vault deployer signer const admin = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Vault controller const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 3. Create Vault const result = await rwa_sdk.vault.createVault({ vaultDeployer: admin, vaultController: alice.address, vaultFactory: "0x5C5Db5CcF63ed6C11063385070C8FD2C990BFd53", infoDesk: "0x3F2c72Ba389632079DA68Ee13E8b955d69D1B5c1", trustedClaimIssuers: [process.env.CLAIM_ISSUER_CONTRACT_ADDRESS], tokenName: "Test Token ABC", tokenSymbol: "ABC", payoutToken: rwa_sdk.getAddresses().erc20.peaq }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'created', vault: '0x4b76a8F7cdB68a9353c83e18077E6bbC760243B3', token: '0x811247945f5fcBD9068F71298a69e71B2A4Ba66f', distributor: '0x4210D83E736789e361DC96CC07756cb573e23CEd', receipt: ContractTransactionReceipt { ... } } ``` Notes: * The Vault deployer must be authorized in the Vault Factory. * `trustedClaimIssuers` should include Claim Issuer contracts required by compliance. # Deposit Yield Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/depositYield ## `vault.depositYield(DepositYield)` Deposit yield into a vault’s reward distributor. This approves the ERC20 amount and then deposits it. ### DepositYield Type Parameters | Parameter | Type | Required | Description | | ----------------------- | -------- | -------- | ------------------------------------------------------------ | | **depositorSigner** | `Signer` | Required | Signer that deposits yield. Must be connected to a provider. | | **vault** | `string` | Required | Vault address. | | **erc20** | `string` | Required | ERC20 token address used to deposit yield. | | **decimals** | `number` | Required | ERC20 token decimals. | | **humanReadableAmount** | `string` | Required | Amount to deposit in human-readable units (e.g., `"1"`). | ### Returns | Field | Type | Description | | --------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------- | | **status** | `deposited` | Status of the operation. | | **vault** | `string` | Vault address. | | **rewardDistributor** | `string` | Reward distributor contract address. | | **depositor** | `string` | Address that submitted the transaction. | | **token** | `{ address: string; decimals: number }` | ERC20 token details. | | **amount** | `{ human: string; units: bigint }` | Amount details. | | **approval** | `{ status: 'approved'; spender: string; allowanceBefore: bigint; allowanceAfter: bigint }` | Approval details for the reward distributor. | | **receipt** | `TransactionReceipt` | Transaction receipt for the deposit call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Depositor signer const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Deposit yield const result = await rwa_sdk.vault.depositYield({ depositorSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", erc20: rwa_sdk.getAddresses().erc20.peaq, decimals: 18, humanReadableAmount: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Depositor signer const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Deposit yield const result = await rwa_sdk.vault.depositYield({ depositorSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", erc20: rwa_sdk.getAddresses().erc20.peaq, decimals: 18, humanReadableAmount: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'deposited', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', rewardDistributor: '0x...', depositor: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', token: { address: '0x...', decimals: 18 }, amount: { human: '1', units: 1000000000000000000n }, approval: { status: 'approved', spender: '0x...', allowanceBefore: 0n, allowanceAfter: 1000000000000000000n }, receipt: ContractTransactionReceipt { ... } } ``` # Ensure Transfer Fee Allowance Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/ensureTransferFeeAllowance ## `vault.ensureTransferFeeAllowance(EnsureTransferFeeAllowance)` Ensure an ERC20 allowance is set to pay the vault transfer fee for a given token transfer. If the allowance is insufficient, it submits an approval transaction. ### EnsureTransferFeeAllowance Type Parameters | Parameter | Type | Required | Description | | ----------------------- | -------- | -------- | ------------------------------------------------------------------- | | **allowanceSigner** | `Signer` | Required | Signer that pays the transfer fee. Must be connected to a provider. | | **vault** | `string` | Required | Vault address that computes the transfer fee. | | **token** | `string` | Required | Security token address being transferred. | | **erc20** | `string` | Required | ERC20 token address used to pay the fee. | | **transferAmountHuman** | `string` | Required | Transfer amount in human-readable units (e.g., `"2"`). | ### Returns | Field | Type | Description | | -------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | **status** | `already_sufficient` or `approved` | Whether an approval was sent. | | **vault** | `string` | Vault address. | | **feeToken** | `string` | ERC20 token address used for fees. | | **transfer** | `{ token: string; amountHuman: string; amountUnits: bigint; tokenDecimals: number }` | Transfer amount details. | | **fee** | `{ requiredAllowance: bigint; allowanceBefore: bigint; allowanceAfter: bigint }` | Fee allowance details. | | **approvedBy** | `string` | Address that submitted the approval (or current allowance owner). | | **receipt** | `TransactionReceipt` | Only present when `status` is `'approved'`. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Allowance signer (pays transfer fee) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Ensure transfer fee allowance const result = await rwa_sdk.vault.ensureTransferFeeAllowance({ allowanceSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", token: "0x2a6aA5ef4e236aeE4247EAA7B926cd843a95bFc8", erc20: rwa_sdk.getAddresses().erc20.peaq, transferAmountHuman: "2" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Allowance signer (pays transfer fee) const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Ensure transfer fee allowance const result = await rwa_sdk.vault.ensureTransferFeeAllowance({ allowanceSigner: alice, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", token: "0x2a6aA5ef4e236aeE4247EAA7B926cd843a95bFc8", erc20: rwa_sdk.getAddresses().erc20.peaq, transferAmountHuman: "2" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'approved', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', feeToken: '0x...', transfer: { token: '0x2a6aA5ef4e236aeE4247EAA7B926cd843a95bFc8', amountHuman: '2', amountUnits: 2000000000000000000n, tokenDecimals: 18 }, fee: { requiredAllowance: 1000000000000000000n, allowanceBefore: 0n, allowanceAfter: 1000000000000000000n }, approvedBy: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', receipt: ContractTransactionReceipt { ... } } ``` # Mint Security Tokens Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/mintSecurityTokens ## `vault.depositAndMint(DepositAndMint)` Deposit an array of RWA NFTs into the vault and mint the corresponding amount of security tokens. ### DepositAndMint Type Parameters | Parameter | Type | Required | Description | | ------------------- | ---------- | -------- | ---------------------------------------------------------------------------- | | **vaultController** | `Signer` | Required | Vault controller signer that owns the NFTs. Must be connected to a provider. | | **vault** | `string` | Required | Vault address. | | **rwaNfts** | `string[]` | Required | Addresses of the RWA NFT contracts (MNFT/CNFT). | | **tokenIds** | `string[]` | Required | Token IDs of the NFTs to deposit. | | **amount** | `number` | Required | Amount of security tokens to mint. | ### Returns | Field | Type | Description | | -------------- | ---------------------- | -------------------------------------------------- | | **status** | `deposited_and_minted` | Status of the operation. | | **vault** | `string` | Vault address. | | **controller** | `string` | Vault controller address. | | **rwaNfts** | `string[]` | NFT contract addresses provided. | | **tokenIds** | `string[]` | Token IDs deposited. | | **amount** | `number` | Amount of security tokens minted. | | **receipt** | `TransactionReceipt` | Transaction receipt for the deposit and mint call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Vault controller const vaultController = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Deposit NFTs and mint tokens const result = await rwa_sdk.vault.depositAndMint({ vaultController: vaultController, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", rwaNfts: [ "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", "0x35D67095A5a6f00CBE288cF744b3efC48de3699a" ], tokenIds: [ "1262843802665614120367007478296348432923457422026", "880598419457374294774049460835571533031091411284", "110399289532161649501907442204937966168773206671183427730650359857010370852178" ], amount: 10000 }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Vault controller const vaultController = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Deposit NFTs and mint tokens const result = await rwa_sdk.vault.depositAndMint({ vaultController: vaultController, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", rwaNfts: [ "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", "0x5Ca3db1f292f913DDdA8C5385E5391438665463c", "0x35D67095A5a6f00CBE288cF744b3efC48de3699a" ], tokenIds: [ "1262843802665614120367007478296348432923457422026", "880598419457374294774049460835571533031091411284", "110399289532161649501907442204937966168773206671183427730650359857010370852178" ], amount: 10000 }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'deposited_and_minted', vault: '0x4b76a8F7cdB68a9353c83e18077E6bbC760243B3', controller: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', rwaNfts: [ '0x5Ca3db1f292f913DDdA8C5385E5391438665463c', '0x5Ca3db1f292f913DDdA8C5385E5391438665463c', '0x35D67095A5a6f00CBE288cF744b3efC48de3699a' ], tokenIds: [ '1262843802665614120367007478296348432923457422026', '880598419457374294774049460835571533031091411284', '110399289532161649501907442204937966168773206671183427730650359857010370852178' ], amount: 10000, receipt: ContractTransactionReceipt { ... } } ``` Notes: * Ensure the vault has been approved as operator for the provided NFTs by the `vaultController`. * Ensure `rwaNfts` and `tokenIds` arrays align by index. # Pause Token Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/pauseToken ## `vault.pauseToken(PauseToken)` Pause the security token for a vault via the Vault Factory. This sends a transaction from the vault deployer. ### PauseToken Type Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | --------------------------------------------------------------------- | | **vaultDeployer** | `Signer` | Required | Deployer signer authorized to pause. Must be connected to a provider. | | **vaultFactory** | `string` | Required | Vault Factory contract address. | | **vault** | `string` | Required | Vault address whose token will be paused. | ### Returns | Field | Type | Description | | ---------------- | -------------------- | --------------------------------------- | | **status** | `paused` | Status of the operation. | | **vault** | `string` | Vault address whose token was paused. | | **vaultFactory** | `string` | Vault Factory contract address. | | **pausedBy** | `string` | Address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the pause call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Vault deployer signer const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Pause token const result = await rwa_sdk.vault.pauseToken({ vaultDeployer: vaultDeployer, vaultFactory: "0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6", vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Vault deployer signer const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Pause token const result = await rwa_sdk.vault.pauseToken({ vaultDeployer: vaultDeployer, vaultFactory: "0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6", vault: "0x4b76a8F7cdB68a9353c83e18077E6bbC760243B3" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'paused', vault: '0x4b76a8F7cdB68a9353c83e18077E6bbC760243B3', vaultFactory: '0x5C5Db5CcF63ed6C11063385070C8FD2C990BFd53', pausedBy: '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', receipt: ContractTransactionReceipt { ... } } ``` Notes: * The vault deployer must be authorized in the Vault Factory. # Register Identity Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/registerIdentity ## `vault.registerIdentity(RegisterIdentity)` Register an ONCHAINID for a user in the vault's Identity Registry. Must be called by a wallet authorized as an agent/operator for the registry. ### RegisterIdentity Type Parameters | Parameter | Type | Required | Description | | ------------------- | -------- | -------- | --------------------------------------------------------------------------- | | **vaultDeployer** | `Signer` | Required | Authorized agent of the Identity Registry. Must be connected to a provider. | | **vault** | `string` | Required | Vault address whose Identity Registry will be updated. | | **subject** | `string` | Required | User's EOA to be associated with the identity. | | **subjectIdentity** | `string` | Required | ONCHAINID contract address for the user. | | **country** | `string` | Required | Country code for the investor (e.g., `'0'`). | ### Returns | Field | Type | Description | | -------------------- | -------------------- | --------------------------------------------- | | **status** | `registered` | Status of the operation. | | **vault** | `string` | Vault address. | | **identityRegistry** | `string` | Identity Registry contract address. | | **subject** | `string` | User's EOA associated with the identity. | | **subjectIdentity** | `string` | ONCHAINID contract address for the user. | | **country** | `string` | Country code. | | **registeredBy** | `string` | Address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the registration call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Vault deployer (must be an authorized agent) const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Get EOA and identity const alice = await rwa_sdk.onchainid.getIdentity({ subject: process.env.ALICE_PUBLIC_ADDRESS! }); // 3. Register Alice's identity in the vault's Identity Registry const result = await rwa_sdk.vault.registerIdentity({ vaultDeployer: vaultDeployer, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", subject: process.env.ALICE_PUBLIC_ADDRESS!, subjectIdentity: alice.identity, country: '0' }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Vault deployer (must be an authorized agent) const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Get EOA and identity const alice = await rwa_sdk.onchainid.getIdentity({ subject: process.env.ALICE_PUBLIC_ADDRESS }); // 3. Register Alice's identity in the vault's Identity Registry const result = await rwa_sdk.vault.registerIdentity({ vaultDeployer: vaultDeployer, vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a", subject: process.env.ALICE_PUBLIC_ADDRESS, subjectIdentity: alice.identity, country: '0' }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'registered', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', identityRegistry: '0x...', subject: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', subjectIdentity: '0x...', country: '0', registeredBy: '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', receipt: ContractTransactionReceipt { ... } } ``` Notes: * Ensure the user does not already have an identity registered for this vault. * `vaultDeployer` must be configured as an agent of the Identity Registry. # Transfer Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/transfer ## `vault.transfer(Transfer)` Transfer tokens between addresses, scaling the human-readable amount using the token's decimals. ### Transfer Type Parameters | Parameter | Type | Required | Description | | ----------------------- | -------- | -------- | ------------------------------------------------------------------- | | **from** | `Signer` | Required | Sender wallet (must hold balance). Must be connected to a provider. | | **to** | `string` | Required | Recipient address. | | **token** | `string` | Required | Token address. | | **transferAmountHuman** | `string` | Required | Human-readable amount; scaled by token decimals. | ### Returns | Field | Type | Description | | ------------- | ---------------------------------------------------- | ------------------------------------- | | **status** | `transferred` | Status of the operation. | | **token** | `string` | Token address. | | **sender** | `string` | Sender address. | | **recipient** | `string` | Recipient address. | | **amount** | `{ human: string; units: bigint; decimals: number }` | Amount details. | | **receipt** | `TransactionReceipt` | Transaction receipt for the transfer. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Sender const alice = new Wallet(process.env.ALICE_PRIVATE_KEY!, provider); // 2. Recipient const bob = process.env.BOB_PUBLIC_ADDRESS!; // 3. Transfer const result = await rwa_sdk.vault.transfer({ from: alice, to: bob, token: "0x2a6aA5ef4e236aeE4247EAA7B926cd843a95bFc8", transferAmountHuman: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Sender const alice = new Wallet(process.env.ALICE_PRIVATE_KEY, provider); // 2. Recipient const bob = process.env.BOB_PUBLIC_ADDRESS; // 3. Transfer const result = await rwa_sdk.vault.transfer({ from: alice, to: bob, token: "0x2a6aA5ef4e236aeE4247EAA7B926cd843a95bFc8", transferAmountHuman: "1" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'transferred', token: '0x811247945f5fcBD9068F71298a69e71B2A4Ba66f', sender: '0x16cd4D21537eD8F33bE08271A9FA6DCC426709b2', recipient: '0xbA9274C766A5961C40bB4a3e0e107699EE9Dab9C', amount: { human: '1', units: 1000000000000000000n, decimals: 18 }, receipt: ContractTransactionReceipt { ... } } ``` Notes: * The method fetches the token's decimals and scales the provided amount accordingly. * Ensure the sender is allowed to transfer (compliance checks may apply). # Unpause Token Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/vault/unpauseToken ## `vault.unpauseToken(UnpauseToken)` Unpause the security token for a vault via the Vault Factory. This sends a transaction from the vault deployer. ### UnpauseToken Type Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | ----------------------------------------------------------------------- | | **vaultDeployer** | `Signer` | Required | Deployer signer authorized to unpause. Must be connected to a provider. | | **vaultFactory** | `string` | Required | Vault Factory contract address. | | **vault** | `string` | Required | Vault address whose token will be unpaused. | ### Returns | Field | Type | Description | | ---------------- | -------------------- | ---------------------------------------- | | **status** | `unpaused` | Status of the operation. | | **vault** | `string` | Vault address whose token was unpaused. | | **vaultFactory** | `string` | Vault Factory contract address. | | **unpausedBy** | `string` | Address that submitted the transaction. | | **receipt** | `TransactionReceipt` | Transaction receipt of the unpause call. | ### Usage #### TypeScript ```TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain, type SDKInit } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const init: SDKInit = { chainId: Chain.AGUNG, provider: provider }; const rwa_sdk = new RWA(init); // 1. Vault deployer const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY!, provider); // 2. Unpause token const result = await rwa_sdk.vault.unpauseToken({ vaultDeployer: vaultDeployer, vaultFactory: "0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6", vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` #### JavaScript ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { RWA, Chain } from "@peaq-network/rwa"; import { JsonRpcProvider, Wallet } from "ethers"; async function main() { // 0. Create RWA instance and get provider const provider = new JsonRpcProvider(process.env.HTTPS_BASE_URL); const rwa_sdk = new RWA({ chainId: Chain.AGUNG, provider }); // 1. Vault deployer const vaultDeployer = new Wallet(process.env.ADMIN_PRIVATE_KEY, provider); // 2. Unpause Security Tokens const result = await rwa_sdk.vault.unpauseToken({ vaultDeployer: vaultDeployer, vaultFactory: "0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6", vault: "0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a" }); console.log("Result", result); } main().catch((err) => { console.error(err); process.exit(1); }); ``` ### Example outputs ``` Result { status: 'unpaused', vault: '0x907229D0A25A5Bb16F0ff3D890f38Eb4Ad52Ea1a', vaultFactory: '0x7809591A43449Ab57452c56Cf7d95b04Ef9886b6', unpausedBy: '0x8BCfa2e9FC4aCa66fCF36Bcf47646E5Fb8d74BA0', receipt: ContractTransactionReceipt { ... } } ``` Notes: * The vault deployer must be authorized in the Vault Factory. # Common Flow Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/workflows/common-flow ## What this workflow shows This page describes an **end-to-end RWA flow**. It runs in order: 1. **Onboard participants** – Create ONCHAINID identities for Alice, Bob, and Charlie; then attach KYC claims so they can hold compliant tokens. 2. **Asset side** – Alice receives Machine NFTs (via a Machine Issuer) and completes a multi-party Contract NFT with Bob and Charlie. 3. **Vault and token** – Admin creates a vault with Alice as controller and unpauses the security token; then registers Alice, Bob, and Charlie in the vault’s Identity Registry. 4. **Collateralize and mint** – Alice approves the vault to move her Machine NFTs and Contract NFT, deposits them into the vault, and mints security tokens. 5. **Transfers and yield** – Alice transfers tokens to Bob and Charlie; Alice deposits yield into the vault; Bob claims yield (for himself and to Charlie). Throughout the examples you will see `process.env.*` (e.g. `process.env.HTTPS_BASE_URL`). These come from a **`.env` file** you set during [initialization](/peaqchain/sdk-reference/rwa/initialize). Keep this file secret and never commit it. ## Environment variables Use the same variable names as in the SDK reference so you can copy-paste and compare with the test. Example layout: ```js theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // Network RPC URL HTTPS_BASE_URL="" // PEAQ OWNER Admin ADMIN_PUBLIC_ADDRESS="" ADMIN_PRIVATE_KEY="" // Claim Issuer CLAIM_ISSUER_PUBLIC_ADDRESS="" CLAIM_ISSUER_PRIVATE_KEY="" CLAIM_ISSUER_CONTRACT_ADDRESS="" CLAIM_ISSUER_IDENTITY_ADDRESS="" // Machine Regulator MACHINE_REGULATOR_PUBLIC_ADDRESS="" MACHINE_REGULATOR_PRIVATE_KEY="" // Machine Issuer MACHINE_ISSUER_PUBLIC_ADDRESS="" MACHINE_ISSUER_PRIVATE_KEY="" // Alice ALICE_PUBLIC_ADDRESS="" ALICE_PRIVATE_KEY="" // Bob BOB_PUBLIC_ADDRESS="" BOB_PRIVATE_KEY="" // Charlie CHARLIE_PUBLIC_ADDRESS="" CHARLIE_PRIVATE_KEY="" ``` ## Parties at play ### Admin (Framework owner / Implementation Authority) **Who:** In production this is peaq (or the Implementation Authority). In tests it is the wallet you put in `ADMIN_*`. **What they do:** Own the ID Factory and Vault Factory; create ONCHAINID identities for users; create vaults and set the vault controller; unpause vault tokens; register identities in a vault’s Identity Registry so those EOAs can hold and transfer the security token. ### Claim Issuer **Who:** A trusted entity that issues and attests to claims (e.g. KYC). Onboarding is required; peaq approves and adds them to the Trusted Issuers Registry. **What they do:** Issue KYC (and optionally role) claims for identities. The Claim Issuer **contract** address is used by the vault’s compliance; the Claim Issuer **signer** (`CLAIM_ISSUER_PRIVATE_KEY`) signs claims. You need both `CLAIM_ISSUER_CONTRACT_ADDRESS` and the signer in `.env`. ### Machine Regulator **Who:** Authority that decides which addresses may act as Machine Issuers. **What they do:** Add or remove Machine Issuers via the PeaqRwaNft contract; set block state for issuers or Contract NFT contracts. Uses `MACHINE_REGULATOR_*` in the full flow setup (e.g. adding the Machine Issuer before the test runs). ### Machine Issuer **Who:** Entity allowed by the Machine Regulator to register Machine NFTs for a given machine value. **What they do:** Call `mnft.registerMachine` to mint Machine NFTs to a **machine controller** (e.g. Alice). The controller pays the ERC20 fee (after setting allowance via `mnft.ensureMachineNftAllowance`). Uses `MACHINE_ISSUER_*`. ### Alice (Asset owner / Vault controller) **Who:** In this flow, the main asset owner and vault controller. **What they do:** Get an identity and KYC; receive Machine NFTs from the Machine Issuer; create a Contract NFT as controller and have Bob and Charlie sign; become the vault controller when Admin creates the vault; approve the vault for her MNFT and CNFT token IDs; deposit those NFTs and mint security tokens; transfer tokens to Bob and Charlie; deposit yield into the vault. ### Bob and Charlie (Investors / Counterparties) **Who:** Participants who will hold security tokens and (in this flow) sign the Contract NFT as counterparties. **What they do:** Get identities and KYC; sign the Contract NFT created by Alice; get registered in the vault’s Identity Registry; receive token transfers from Alice; claim yield from the vault (Bob claims for himself and can claim to Charlie via `claimYieldTo`). ## Flow (step-by-step) Each step links to the SDK reference for that operation. Replace placeholders (e.g. Alice vs Bob) where the doc says “use this address/signer”. 1. **Create identities**\ [Create Identity](/peaqchain/sdk-reference/rwa/identity/createIdentity) for Alice, Bob, and Charlie. Use `idFactoryAdmin: admin`, `subject: ALICE_PUBLIC_ADDRESS` (or Bob / Charlie), and a unique `deploymentSalt`. 2. **Add KYC claims**\ [Add claim to identity](/peaqchain/sdk-reference/rwa/identity/addClaimToIdentity) for each of Alice, Bob, and Charlie. Use the Claim Issuer signer and contract; each identity owner signs `addClaimToIdentity`. 3. **Register Machine NFTs for Alice**\ [Ensure allowance](/peaqchain/sdk-reference/rwa/mnft/ensureMachineNftAllowance) then [Register machine](/peaqchain/sdk-reference/rwa/mnft/registerMachineNft). Use `machineIssuer` (Machine Issuer signer), `machineControllerAddr: alice.address`, and the same used in your deployment. Record the `machineIds` as these will be needed later when approving and minting. 4. **Create and complete a Contract NFT**\ [Create contract](/peaqchain/sdk-reference/rwa/cnft/createContract) (Alice as controller, Bob and Charlie as counterparties). Then [Sign contract](/peaqchain/sdk-reference/rwa/cnft/signContract) as Bob and as Charlie until status is `completed`. Save the `contractId` that is generated as it will be needed for signing and approval/minting later. 5. **Create vault and unpause token**\ [Create vault](/peaqchain/sdk-reference/rwa/vault/createVaultAndToken) (Admin as `vaultDeployer`, Alice as `vaultController`). Set the vault factory and info desk as the same contracts in your deployed framework. Make sure to write down the addresses for the vault, token, and distributor. Then [Unpause token](/peaqchain/sdk-reference/rwa/vault/unpauseToken) for that vault. 6. **Register identities for the vault**\ [Register identity](/peaqchain/sdk-reference/rwa/vault/registerIdentity) for Alice, Bob, and Charlie in the vault’s Identity Registry (Admin as `vaultDeployer`). 7. **Approve vault for NFTs**\ [NFT approval](/peaqchain/sdk-reference/rwa/vault/approveVaultAsOperator): approve the vault for the Machine NFT token IDs, then for the Contract NFT contract and its token ID (Alice as `machineController`). 8. **Deposit and mint**\ [Deposit and mint](/peaqchain/sdk-reference/rwa/vault/mintSecurityTokens): Alice deposits the same Machine NFTs and Contract NFT token IDs and mints the chosen amount of security tokens. 9. **Transfer tokens**\ [Ensure transfer fee allowance](/peaqchain/sdk-reference/rwa/vault/ensureTransferFeeAllowance) then [Transfer](/peaqchain/sdk-reference/rwa/vault/transfer) from Alice to Bob and from Alice to Charlie (use the vault’s security token address from step 5). 10. **Yield (optional)**\ [Deposit yield](/peaqchain/sdk-reference/rwa/vault/depositYield) (e.g. Alice deposits). [Claim yield](/peaqchain/sdk-reference/rwa/vault/claimYield) (e.g. Bob claims). [Claim yield to](/peaqchain/sdk-reference/rwa/vault/claimYieldTo) (e.g. Bob claims to Charlie). # Machine Issuer Flow Source: https://docs.peaq.xyz/peaqchain/sdk-reference/rwa/workflows/machine-issuer-flow This workflow shows how to **add a new Machine Issuer** so that an address can call `mnft.registerMachine` for a given Machine NFT contract. ## What “adding a Machine Issuer” means * The **Machine Regulator** (e.g. Framework Owner) approves an EOA as a **Machine Issuer**. * That EOA must have an ONCHAINID identity with a **Machine Issuer role claim** (`CT_MNFT_ISSUER`) issued by a trusted Claim Issuer. * The regulator then registers that EOA in the **PeaqRwaNft** contract via `rwanft.addMachineIssuer`. After that, the address can register machines (mint Machine NFTs) for machine controllers. ## Prerequisites * **Machine Regulator** signer (e.g. `MACHINE_REGULATOR_PRIVATE_KEY`) — will issue the role claim and call `addMachineIssuer`. * **Claim Issuer** contract address (`CLAIM_ISSUER_CONTRACT_ADDRESS`) — the same Claim Issuer used for KYC; its signer can issue role claims (in the test the admin is both regulator and claim issuer). * **Candidate Machine Issuer** — the EOA you want to turn into a Machine Issuer (e.g. Alice). They must already have an **ONCHAINID identity**. Create one first with [Create Identity](/peaqchain/sdk-reference/rwa/identity/createIdentity) if needed. ## Steps ### 1. Ensure the candidate has an identity Resolve the candidate’s ONCHAINID identity (create it first if they don’t have one): * [Get Identity](/peaqchain/sdk-reference/rwa/identity/getIdentity) with `subject: candidateAddress`. * If `status === 'not_found'`, [Create Identity](/peaqchain/sdk-reference/rwa/identity/createIdentity) for that address (ID Factory admin signs), then call Get Identity again. ### 2. Issue the Machine Issuer role claim The **Claim Issuer** (or the same signer acting as claim issuer) issues a role claim for the candidate’s identity with topic `CT_MNFT_ISSUER`: * [Issue Role Claim](/peaqchain/sdk-reference/rwa/identity/issueRoleClaim) with: * `claimIssuerSigner`: Machine Regulator (or your Claim Issuer signer). * `claimIssuerContract`: `CLAIM_ISSUER_CONTRACT_ADDRESS`. * `subjectIdentity`: the candidate’s identity address from step 1. * `roleTopic`: `ClaimTopics.CT_MNFT_ISSUER` (e.g. `7` — use the value from your SDK’s `ClaimTopics` enum). * `roleDescription`: e.g. `'Machine Issuer'`. You get back `{ claim, signature }`. ### 3. Add the claim to the candidate’s identity The **identity owner** (the candidate) must add the claim to their identity: * [Add Claim to Identity](/peaqchain/sdk-reference/rwa/identity/addClaimToIdentity) with: * `identityController`: candidate’s signer (e.g. Alice’s wallet). * `subjectIdentity`: candidate’s identity address. * `claim`: the claim from step 2. * `claimSignature`: the signature from step 2. ### 4. Register the Machine Issuer in PeaqRwaNft The **Machine Regulator** adds the candidate’s EOA to the PeaqRwaNft contract: * [Add Machine Issuer](/peaqchain/sdk-reference/rwa/rwanft/addMachineIssuer) with: * `machineRegulatorSigner`: Machine Regulator wallet. * `newMachineIssuer`: candidate’s EOA address (e.g. `alice.address`). Result includes `status: 'added'`, `machineIssuer`, `machineNft` (the contract created for this issuer), and `receipt`. ### 5. (Optional) Verify * [Get Machine Issuers](/peaqchain/sdk-reference/rwa/rwanft/getMachineIssuers) before and after step 4 to confirm the new address appears in the list. # What is peaq chain Source: https://docs.peaq.xyz/peaqchain/what-is-peaq The home chain of the peaq Trust Layer, where the peaqOS contracts live. peaq chain is the home chain of the peaq Trust Layer — the decentralized infrastructure layer underneath peaqOS. It is an L1 blockchain where the peaqOS contracts are deployed; their state mirrors out to satellite chains so other ecosystems can read it locally. Most teams building on peaqOS do not need to interact with peaq chain directly, since the peaqOS SDK abstracts it. This section covers protocol-layer work: deploying your own smart contracts, running a node, staking, and legacy peaqID, RBAC, and UMT primitives that predate peaqOS. New to peaq? The [peaq overview](/home) explains how peaqOS and peaq chain fit together. The opinionated Machine Financial Passport flow. What 95% of teams should use. Solidity, node operations, staking, and the legacy peaq SDK primitives. ## When to use peaqOS vs peaq chain | I want to... | Use | | :------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------- | | Give a machine a cross-chain identity | [peaqOS Activate](/peaqos/functions/activate) | | Query a machine's credit rating from another chain | [peaqOS MCR API](/peaqos/api-reference/overview) | | Submit revenue events for a fleet | [peaqOS SDK (proxy operator)](/peaqos/guides/proxy-operator-fleet) | | Delegate spending to an AI agent | [peaqOS Scale](/peaqos/functions/scale) | | Deploy a Solidity contract | [peaq chain (Build tab)](/peaqchain/build/basic-operations/smart-contracts/deploy-smart-contract) | | Run a validator | [peaq chain (Node operations)](/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/introduction) | | Write to the DID pallet directly | [peaq SDK (legacy)](/peaqchain/sdk-reference/javascript/did-operations) | | Use on-chain RBAC primitives | [peaq SDK (legacy)](/peaqchain/sdk-reference/javascript/rbac-operations/role) | | Fractionalize a machine as an RWA | [peaqOS Tokenize](/peaqos/functions/tokenize) + [peaq chain RWA SDK](/peaqchain/sdk-reference/rwa/initialize) | **Both SDKs coexist.** `@peaqos/peaq-os-sdk` is the opinionated path through the Machine Financial Passport flow. `@peaq-network/sdk` gives full access to peaq chain primitives (DID pallet operations, RBAC, Storage, UMT, chain-level transactions). Most teams use peaqOS SDK; drop to peaq SDK for primitives peaqOS does not cover. ## What you'll find in this section * [**Build** reference](/peaqchain/build/getting-started/install-peaq-sdk): Solidity deployments, gas, wallets, event listening, indexers, precompiles, node operations, account abstraction, and ERC-8004 at the contract layer. * [**SDK reference**](/peaqchain/sdk-reference/home): legacy `@peaq-network/sdk` (JavaScript + Python), the Robotics SDK (Python + ROS 2), and the RWA SDK for fractional ownership primitives. * [**Tokenomics**](/peaqchain/learn/tokenomics): \$PEAQ supply, inflation schedule, validator rewards, and staking economics. # GET /machine/{did} Source: https://docs.peaq.xyz/peaqos/api-reference/get-machine GET https://mcr.peaq.xyz/machine/{did} Return the full machine profile for a DID, with response fields that vary by data visibility setting. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /machine/{did} ``` Returns the full machine profile for a DID. The response includes identity, MCR score, bond status, and data visibility-dependent fields. This endpoint shares its response builder with [GET /metadata/](/peaqos/api-reference/get-metadata); both return the same shape. ## Path parameters Machine DID (`did:peaq:0x...`) or raw EVM address (`0x...`) ## Response **200 OK -- base fields (always present)** | Field | Type | Description | | :------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `schema_version` | string | Always `"1.0"` | | `name` | string | `"Machine #"` if an NFT token exists, otherwise `"Machine (no NFT)"` | | `peaqos.machine_id` | integer | On-chain machine ID | | `peaqos.did` | string | Machine DID | | `peaqos.operator` | string or null | Operator DID from the DID document. `null` if not set. | | `peaqos.mcr` | string | Letter rating: `AAA`, `AA`, `A`, `BBB`, `BB`, `B`, `NR`, or `Provisioned` | | `peaqos.mcr_score` | integer | MCR score 0-100 | | `peaqos.bond_status` | string | `"bonded"` or `"unbonded"` | | `peaqos.negative_flag` | boolean | `true` when the AdminFlags contract has a negative-flag timestamp set for this machine. Stays `true` for as long as the timestamp is recorded on-chain, regardless of whether the 180-day MCR scoring penalty is still in effect. | | `peaqos.event_count` | integer | Total on-chain event count | | `peaqos.data_visibility` | string | `"private"`, `"onchain"`, or `"public"`. Defaults to `"private"` if the DID document has no `data_visibility` attribute or contains an unrecognised value. | | `peaqos.documentation_url` | string or null | Public documentation URL from the DID document | ### Additional fields by data visibility The `data_visibility` value on the machine's DID document determines which additional fields appear in the `peaqos` object. #### `data_visibility == "private"` | Field | Type | Description | | :---------------- | :----- | :-------------------------------------------------------------------------------------------------------- | | `peaqos.data_api` | string | The machine's data API URL from the DID document. Only present when a `data_api` attribute is configured. | #### `data_visibility == "onchain"` | Field | Type | Description | | :------------------ | :---- | :--------------------------------------------------------------------------------------------------------- | | `peaqos.event_data` | array | Up to the first 100 events for this machine (oldest first by on-chain index), parsed from on-chain storage | Each element in `event_data`: | Field | Type | Description | | :---------------- | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event_type` | integer | `0` = revenue, `1` = activity | | `origin_value` | integer | Non-negative ISO 4217 minor-unit integer (e.g. cents for USD/HKD, whole units for JPY/KRW). Revenue: raw partner-submitted amount in `origin_currency`. Activity: arbitrary non-negative integer. | | `timestamp` | integer | Unix timestamp | | `trust_level` | integer | `0` = self-reported, `1` = on-chain verifiable, `2` = hardware-signed | | `metadata` | object | Parsed JSON metadata from the event. Falls back to `{"raw": "..."}` if the stored metadata is not valid JSON. | | `origin_currency` | string | **Revenue only.** ISO 4217 currency code in upper-case. The API normalises against the 25-fiat supported list: USD, HKD, JPY, CNY, KRW, SGD, TWD, THB, PHP, MYR, IDR, VND, INR, EUR, GBP, CHF, SEK, NOK, DKK, PLN, CAD, AUD, NZD, MXN, BRL. Codes outside this set are echoed back as-is with `amount_status: "unsupported_currency"`. When the partner omits the currency on-chain, the server defaults to `"USD"`. Activity events omit this field. | | `origin_subunit` | integer or null | **Revenue only.** Divisor that converts `origin_value` to a display amount: `100` for two-decimal currencies (USD, HKD, EUR, etc.) and `1` for the zero-decimal codes JPY, KRW, and VND. `null` when `origin_currency` is outside the supported whitelist (`amount_status: "unsupported_currency"`). | | `usd_value` | integer or null | **Revenue only.** FX-normalized value in **USD cents**. Display = `usd_value / 100`. `null` when `amount_status != "ok"` (FX failed); consumers must branch on `amount_status` before displaying. | | `usd_subunit` | integer | **Revenue only.** Always `100`. USD's minor unit divisor; the field exists to make the cents convention explicit on the wire. | | `amount_status` | string | **Revenue only.** `"ok"` (FX succeeded; `usd_value` is meaningful), `"unsupported_currency"` (currency not in MCR FX whitelist), or `"fx_unavailable"` (FX feed degraded or stale). Non-`"ok"` rows score conservatively and surface `mcr_degraded: true` on [`/mcr/{did}`](/peaqos/api-reference/get-mcr). | #### `data_visibility == "public"` The server fetches live data from the machine's `data_api` URL with a 5-second timeout. The URL is validated before the request is made: private IP ranges, loopback addresses, link-local addresses, reserved ranges, multicast addresses, and known cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`) are blocked. Redirects are not followed. Responses must be valid JSON objects no larger than 1 MB. On success: | Field | Type | Description | | :-------------------- | :----- | :------------------------------------------------------- | | `peaqos.partner_data` | object | The JSON object returned by the machine's `data_api` URL | On failure: | Field | Type | Description | | :-------------------------- | :----- | :----------------------------- | | `peaqos.partner_data_error` | string | One of the fixed strings below | Possible `partner_data_error` values: | Value | Cause | | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | | `"data_api not configured"` | DID has no `data_api` attribute | | `"blocked: unsafe URL"` | URL scheme is not http/https, hostname is missing, host is in the blocked list, or DNS resolves to a private/loopback/link-local/reserved/multicast IP | | `"blocked: malformed URL"` | URL could not be parsed | | `"fetch failed"` | Network error, timeout, redirect, or non-2xx HTTP response | | `"response too large"` | Response exceeded 1 MB | | `"invalid JSON response"` | Body was not valid JSON, or the parsed value was not a JSON object | ## Error responses | Status | `detail` | Condition | | :----- | :---------------------------------- | :----------------------------------------------------------- | | 400 | `"Empty DID"` | Path parameter resolves to an empty string | | 400 | `"Invalid Ethereum address format"` | DID or address does not match the expected hex format | | 404 | `"Machine DID not found"` | DID address has no `machineId` attribute in the DID registry | | 404 | `"Machine not registered"` | Machine ID is not present in the IdentityRegistry contract | | 503 | `"Service not initialised"` | Server started without contract addresses | | 503 | `"Chain unavailable"` | Any chain call failed | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/machine/did:peaq:0xabc1230000000000000000000000000000000001" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch( `${PEAQOS_MCR_API_URL}/machine/did:peaq:0xabc1230000000000000000000000000000000001` ); const data = await response.json(); console.log(data.peaqos.mcr, data.peaqos.data_visibility); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get( f"{PEAQOS_MCR_API_URL}/machine/did:peaq:0xabc1230000000000000000000000000000000001" ) data = response.json() print(data["peaqos"]["mcr"], data["peaqos"]["data_visibility"]) ``` **Response (private visibility)** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "schema_version": "1.0", "name": "Machine #7", "peaqos": { "machine_id": 1, "did": "did:peaq:0xabc1230000000000000000000000000000000001", "operator": "did:peaq:0xoperator0000000000000000000000000000001", "mcr": "BB", "mcr_score": 45, "bond_status": "bonded", "negative_flag": false, "event_count": 12, "data_visibility": "private", "documentation_url": null, "data_api": "https://machine.example.com/api/data" } } ``` **Response (onchain visibility)** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "schema_version": "1.0", "name": "Machine #7", "peaqos": { "machine_id": 1, "did": "did:peaq:0xabc1230000000000000000000000000000000001", "operator": null, "mcr": "B", "mcr_score": 30, "bond_status": "bonded", "negative_flag": false, "event_count": 5, "data_visibility": "onchain", "documentation_url": null, "event_data": [ { "event_type": 0, "origin_value": 20000, "timestamp": 1711900000, "trust_level": 2, "metadata": { "job_id": "abc" }, "origin_currency": "HKD", "origin_subunit": 100, "usd_value": 2564, "usd_subunit": 100, "amount_status": "ok" } ] } } ``` **Response (public visibility, fetch failure)** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "schema_version": "1.0", "name": "Machine (no NFT)", "peaqos": { "machine_id": 3, "did": "did:peaq:0xdef4560000000000000000000000000000000003", "operator": null, "mcr": "NR", "mcr_score": 0, "bond_status": "unbonded", "negative_flag": false, "event_count": 0, "data_visibility": "public", "documentation_url": null, "partner_data_error": "blocked: unsafe URL" } } ``` ## Related endpoints * [GET /mcr/](/peaqos/api-reference/get-mcr) returns a focused MCR score response with revenue trend and summary. * [GET /metadata/](/peaqos/api-reference/get-metadata) returns the same response shape, resolved by NFT token ID instead of DID. * [GET /machines/](/peaqos/api-reference/get-machine-card) returns the peaqOS Machine Card resolved by numeric machine ID. # GET /machines/{machine_id} Source: https://docs.peaq.xyz/peaqos/api-reference/get-machine-card GET https://mcr.peaq.xyz/machines/{machine_id} Return a peaqOS Machine Card for a machine identified by its numeric machine ID. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /machines/{machine_id} ``` Returns a peaqOS Machine Card for a registered machine. The Machine Card follows the ERC-8004 agent registration file pattern, restyled with peaqOS terminology, and includes identity, services, operator, bond status, and registry references. ## Path parameters On-chain machine ID (not an NFT token ID and not a DID) ## Response **200 OK** | Field | Type | Description | | :-------------------------------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | peaqOS registration type: `"peaqos:registration:v1"` | | `name` | string | `"Machine #"` | | `description` | string | Fixed string: `"peaqOS machine"` | | `did` | string | Machine DID derived from the machine wallet address (`did:peaq:`) | | `active` | boolean | Always `true` for registered machines | | `services` | array | List of service endpoints. Contains one `"web"` entry if a `data_api` attribute is configured on the DID document; otherwise empty. | | `services[].name` | string | `"web"` | | `services[].endpoint` | string | Value of the `data_api` DID attribute | | `data_visibility` | string | `"private"`, `"onchain"`, or `"public"`. Defaults to `"private"` if the DID document has no `data_visibility` attribute. | | `documentation_url` | string | Documentation URL from the DID document. Empty string if not set. | | `operator` | string or null | Operator DID from the DID document | | `bond_status` | string | `"bonded"` or `"unbonded"` | | `event_count` | integer | Total on-chain event count | | `registrations` | array | List of registry references (always one entry) | | `registrations[].type` | string | `"peaqos:registration:v1"` | | `registrations[].machineId` | integer | The machine's on-chain ID | | `registrations[].machineRegistry` | string | CAIP-compatible registry identifier: `eip155::` | The `chainId` in `machineRegistry` is read from the connected RPC node at server startup (default `3338` for peaq mainnet). The `registryAddress` is the IdentityRegistry contract address. ## Error responses | Status | `detail` | Condition | | :----- | :--------------------------- | :----------------------------------------------------------------- | | 404 | `"Machine not found"` | Machine ID does not exist in the IdentityRegistry contract | | 404 | `"Machine wallet not found"` | The machine's wallet address is the zero address (`0x0000...0000`) | | 422 | (FastAPI validation error) | `machine_id` is less than 1 or not an integer | | 503 | `"Service not initialised"` | Server started without contract addresses | | 503 | `"Chain unavailable"` | Any chain or DID call failed | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/machines/42" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch(`${PEAQOS_MCR_API_URL}/machines/42`); const data = await response.json(); console.log(data.name, data.bond_status, data.registrations); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get(f"{PEAQOS_MCR_API_URL}/machines/42") data = response.json() print(data["name"], data["bond_status"], data["registrations"]) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "type": "peaqos:registration:v1", "name": "Machine #42", "description": "peaqOS machine", "did": "did:peaq:0xabc1230000000000000000000000000000000001", "active": true, "services": [ { "name": "web", "endpoint": "https://machine.example.com/api/data" } ], "data_visibility": "private", "documentation_url": "https://example.com/docs", "operator": "did:peaq:0xoperator0000000000000000000000000000001", "bond_status": "bonded", "event_count": 150, "registrations": [ { "type": "peaqos:registration:v1", "machineId": 42, "machineRegistry": "eip155:3338:0xIdentityRegistryAddress0000000000000001" } ] } ``` ## Related endpoints * [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile with MCR score and data visibility-dependent fields. * [GET /mcr/](/peaqos/api-reference/get-mcr) returns the detailed MCR score, revenue trend, and revenue summary. * [GET /metadata/](/peaqos/api-reference/get-metadata) returns NFT metadata for a MachineNFT token. # GET /machine/{key}/monetization Source: https://docs.peaq.xyz/peaqos/api-reference/get-machine-monetization GET https://mcr.peaq.xyz/machine/{key}/monetization Read a machine's current monetization state. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /machine/{key}/monetization ``` Reads a machine's current monetization state. Public: no signature required. ## Path parameters Machine DID (`did:peaq:0x...`) or a decimal machine ID (e.g. `42`). ## Response **200 OK** | Field | Type | Description | | :----------- | :-------------- | :----------------------------------------------------------------------------- | | `machine_id` | integer | The resolved numeric machine ID | | `status` | string | `"PENDING"`, `"OPTED_IN"`, or `"OPTED_OUT"` | | `signer` | string or null | Address that authorized the current state; `null` when `status` is `"PENDING"` | | `updated_at` | integer or null | Unix seconds of the last applied change; `null` when `status` is `"PENDING"` | `PENDING` is the state of a machine that has never opted in or out (no record stored), distinct from `OPTED_OUT` (it opted in before and then opted out). `signer` and `updated_at` are `null` only in the `PENDING` case. ## Error responses Errors use a coded envelope: `{"detail": {"code", "message"}}`. Branch on `code`, never on `message`. | Status | `code` | Condition | | :----- | :------------------ | :------------------------------------------------------ | | 400 | `INVALID_REQUEST` | Unparseable `key`, or machine ID out of the int64 range | | 404 | `MACHINE_NOT_FOUND` | The `key` does not resolve to an existing machine | | 503 | `CHAIN_UNAVAILABLE` | An on-chain read failed while resolving the machine | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/machine/42/monetization" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch( `${PEAQOS_MCR_API_URL}/machine/42/monetization` ); const data = await response.json(); console.log(data.status); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get(f"{PEAQOS_MCR_API_URL}/machine/42/monetization") print(response.json()["status"]) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "machine_id": 42, "status": "OPTED_IN", "signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "updated_at": 1712000004 } ``` ## Related endpoints * [PUT /machine//monetization](/peaqos/api-reference/put-machine-monetization) sets the state with a signed request. * [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile. # GET /mcr/{did} Source: https://docs.peaq.xyz/peaqos/api-reference/get-mcr GET https://mcr.peaq.xyz/mcr/{did} Compute and return the Machine Credit Rating for a single machine DID. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /mcr/{did} ``` Returns the Machine Credit Rating (MCR) score, letter rating, bond status, event counts, revenue summary, and trend for a single machine. The server paginates the full event history in batches of 100 to compute the score. Results are cached in memory with a TTL configurable via the `MCR_CACHE_TTL` env var (defaults to 3600 seconds; set `MCR_CACHE_TTL=0` to disable caching). Responses are not cached when `mcr_degraded` is `true`: the server retries upstream FX sources on the next request rather than pinning a conservative score. ## Path parameters Machine DID (`did:peaq:0x...`) or raw EVM address (`0x...`). ## Response **200 OK** | Field | Type | Description | | :-------------------------- | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `did` | string | The DID as supplied in the request path | | `machine_id` | integer | On-chain machine ID resolved from the DID | | `mcr_score` | integer | MCR score 0-100. `0` when the rating is `Provisioned` (not enough history to score) or `NR` (unbonded). | | `mcr` | string | Letter rating: `AAA`, `AA`, `A`, `BBB`, `BB`, `B`, `NR`, or `Provisioned` | | `mcr_degraded` | boolean | `true` if any event in the score window came from a degraded FX path (stale snapshot or upstream FX outage). Distinguishes a conservative score caused by infra degradation from a machine with no revenue history. Degraded responses bypass the cache so the next request retries fresh FX. | | `bond_status` | string | `"bonded"` or `"unbonded"` | | `negative_flag` | boolean | `true` when the AdminFlags contract has a negative-flag timestamp set for this machine (and the value is in the plausible range 2020-01-01 ≤ ts ≤ now+1 day). The MCR scoring penalty only applies while the flag is within its active window, but this field stays `true` for as long as the timestamp is recorded on-chain. | | `event_count` | integer | Total events stored on-chain for this machine | | `revenue_event_count` | integer | Number of revenue events (event\_type 0) over the machine's full event history. | | `activity_event_count` | integer | Number of activity events (event\_type 1) over the machine's full event history. | | `revenue_trend` | string | `"up"`, `"stable"`, `"down"`, or `"insufficient"` (returned when there isn't enough revenue history to compute a trend). | | `total_revenue` | integer | Sum of qualifying revenue-event values, in **USD cents**. Only events meeting the per-event qualifying threshold are summed; daily aggregation for the MCR score uses a separate qualifying threshold. | | `average_revenue_per_event` | number | Mean value per qualifying revenue event, in **USD cents**, rounded to 2 decimals. Divides only by events that meet the per-event qualifying threshold, so the divisor can be smaller than `revenue_event_count`. `0` if no qualifying events. | | `last_updated` | integer or null | Unix timestamp of the most recent event. `null` if no events exist. | ### Rating values Possible values of `mcr`: `AAA`, `AA`, `A`, `BBB`, `BB`, `B`, `NR`, or `Provisioned`. Unbonded machines return a rating of `NR` with `mcr_score: 0`. Bonded machines that have not yet accumulated enough history return a rating of `Provisioned` with `mcr_score: 0`. ## Error responses | Status | `detail` | Condition | | :----- | :---------------------------------- | :----------------------------------------------------------- | | 400 | `"Empty DID"` | Path parameter resolves to an empty string | | 400 | `"Invalid Ethereum address format"` | DID or address does not match the expected hex format | | 404 | `"Machine DID not found"` | DID address has no `machineId` attribute in the DID registry | | 404 | `"Machine not registered"` | Machine ID is not present in the IdentityRegistry contract | | 503 | `"Service not initialised"` | Server started without contract addresses | | 503 | `"Chain unavailable"` | RPC call failed during any chain read | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc1230000000000000000000000000000000001" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch( `${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc1230000000000000000000000000000000001` ); const data = await response.json(); console.log(data.mcr_score, data.mcr); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get( f"{PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc1230000000000000000000000000000000001" ) data = response.json() print(data["mcr_score"], data["mcr"]) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "did": "did:peaq:0xabc1230000000000000000000000000000000001", "machine_id": 1, "mcr_score": 45, "mcr": "BB", "mcr_degraded": false, "bond_status": "bonded", "negative_flag": false, "event_count": 12, "revenue_event_count": 7, "activity_event_count": 5, "revenue_trend": "stable", "total_revenue": 35000, "average_revenue_per_event": 5000.0, "last_updated": 1711900000 } ``` ## Related endpoints * [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile including data visibility-dependent fields. * [GET /operator//machines](/peaqos/api-reference/get-operator-machines) returns MCR scores for all machines under an operator. * [GET /metadata/](/peaqos/api-reference/get-metadata) returns the same profile shape as `/machine/{did}`, resolved by NFT token ID. # GET /metadata/{token_id} Source: https://docs.peaq.xyz/peaqos/api-reference/get-metadata GET https://mcr.peaq.xyz/metadata/{token_id} Return NFT metadata for a MachineNFT token, intended as an ERC-721 tokenURI target. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /metadata/{token_id} ``` Returns NFT metadata for a MachineNFT token. The server looks up the machine ID from the token ID via the MachineNFT contract, then resolves the machine owner address and builds the same response as [GET /machine/](/peaqos/api-reference/get-machine). This endpoint is designed to serve as an ERC-721 `tokenURI` target. ## Path parameters NFT token ID (not the machine ID) ## Response **200 OK** The response shape is identical to [GET /machine/](/peaqos/api-reference/get-machine). All base fields and data visibility-dependent fields apply. See that page for the full field reference. | Field | Type | Description | | :------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `schema_version` | string | Always `"1.0"` | | `name` | string | `"Machine #"` when the token round-trips, or `"Machine (no NFT)"` if the on-chain NFT lookup returns 0 | | `peaqos.machine_id` | integer | On-chain machine ID resolved from the token | | `peaqos.did` | string | Machine DID derived from the owner address (`did:peaq:`) | | `peaqos.operator` | string or null | Operator DID from the DID document | | `peaqos.mcr` | string | Letter rating: `AAA`, `AA`, `A`, `BBB`, `BB`, `B`, `NR`, or `Provisioned` | | `peaqos.mcr_score` | integer | MCR score 0-100 | | `peaqos.bond_status` | string | `"bonded"` or `"unbonded"` | | `peaqos.negative_flag` | boolean | `true` when the AdminFlags contract has a negative-flag timestamp set for this machine. Stays `true` for as long as the timestamp is recorded on-chain, regardless of whether the 180-day MCR scoring penalty is still in effect. | | `peaqos.event_count` | integer | Total on-chain event count | | `peaqos.data_visibility` | string | `"private"`, `"onchain"`, or `"public"` | | `peaqos.documentation_url` | string or null | Public documentation URL from the DID document | Additional fields (`data_api`, `event_data`, `partner_data`, `partner_data_error`) depend on `data_visibility`. See [GET /machine/](/peaqos/api-reference/get-machine) for details. ## Error responses | Status | `detail` | Condition | | :----- | :-------------------------- | :--------------------------------------------------------------------- | | 404 | `"Token not found"` | Token ID does not map to any machine in the MachineNFT contract | | 404 | `"Machine not registered"` | The token's machine ID is not present in the IdentityRegistry contract | | 422 | (FastAPI validation error) | `token_id` is less than 1 or not an integer | | 503 | `"Service not initialised"` | Server started without contract addresses | | 503 | `"Chain unavailable"` | Any chain or DID call failed | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/metadata/7" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch(`${PEAQOS_MCR_API_URL}/metadata/7`); const data = await response.json(); console.log(data.name, data.peaqos.mcr); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get(f"{PEAQOS_MCR_API_URL}/metadata/7") data = response.json() print(data["name"], data["peaqos"]["mcr"]) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "schema_version": "1.0", "name": "Machine #7", "peaqos": { "machine_id": 42, "did": "did:peaq:0xabc1230000000000000000000000000000000001", "operator": "did:peaq:0xoperator0000000000000000000000000000001", "mcr": "B", "mcr_score": 30, "bond_status": "bonded", "negative_flag": false, "event_count": 5, "data_visibility": "onchain", "documentation_url": null, "event_data": [ { "event_type": 0, "origin_value": 1500, "timestamp": 1711900000, "trust_level": 1, "metadata": {}, "origin_currency": "USD", "origin_subunit": 100, "usd_value": 1500, "usd_subunit": 100, "amount_status": "ok" } ] } } ``` ## Related endpoints * [GET /machine/](/peaqos/api-reference/get-machine) returns the same response shape, resolved by DID instead of token ID. * [GET /mcr/](/peaqos/api-reference/get-mcr) returns a focused MCR response with revenue trend and summary. * [GET /machines/](/peaqos/api-reference/get-machine-card) returns the peaqOS Machine Card resolved by numeric machine ID. # GET /operator/{did}/machines Source: https://docs.peaq.xyz/peaqos/api-reference/get-operator-machines GET https://mcr.peaq.xyz/operator/{did}/machines List all machines registered under an operator DID, with paginated MCR scores. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /operator/{did}/machines ``` Returns a paginated list of machines registered under an operator DID, with a full MCR score for each machine. The MCR score is computed from the machine's full event history (not bond-only). Machines that are not registered in the IdentityRegistry contract are silently excluded. The server reads the operator's `machines` DID attribute and paginates over the full list. Pagination works past 100 machines using `offset` and `limit`; the underlying DID-attribute storage is bounded by the peaq DID precompile (\~2560 bytes), so very large fleets should iterate `offset` rather than expect an unbounded single response. ## Path parameters Operator DID (`did:peaq:0x...`) or raw EVM address (`0x...`) ## Query parameters Number of machines to skip before the returned page. Must be `>= 0`. Maximum number of machines to return per page. Range `1-20`. ## Response **200 OK** | Field | Type | Description | | :------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `operator_did` | string | The operator DID as supplied in the request path | | `machines` | array | List of machines registered to this operator (for the current page) | | `machines[].did` | string | Machine DID derived from the machine wallet address (`did:peaq:`) | | `machines[].machine_id` | integer | On-chain machine ID | | `machines[].mcr_score` | integer | MCR score 0-100 | | `machines[].mcr` | string | Letter rating: `AAA`, `AA`, `A`, `BBB`, `BB`, `B`, `NR`, or `Provisioned` | | `machines[].negative_flag` | boolean | `true` when the AdminFlags contract has a negative-flag timestamp set for this machine. Stays `true` regardless of whether the 180-day MCR scoring penalty is still active. | | `pagination` | object | Pagination metadata | | `pagination.offset` | integer | The offset used for this page | | `pagination.limit` | integer | The limit used for this page | | `pagination.total` | integer | Number of valid machine IDs in the operator's `machines` DID attribute, after integer validation but before registration filtering. Page slices may be smaller than `limit` because unregistered machines are dropped silently. | An operator with no machines returns `200` with an empty `machines` array and `pagination.total` of `0`. ## Error responses | Status | `detail` | Condition | | :----- | :---------------------------------- | :---------------------------------------------------- | | 400 | `"Empty DID"` | Path parameter resolves to an empty string | | 400 | `"Invalid Ethereum address format"` | DID or address does not match the expected hex format | | 422 | (FastAPI validation error) | `offset` or `limit` fails validation constraints | | 503 | `"Service not initialised"` | Server started without contract addresses | | 503 | `"Chain unavailable"` | Any chain or DID call failed | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/operator/did:peaq:0xoperator0000000000000000000000000000001/machines?offset=0&limit=10" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch( `${PEAQOS_MCR_API_URL}/operator/did:peaq:0xoperator0000000000000000000000000000001/machines?offset=0&limit=10` ); const data = await response.json(); console.log(data.machines.length, data.pagination.total); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get( f"{PEAQOS_MCR_API_URL}/operator/did:peaq:0xoperator0000000000000000000000000000001/machines", params={"offset": 0, "limit": 10}, ) data = response.json() print(len(data["machines"]), data["pagination"]["total"]) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "operator_did": "did:peaq:0xoperator0000000000000000000000000000001", "machines": [ { "did": "did:peaq:0xabc1230000000000000000000000000000000001", "machine_id": 1, "mcr_score": 45, "mcr": "BB", "negative_flag": false }, { "did": "did:peaq:0xdef4560000000000000000000000000000000002", "machine_id": 2, "mcr_score": 0, "mcr": "NR", "negative_flag": false } ], "pagination": { "offset": 0, "limit": 10, "total": 2 } } ``` ## Related endpoints * [GET /mcr/](/peaqos/api-reference/get-mcr) returns detailed MCR data for a single machine, including revenue trend and summary. * [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile with data visibility-dependent fields. # GET /solana/mcr/{solana_addr} Source: https://docs.peaq.xyz/peaqos/api-reference/get-solana-mcr GET https://mcr.peaq.xyz/solana/mcr/{solana_addr} Resolve a Solana address to its bound peaq machine and return the MCR scorecard, or the operator's fleet. ## Endpoints ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /solana/mcr/{solana_addr} GET /solana/operator/{solana_addr}/machines?offset=0&limit=20 ``` Both endpoints key the existing MCR reads by a **Solana address**. The server resolves the address through the peaq binding registry on Solana mainnet (program `GrsCoPkeLUAoXjuwPfCcuch77Chco1Gx95zCCQNYGqG6`, one `getAccountInfo` read of the reverse-lookup account at `finalized` commitment) to the bound peaq EVM identity, then serves the same payload as [GET /mcr/\{did}](/peaqos/api-reference/get-mcr) or [GET /operator/\{did}/machines](/peaqos/api-reference/get-operator-machines). Scoring inputs always come from peaq; Solana is used for identity resolution only. No chain writes. A machine or operator binds its Solana wallet to its peaq address with a dual-signed `bind` instruction on that program (a permissionless, trustless instruction: no peaq key is involved). The SDK and CLI do not expose the binding write yet; peaq runs it for partner fleets. See [Omni-chain: Solana](/peaqos/concepts/omni-chain#solana). ## Path parameters Base58-encoded 32-byte Solana public key. ## Query parameters (operator endpoint only) Zero-based offset into the operator's machine list. Page size, 1 to 20. ## Response **200 OK**: the [GET /mcr/\{did}](/peaqos/api-reference/get-mcr#response) payload (or the operator listing) plus two fields: | Field | Type | Description | | :--------------- | :----- | :--------------------------------------------------------------------------- | | `solana_address` | string | The requested base58 address | | `binding` | object | `evm_addr` (the bound peaq address), `state` (`"active"`), `nonce` (integer) | ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "did": "did:peaq:0xcc9688aee5c2a9e48897afe46c7b6524383d6f13", "machine_id": 445, "mcr_score": 0, "mcr": "Provisioned", "mcr_degraded": false, "bond_status": "bonded", "negative_flag": false, "event_count": 0, "revenue_event_count": 0, "activity_event_count": 0, "revenue_trend": "insufficient", "total_revenue": 0, "average_revenue_per_event": 0, "last_updated": null, "solana_address": "2hga7WjszaFHAkiUGngLvY16ebtkrKwCeTSGYv9uzK8p", "binding": { "evm_addr": "0xcc9688aee5c2a9e48897afe46c7b6524383d6f13", "state": "active", "nonce": 0 } } ``` An operator with no machines returns an empty `machines` list, not an error. ## Resolution errors | Condition | HTTP | `detail` | | :--------------------------------------------------------- | :--- | :----------------------- | | Not a valid base58 public key | 400 | `invalid_address` | | No binding record, or a record created but never activated | 404 | `not_bound` | | Binding revoked | 410 | `binding_revoked` | | Resolution budget exhausted | 429 | `rate_limited` | | Solana RPC unreachable or malformed reply | 503 | `solana_rpc_unavailable` | | Binding account malformed | 500 | `binding_data_invalid` | Once resolution succeeds, every downstream error is the EVM endpoint's own. Settled outcomes (bound, `not_bound`, `binding_revoked`) are cached for 60 seconds, so a just-activated or just-revoked binding can lag by up to one minute. ## Examples ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "https://mcr.peaq.xyz/solana/mcr/2hga7WjszaFHAkiUGngLvY16ebtkrKwCeTSGYv9uzK8p" curl "https://mcr.peaq.xyz/solana/operator/2hga7WjszaFHAkiUGngLvY16ebtkrKwCeTSGYv9uzK8p/machines?offset=0&limit=20" ``` These endpoints serve Tokenomics 1.0 machines (address DIDs). The Economics 2.0 MCR server at `mcr-20.peaq.xyz` (live since 2026-09-05) does not expose the `/solana/*` routes. Treat them as 1.0 surface. # GET /health and GET /ready Source: https://docs.peaq.xyz/peaqos/api-reference/health GET https://mcr.peaq.xyz/health Liveness and readiness probes for monitoring the peaqOS MCR API server. ## GET /health ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /health ``` A lightweight liveness probe. Returns `200` whenever the process is running. Does not check chain connectivity or contract readiness. ### Response **200 OK** | Field | Type | Description | | :------- | :----- | :---------------------------------------- | | `status` | string | Always `"ok"` when the process is running | ### Error responses None. This endpoint always returns `200` while the server process is alive. ### Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/health" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch(`${PEAQOS_MCR_API_URL}/health`); const data = await response.json(); console.log(data.status); // "ok" ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get(f"{PEAQOS_MCR_API_URL}/health") data = response.json() print(data["status"]) # "ok" ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "status": "ok" } ``` *** ## GET /ready ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} GET /ready ``` A readiness probe. Verifies that the RPC node is reachable and all six contracts (IdentityRegistry, IdentityStaking, EventRegistry, MachineNFT, DID registry, AdminFlags) are callable. Returns `200` when all checks pass and `503` when any check fails. ### Response **200 OK (all contracts ready)** | Field | Type | Description | | :---------------------------- | :------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | string | `"ready"` | | `rpc_connected` | boolean | `true` when the RPC node is reachable | | `contracts.identity_registry` | boolean | IdentityRegistry contract callable | | `contracts.identity_staking` | boolean | IdentityStaking contract callable | | `contracts.event_registry` | boolean | EventRegistry contract callable | | `contracts.machine_nft` | boolean | MachineNFT contract callable | | `contracts.admin_flags` | boolean | AdminFlags contract callable. **Returns `true` when the contract address is not configured.** AdminFlags is optional, so the readiness probe skips the call and reports ready. | | `contracts.did_registry` | boolean | DID precompile callable | **503 Service Unavailable (one or more checks failed)** The response body has the same shape as the 200 response, but `status` is `"not_ready"` and one or more boolean fields are `false`. | Status | Condition | | :----- | :----------------------------------------------------------------- | | 503 | Any contract unreachable, RPC failure, or services not initialised | Two failure modes produce a 503: * **Services not initialised** (server started without `IDENTITY_REGISTRY_ADDRESS`): every field is `false`, including `rpc_connected` and `contracts.admin_flags`. * **Initialised but a check failed** (RPC down, contract unreachable, etc.): only the failing fields are `false`. When `ADMIN_FLAGS_ADDRESS` is unset, `contracts.admin_flags` is **`true`**: AdminFlags is optional, the probe skips it, and that field never blocks readiness. ### Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl "${PEAQOS_MCR_API_URL}/ready" ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const response = await fetch(`${PEAQOS_MCR_API_URL}/ready`); const data = await response.json(); if (response.ok) { console.log("Service ready"); } else { console.log("Not ready:", data.contracts); } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import requests response = requests.get(f"{PEAQOS_MCR_API_URL}/ready") data = response.json() if response.status_code == 200: print("Service ready") else: print("Not ready:", data["contracts"]) ``` **Response (healthy)** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "status": "ready", "rpc_connected": true, "contracts": { "identity_registry": true, "identity_staking": true, "event_registry": true, "machine_nft": true, "admin_flags": true, "did_registry": true } } ``` **Response (not ready)** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "status": "not_ready", "rpc_connected": false, "contracts": { "identity_registry": false, "identity_staking": false, "event_registry": false, "machine_nft": false, "admin_flags": false, "did_registry": false } } ``` ## Related endpoints * [API reference overview](/peaqos/api-reference/overview) lists all available endpoints and common error patterns. # Machine Markets: Skills, services, search Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-discovery Browse the skill registry, the partner service catalogue, and run machine-aware market search on the peaqOS Machine Markets API. Skills are the shared schema. Services are concrete provider instances from those schemas. Search ranks services against machine context and requirements. See the [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) for base path, auth model, common envelopes, and error codes. ## Types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type SkillSummary = { key: string; providerKey: string; label: string; description: string; serviceTypes: ServiceType[]; operations: string[]; executionMode: ExecutionMode; integrationStatus: IntegrationStatus; }; type MarketService = { id: string; skillKey: string; providerKey: string; // peaqOS adapter or gateway key companyName: string; // underlying company (Tether, Google, Anthropic, ...) serviceType: ServiceType; name: string; description: string; operations: string[]; pricing: { mode: "provider-defined" | "fixed" | "quote-required"; amount?: number | null; currency?: string | null; rate?: string | null; // e.g. "per_request", "per_1m_tokens" notes: string[]; }; payment: MarketOrder["payment"]; // copy of the rail recommendation endpoints: Array<{ label?: string | null; operation?: string | null; url?: string | null; method?: string | null; // HTTP method when applicable path?: string | null; providerName?: string | null; key?: string | null; protocols: string[]; // e.g. ["x402"], ["mpp"] networks: string[]; // e.g. ["base", "solana"] }>; endpoint?: { // legacy single-endpoint alias for endpoints[0] label?: string | null; operation?: string | null; url?: string | null; method?: string | null; path?: string | null; providerName?: string | null; key?: string | null; protocols: string[]; networks: string[]; } | null; consumption: { // canonical flow paths for this service adapterSetupPath: string; serviceDetailPath: string; marketSearchPath: string; orderPath: string; paymentIntentPath: string | null; paymentProofPath: string | null; orderExecutePath: string; confirmPath: string; directExecutePath: string | null; // set for free read-only services requiredContext: string[]; requiredRequestFields: string[]; optionalRequestFields: string[]; paymentFields: string[]; instructions: string[]; }; executionMode: ExecutionMode; integrationStatus: IntegrationStatus; region: string; capabilities: string[]; handoff?: { label: string; url: string; notes: string[]; } | null; metadata: Record; }; type CatalogService = MarketService & { providerId: string; providerLabel: string; authType: "none" | "bearer" | "api-key" | "wallet" | "local"; accessMode: "public" | "credentialed" | "partner-approved" | "whitelisted" | "local"; distribution: "peaq-native" | "clawhub" | "openapi" | "mcp" | "docs-only"; maturity: "reference" | "beta" | "production"; kind: "runtime" | "reference"; freshnessSeconds: number; requiresHumanEnablement: boolean; referenceUrl?: string; artifactUrl?: string; }; type MarketQuote = { id: string; serviceId: string; skillKey: string; providerKey: string; executionMode: ExecutionMode; integrationStatus: IntegrationStatus; operation: string; score: number; reasons: string[]; pricing: { mode: "provider-defined" | "fixed" | "quote-required"; amount: number | null; currency: string | null; rate: string | null; notes: string[]; }; payment: { defaultRail: "not-required" | "x402" | "vault-stripe" | "escrow" | "offchain-record" | "external" | "onchain-escrow"; supportedRails: MarketQuote["payment"]["defaultRail"][]; required: boolean; provider: string | null; chain: string | null; token: string | null; walletAddress: string | null; externalUrl: string | null; notes: string[]; }; expiresAt: string; // 15 minutes from search creation }; type MarketSearch = { id: string; machineId: string; agentPairingId: string | null; request: Record; // original search body, provider credentials redacted normalizedTask: { serviceType: ServiceType; operation?: string; requiredCapabilities: string[]; region?: string; allowExternalHandoff: boolean; requireNativeExecution: boolean; }; status: "completed" | "no_match"; quotes: MarketQuote[]; createdAt: string; }; ``` ## Skills ### `GET /skills` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListSkillsResponse = ListResponse; ``` ### `GET /skills/:skillKey` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetSkillResponse = ItemResponse; ``` ### `GET /skills/:skillKey/manifest` Returns the full skill manifest, including per-operation input/output contracts. Use this when an SDK or integrator needs the schema for a skill without resolving a market service first. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type OperationFieldSpec = { key: string; type: string; required?: boolean; description: string; example?: unknown; defaultFrom?: string; // e.g. "service.metadata.feedId" enum?: string[]; fields?: OperationFieldSpec[]; }; type OperationContract = { operation: string; summary?: string; inputFields: OperationFieldSpec[]; outputFields: OperationFieldSpec[]; exampleInput?: Record; exampleOutput?: unknown; notes?: string[]; }; type SkillManifest = { key: string; providerKey: string; label: string; summary: string; listing: { direction: SkillDirection; scope: SkillScope; source: SkillSource; listedBy: string; lastVerifiedAt: string | null; }; distribution: SkillDistribution; maturity: SkillMaturity; inputs: OperationFieldSpec[]; outputs: OperationFieldSpec[]; access: { accessMode: SkillAccessMode; requiredEnv: string[]; requiresHumanEnablement: boolean; notes: string[]; }; usage: { operations: string[]; referenceUrl?: string; artifactUrl?: string; handoff?: ExternalHandoff; }; operationContracts?: OperationContract[]; }; type GetSkillManifestResponse = ItemResponse; ``` `operationContracts` also surfaces on `MarketQuote.operationContract` (single, optional) and on `MarketService.operationContracts` (full list) so downstream code can build a request without a second roundtrip. ## Service catalog ### `GET /catalog` Returns the normalised backend service catalogue used by discovery and market service listing. `companyName` is the underlying service company or organisation; `providerKey` remains the peaqOS adapter or gateway. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListCatalogServicesQuery = { limit?: number; cursor?: string; }; type ListCatalogServicesResponse = ListResponse; ``` ## Adapter setup catalog ### `GET /market/adapter-credential-stack` Public endpoint that feeds the frontend "adapter setup" cards (the rich provider tiles on robotic.sh and the peaq app's Services page). Data is backend-owned and repo-backed; the public API does not write. Adding or updating an adapter requires a PR to `peaqos-orchestration` that adds a registry entry and passes the metadata completeness tests; the PR template lives at `.github/PULL_REQUEST_TEMPLATE/adapter-setup-catalog.md`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListAdapterServiceListingsQuery = { providerKey?: string; skillKey?: string; serviceType?: ServiceType; limit?: number; cursor?: string; }; type ListAdapterServiceListingsResponse = ListResponse; type AdapterServiceListing = { providerKey: string; label: string; displayName: string; companyName: string; // underlying company or organisation description: string; category: | "inference" | "vision" | "speech" | "navigation" | "positioning" | "translation" | "web" | "onchain" | "compute" | "storage"; chains: Array<"base" | "solana" | "peaq" | "sui">; price: string; robotTypes: Array<"drones" | "amrs" | "humanoids" | "evs" | "arms" | "sensors">; serviceTypes: ServiceType[]; skillKeys: string[]; setupUrl: string; referenceUrl: string; serviceCatalogPath: string; // fetch endpoint-level services for this adapter paymentRails: string[]; authModes: string[]; // e.g. ["bearer", "api-key", "wallet"] setupComplexity: "low" | "medium" | "high"; displayOrder: number; requiresCredentials: boolean; requiredFields: string[]; optionalFields: string[]; nativeExecutionAvailableWithCredentials: boolean; nativeExecutionNote: string; operatorAction: string; agentInstructions: string[]; }; ``` Adapter registry on the peaq-managed orchestrator (2026-09-04, in `displayOrder`): `akash`, `acurast`, `walrus`, `arcium`, `world-id`, `geodnet`, `naver-maps`, `data-go-kr`, `aethir`, `home-assistant`, `qvac`, `pay-sh`, `agentic-market`. The list changes as providers land; treat `GET /market/adapter-credential-stack` as the source of truth. Each entry carries a `setupUrl` pointing at the central robotic.sh catalog, a provider-specific `referenceUrl`, and a `serviceCatalogPath` you call to fetch the endpoint-level services that belong to the adapter. Those service records include callable endpoint URLs, pricing, payment rails, and consumption instructions. Frontend rendering uses `category`, `robotTypes`, `chains`, and `setupComplexity` for filter chips; `displayOrder` controls the card order on the page. `requiresCredentials` plus `requiredFields` / `optionalFields` drive the credential-entry modal. ## Market services ### `GET /market/services` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListMarketServicesQuery = { serviceType?: ServiceType; executionMode?: ExecutionMode; providerKey?: string; machineId?: string; // include to filter against machine context }; type ListMarketServicesResponse = ListResponse; ``` ### `GET /market/services/:serviceId` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetMarketServiceQuery = { machineId?: string; }; type GetMarketServiceResponse = ItemResponse; ``` ### `POST /market/services/:serviceId/execute` Directly executes a native, read-only service operation without creating a market order. Use this for free lookup or read operations. Paid, external-handoff, and state-changing operations must still flow through `POST /market/orders`. Requires an active peaqOS machine, an active Machine Agent pairing, and pairing session token auth when enabled. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ExecuteMarketServiceRequest = { machineId: string; agentPairingId: string; operation: string; input?: Record; budget?: { amount?: number; max?: number; currency?: string; }; providerCredentials?: ProviderCredentials; }; type ExecuteMarketServiceResponse = { service: MarketService & { operation: string; payment: MarketOrder["payment"]; }; execution: { task: Record; route: Record; outcome: Record; run: Record; }; }; ``` ## Market search ### `POST /market/search` Submits a normalised search and returns a ranked `MarketSearch` with quotes. Required preconditions: * the machine is `active` * its `identityRef` is verified with an identity ownership proof * there is an active Machine Agent pairing for the machine * when `PEAQOS_REQUIRE_AGENT_PAIRING_AUTH=true`, the `x-agent-pairing-token` header is present ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type SearchMarketRequest = { machineId: string; agentPairingId?: string; // required if the machine has more than one active pairing serviceType: ServiceType; operation?: string; requiredCapabilities?: string[]; region?: string; maxResults?: number; // integer 1-25, default 5 allowExternalHandoff?: boolean; requireNativeExecution?: boolean; budget?: { amount?: number; // max acceptable cost; alias of `max` max?: number; // alias of `amount` currency?: string; // defaults to "USD" }; providerCredentials?: ProviderCredentials; // redacted before persistence }; type SearchMarketResponse = ItemResponse; ``` Errors: `MACHINE_NOT_ACTIVE`, `MACHINE_IDENTITY_PROOF_REQUIRED`, `AGENT_PAIRING_REQUIRED`, `AGENT_PAIRING_INACTIVE`, `AGENT_AUTH_REQUIRED`, `AGENT_AUTH_INVALID`, `AGENT_AUTH_EXPIRED`, `AGENT_POLICY_DENIED`, `VALIDATION_ERROR`. ### `GET /market/searches/:searchId` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetMarketSearchResponse = ItemResponse; ``` ## Related * [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) * [Machine Markets: Machines & identity](/peaqos/api-reference/machine-markets-machines) * [Machine Markets: Pairings](/peaqos/api-reference/machine-markets-pairings) * [Machine Markets concept](/peaqos/concepts/machine-markets) * [Scale function](/peaqos/functions/scale) # Machine Markets: Machines & identity Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-machines Identity challenges and machine CRUD for the peaqOS Machine Markets API. These endpoints register and update machine records in the orchestration service. When `PEAQOS_MACHINE_IDENTITY_VERIFICATION=required` is on (production default), the caller must include a signed EIP-191 controller proof obtained from a challenge before the orchestrator will persist any machine-bound write. See the [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) for base path, auth model, common envelopes, and error codes. ## `Machine` type ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type Machine = { id: string; displayName: string; status: "draft" | "active" | "degraded" | "blocked" | "archived"; ownerId: string; identityRef: string; // did:peaq:0x... or peaqos:machine: identityProof?: { method: "eip191"; identityRef: string; signerAddress: string; challengeId: string; verifiedAt: string; challengeExpiresAt: string; controllerAddresses: string[]; resolutionSource: "peaqos-mcr"; } | null; machineType: string; runtimeProfile: string; capabilities: string[]; labels: Record; policyIds: string[]; skillKeys: string[]; createdAt: string; updatedAt: string; }; ``` ## Machine identity ### `POST /machine-identity/challenges` Creates a short-lived challenge for a peaqOS machine identity. The caller signs `message` with the DID-controller private key and submits `{ challengeId, signature }` as `identityProof` when creating or updating the machine. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateMachineIdentityChallengeRequest = { identityRef: string; // did:peaq:0x... or peaqos:machine: }; type CreateMachineIdentityChallengeResponse = ItemResponse<{ challengeId: string; identityRef: string; message: string; expiresAt: string; verificationMethod: "eip191"; controllerAddresses: string[]; }>; type MachineIdentityProofInput = { challengeId: string; signature: string; // EIP-191 personal_sign }; ``` Challenges expire. If a proof is submitted past `expiresAt` the orchestrator returns `MACHINE_IDENTITY_PROOF_EXPIRED` and the caller must re-challenge. ## Machines ### `GET /machines` Only returns machines whose `identityRef` resolves and whose stored `identityProof` matches MCR controller data. Draft machines without a verified proof are filtered out. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListMachinesResponse = ListResponse; ``` ### `POST /machines` Creates a machine record. Requires `identityProof` when identity verification is enabled. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateMachineRequest = { displayName: string; ownerId: string; machineType: string; runtimeProfile: string; identityRef: string; identityProof?: MachineIdentityProofInput; capabilities?: string[]; labels?: Record; policyIds?: string[]; skillKeys?: string[]; }; type CreateMachineResponse = ItemResponse; ``` Errors: `MACHINE_IDENTITY_PROOF_REQUIRED`, `MACHINE_IDENTITY_PROOF_INVALID`, `MACHINE_IDENTITY_PROOF_EXPIRED`, `MACHINE_IDENTITY_EXISTS`, `PEAQOS_IDENTITY_UNAVAILABLE`, `VALIDATION_ERROR`. ### `GET /machines/:machineId` The orchestrator re-verifies the persisted identity proof against peaqOS MCR data on every read. Returns `MACHINE_NOT_ACTIVATED` if the machine no longer resolves through MCR. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetMachineResponse = ItemResponse; ``` ### `PATCH /machines/:machineId` Partial update. `identityRef` is immutable once set; attempting to change it returns `MACHINE_IDENTITY_IMMUTABLE`. Identity proofs are re-verified on every update. Activating an unverified machine returns `MACHINE_NOT_ACTIVATED`. Submitting a proof that does not recover to an MCR controller returns `MACHINE_IDENTITY_PROOF_INVALID`. MCR resolution failures return `PEAQOS_IDENTITY_UNAVAILABLE`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type UpdateMachineRequest = Partial & { status?: Machine["status"]; }; type UpdateMachineResponse = ItemResponse; ``` ## Related * [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) * [Machine Markets: Pairings](/peaqos/api-reference/machine-markets-pairings) * [Machine Markets: Skills, services, search](/peaqos/api-reference/machine-markets-discovery) * [Machine Markets concept](/peaqos/concepts/machine-markets) # Machine Markets: Orchestration Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-orchestration Runtime endpoints, machine-side agents, graph, tasks, routes, outcomes, runs, policies, and audit events on the peaqOS Machine Markets API. The orchestration surface is what makes a service execute end to end. Native skills run through registered runtime endpoints. Machine-side runtime agents register and heartbeat their own endpoints. Tasks model a discover-resolve-execute lifecycle and emit `Route`s, `Outcome`s, and `Run`s. Policies enforce cross-cutting rules. Audit events log every state change. See the [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) for base path, auth model, and common envelopes. All endpoints below require the machine to be `active` and identity-verified. Errors `MACHINE_NOT_ACTIVE` and `MACHINE_NOT_ACTIVATED` are uniform across the group. ## Runtime endpoints ### `GET /machines/:machineId/runtime-endpoints` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListRuntimeEndpointsResponse = ListResponse; ``` ### `GET /machines/:machineId/runtime-endpoints/:providerKey` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetRuntimeEndpointResponse = ItemResponse; ``` ### `PUT /machines/:machineId/runtime-endpoints/:providerKey` Upsert a manual runtime endpoint. `source` is forced to `manual`. Returns 201 on create, 200 on update. The server lowercases `providerKey` in the path. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type UpsertRuntimeEndpointRequest = { endpointBaseUrl: string; // http/https URL, path/query/hash stripped label?: string; // default `${providerKey.toUpperCase()} runtime` authType?: "none" | "api-key" | "bearer"; authToken?: string; modelAlias?: string; networkScope?: "loopback" | "lan" | "vpn" | "public"; probePath?: string; // default "/v1/models" for qvac, else "/health" status?: "active" | "degraded" | "unreachable"; leaseExpiresAt?: string; // ISO 8601 }; type UpsertRuntimeEndpointResponse = ItemResponse; ``` Errors: `VALIDATION_ERROR` (invalid URL). ### `DELETE /machines/:machineId/runtime-endpoints/:providerKey` Returns 204. Errors: `NOT_FOUND`. ### `RuntimeEndpointView` The view never echoes `authToken`. Use `hasAuthToken` to check whether one is stored. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type RuntimeEndpointView = { id: string; machineId: string; providerKey: string; source: "manual" | "agent"; agentId: string | null; label: string; endpointBaseUrl: string; authType: "none" | "api-key" | "bearer"; hasAuthToken: boolean; modelAlias: string | null; networkScope: "loopback" | "lan" | "vpn" | "public"; probePath: string; status: "active" | "degraded" | "unreachable"; leaseExpiresAt: string | null; lastCheckedAt: string | null; lastHealthyAt: string | null; createdAt: string; updatedAt: string; }; ``` ## Machine-side agents ### `POST /machines/:machineId/agents/enrollment` Enrols a new runtime agent. Returns the agent record plus a one-time `agentToken`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type EnrollMachineAgentRequest = { label?: string; // default "Machine runtime agent" allowedProviderKeys?: string[]; // default ["qvac"], normalised to lowercase providerKeys?: string[]; // alias for allowedProviderKeys }; type EnrollMachineAgentResponse = { item: MachineAgentView & { agentToken: string }; }; ``` ### `POST /machine-agents/heartbeat` Heartbeat from an on-machine runtime agent. Auth via `agentToken` body field or `x-agent-pairing-token` / `Authorization: Bearer` header. Upserts one or many endpoints with `source: "agent"` and a lease. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type HeartbeatRequest = { machineId: string; agentId: string; agentToken?: string; leaseSeconds?: number; // [30, 86400], default 120 endpoints?: Array<{ providerKey: string; endpointBaseUrl: string; label?: string; authType?: "none" | "api-key" | "bearer"; authToken?: string; modelAlias?: string; networkScope?: "loopback" | "lan" | "vpn" | "public"; probePath?: string; status?: "active" | "degraded" | "unreachable"; }>; }; type HeartbeatResponse = { item: MachineAgentView; endpoints: RuntimeEndpointView[]; }; ``` Errors: `VALIDATION_ERROR`, `NOT_FOUND`, `AGENT_PAIRING_INACTIVE` (status `revoked`), `AGENT_AUTH_REQUIRED`, `AGENT_AUTH_INVALID`, `AGENT_POLICY_DENIED` (provider key not in `agent.allowedProviderKeys`). Side effects: agent status flips to `active`, `lastSeenAt` updates, each endpoint upserts with `leaseExpiresAt = now + leaseSeconds`. ### `MachineAgentView` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type MachineAgentView = { id: string; machineId: string; label: string; allowedProviderKeys: string[]; status: "provisioned" | "active" | "revoked"; hasAgentToken: boolean; lastSeenAt: string | null; createdAt: string; updatedAt: string; }; ``` ## Graph ### `GET /machines/:machineId/graph` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetGraphResponse = { nodes: GraphNodeRecord[]; edges: GraphEdgeRecord[]; }; ``` ### `POST /machines/:machineId/graph/nodes` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateGraphNodeRequest = { type: NodeType; title: string; refId?: string; position?: { x?: number; y?: number }; data?: Record; }; type CreateGraphNodeResponse = ItemResponse; ``` ### `PATCH /machines/:machineId/graph/nodes/:nodeId` All fields optional; `data` is shallow-merged. Status accepted: `"draft" | "active" | "blocked" | "degraded"`. ### `DELETE /machines/:machineId/graph/nodes/:nodeId` Returns 204. ### `POST /machines/:machineId/graph/edges` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateGraphEdgeRequest = { sourceNodeId: string; targetNodeId: string; type: EdgeType; label?: string; data?: Record; }; type CreateGraphEdgeResponse = ItemResponse; ``` ### `PATCH /machines/:machineId/graph/edges/:edgeId` / `DELETE /machines/:machineId/graph/edges/:edgeId` Standard patch and delete semantics. ### Graph types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type NodeType = | "machine" | "trigger" | "skill" | "task" | "policy" | "discovery" | "storage" | "memory" | "handoff" | "route" | "outcome" | "catalog-entry"; type EdgeType = | "connects" | "request" | "discover" | "constrain" | "candidate" | "route" | "result" | "route-select" | "route-source" | "execute" | "handoff" | "receipt" | "sync" | "allow" | "deny"; type GraphNodeRecord = { id: string; machineId: string; type: NodeType; refId?: string | null; title: string; status: "draft" | "active" | "blocked" | "degraded"; position: { x: number; y: number }; data: Record; createdAt: string; updatedAt: string; }; type GraphEdgeRecord = { id: string; machineId: string; sourceNodeId: string; targetNodeId: string; type: EdgeType; label?: string; data: Record; createdAt: string; updatedAt: string; }; ``` ## Tasks Tasks model the discover-resolve-execute pipeline that orders ride on. `POST /market/orders` materialises an order as a task internally, but tasks can also be driven directly. ### `GET /machines/:machineId/tasks` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListTasksResponse = ListResponse; ``` ### `POST /machines/:machineId/tasks` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateTaskRequest = { serviceType: ServiceType; taskType: string; operation?: string; input?: Record; constraints?: { region?: string; budget?: number; // >= 0 allowExternalHandoff?: boolean; // default true requireNativeExecution?: boolean;// default false requiredCapabilities?: string[]; preferredSkillKeys?: string[]; // must all be known skills }; discovery?: { maxCandidates?: number }; // 1..25, default 10 }; type CreateTaskResponse = ItemResponse; ``` ### `GET /machines/:machineId/tasks/:taskId` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetTaskResponse = ItemResponse; ``` ### `POST /machines/:machineId/tasks/:taskId/discover` Runs discovery against the catalogue for a queued task. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type DiscoverTaskRequest = { allowExternalHandoff?: boolean; requireNativeExecution?: boolean; maxCandidates?: number; }; type DiscoverTaskResponse = { taskId: string; machineId: string; resolution: TaskResolution; candidates: Candidate[]; }; ``` ### `POST /machines/:machineId/tasks/:taskId/resolve` Picks a route and persists it. Returns 201. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ResolveTaskResponse = { item: RouteRecord; resolution: TaskResolution; }; ``` ### `POST /machines/:machineId/tasks/:taskId/execute` Executes the resolved route. HTTP status propagates from the inner execution result. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ExecuteTaskRequest = { input?: Record; }; type ExecuteTaskResponse = { item: OutcomeRecord; run: RunRecord; resolution: TaskResolution; }; ``` Errors: `ROUTE_REQUIRED` (no route resolved), plus all downstream skill-runtime errors. ### Task types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ExternalHandoff = { label: string; url: string; notes: string[]; }; type Candidate = { service: CanonicalService; // catalogue service shape, see machine-markets-discovery score: number; reasons: string[]; }; type TaskResolution = | { kind: "native"; candidate: Candidate } | { kind: "handoff"; candidate: Candidate; handoff: ExternalHandoff } | { kind: "unavailable"; reason: string; candidates: Candidate[] }; type TaskStatus = | "draft" | "queued" | "resolving" | "resolved" | "executing" | "completed" | "failed" | "handed_off"; type TaskRecord = { id: string; machineId: string; serviceType: ServiceType; operation?: string; taskType: string; status: TaskStatus; input: Record; constraints: { region?: string; budget?: number; allowExternalHandoff: boolean; requireNativeExecution: boolean; requiredCapabilities: string[]; preferredSkillKeys?: string[]; }; discovery: { maxCandidates: number }; routeId?: string | null; outcomeId?: string | null; createdAt: string; updatedAt: string; }; ``` ## Routes, outcomes, runs ### `GET /machines/:machineId/routes` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListRoutesResponse = ListResponse; ``` ### `GET /machines/:machineId/outcomes` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListOutcomesResponse = ListResponse; ``` ### `GET /runs?machineId={id}&...` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListRunsQuery = { machineId: string; status?: RunStatus; skillKey?: string; serviceId?: string; executionMode?: ExecutionMode; limit?: number; // default 100, max 500 }; type ListRunsResponse = ListResponse; ``` ### `GET /runs/:runId` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetRunResponse = { run: RunRecord; outcome: OutcomeRecord | null; }; ``` ### Route / Outcome / Run types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type RouteStatus = "selected" | "blocked" | "handed_off" | "failed" | "completed"; type OutcomeStatus = "pending" | "running" | "completed" | "failed" | "external"; type RunStatus = "queued" | "running" | "completed" | "failed" | "external" | "blocked"; type RouteRecord = { id: string; machineId: string; taskId: string; selectedSkillKey: string; selectedServiceId: string; executionMode: ExecutionMode; integrationStatus: IntegrationStatus; status: RouteStatus; reason: string; score: number; fallbackChain: Array<{ skillKey: string; serviceId: string; executionMode: ExecutionMode }>; handoff: ExternalHandoff | null; createdAt: string; updatedAt: string; }; type OutcomeRecord = { id: string; machineId: string; taskId: string; routeId: string; status: OutcomeStatus; skillKey: string; serviceId: string; executionMode: ExecutionMode; receiptRef?: string | null; logsRef?: string | null; externalRef?: string | null; errorCode?: string | null; errorMessage?: string | null; result?: unknown; createdAt: string; updatedAt: string; }; type RunRecord = { id: string; machineId: string; taskId: string; routeId?: string | null; outcomeId?: string | null; status: RunStatus; executionMode?: ExecutionMode | null; skillKey?: string | null; serviceId?: string | null; summary?: string | null; createdAt: string; updatedAt: string; }; ``` ## Policies Policies enforce cross-cutting rules above and beyond a pairing's delegation policy. ### `GET /policies` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListPoliciesResponse = ListResponse; ``` ### `POST /policies` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreatePolicyRequest = { name: string; allowedSkillKeys?: string[]; deniedSkillKeys?: string[]; budgetCap?: { amount: number; currency: string } | null; regionRules?: string[]; nativeOnly?: boolean; handoffAllowed?: boolean; requiredCapabilities?: string[]; }; type CreatePolicyResponse = ItemResponse; ``` ### `GET /policies/:policyId` / `PATCH /policies/:policyId` Standard get and patch semantics. ### `PolicyRecord` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type PolicyRecord = { id: string; name: string; allowedSkillKeys: string[]; deniedSkillKeys: string[]; budgetCap?: { currency: string; amount: number } | null; regionRules: string[]; nativeOnly: boolean; handoffAllowed: boolean; requiredCapabilities: string[]; createdAt: string; updatedAt: string; }; ``` ## Audit events ### `GET /audit-events` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListAuditEventsQuery = { type?: string; resourceType?: string; resourceId?: string; limit?: number; // default 100, max 500 }; type ListAuditEventsResponse = ListResponse; type AuditEventRecord = { id: string; actor: string; // "api" | "system:machine-markets" type: string; resourceType: string; resourceId: string; payload: Record; createdAt: string; }; ``` Event types emitted by the orchestration surface: `runtime-endpoint.upserted`, `runtime-endpoint.deleted`, `machine-agent.enrolled`, `machine-agent.heartbeat`, `graph.node.created`, `graph.node.updated`, `graph.node.deleted`, `graph.edge.created`, `graph.edge.updated`, `graph.edge.deleted`, `task.created`, `policy.created`, `policy.updated`, `market.order.created`, `market.payment.intent-created`, `market.payment.proof-recorded`, `market.order.executed`, `market.order.confirmed`, `market.order.disputed`, `market.payment.escrow-held`, `market.payment.released`, `market.payment.refunded`. ## Related * [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) * [Machine Markets: Orders & payments](/peaqos/api-reference/machine-markets-orders) * [Machine Markets: Pairings](/peaqos/api-reference/machine-markets-pairings) * [Machine Markets: Skills, services, search](/peaqos/api-reference/machine-markets-discovery) * [Machine Markets concept](/peaqos/concepts/machine-markets) * [Scale function](/peaqos/functions/scale) # Machine Markets: Orders & payments Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-orders Order lifecycle, payment intent, escrow, execute, confirm, dispute on the peaqOS Machine Markets API. A Machine Agent buys a service through an order. The orchestrator owns the full lifecycle: order create, payment intent, proof or escrow lock, execute, confirm, dispute, release, refund. Server-side enforcement covers identity proofs, agent pairing, delegation policy, per-transaction and daily spend limits, quote freshness, and rail compatibility. See the [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) for base path, auth model, and common envelopes. ## Order state machine ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} created -> payment_pending -> ready -> executing -> delivered -> confirmed \-> handoff \-> disputed \-> failed \-> cancelled ``` ## Payment state machine ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} intent_created -> held -> release_pending -> released not_required \-> refunded \-> frozen (on dispute) ``` ## Types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type MarketOrderStatus = | "created" | "payment_pending" | "ready" | "executing" | "delivered" | "confirmed" | "disputed" | "cancelled" | "failed" | "handoff"; type MarketPaymentStatus = | "not_required" | "intent_created" | "held" | "release_pending" | "released" | "frozen" | "refunded"; type MarketPaymentRailType = | "not-required" | "wallet" | "x402" | "xvv42" | "vault-stripe" | "wdk-usdt-transfer" | "escrow" | "offchain-record" | "external" | "onchain-escrow"; type MarketOrderRecord = { id: string; machineId: string; agentPairingId: string; searchId: string | null; quoteId: string | null; serviceId: string; skillKey: string; providerKey: string; serviceType: ServiceType; operation: string; executionMode: ExecutionMode; integrationStatus: IntegrationStatus; status: MarketOrderStatus; input: Record; budget?: { amount?: number | null; currency?: string | null } | null; payment: MarketPaymentRailRecommendation; serviceSnapshot: Record; taskId: string | null; routeId: string | null; runId: string | null; outcomeId: string | null; handoff: ExternalHandoff | null; errorMessage: string | null; createdAt: string; updatedAt: string; }; type MarketPaymentRailRecommendation = { defaultRail: MarketPaymentRailType; supportedRails: MarketPaymentRailType[]; required: boolean; provider?: string | null; chain?: string | null; token?: string | null; walletAddress?: string | null; externalUrl?: string | null; notes: string[]; }; type MarketPaymentRecord = { id: string; orderId: string; machineId: string; status: MarketPaymentStatus; rail: { type: MarketPaymentRailType; chain?: string | null; token?: string | null; escrowAddress?: string | null; externalUrl?: string | null; provider?: string | null; accountId?: string | null; walletAddress?: string | null; payerAddress?: string | null; payeeAddress?: string | null; tokenAddress?: string | null; transactionHash?: string | null; paymentIntentId?: string | null; metadata?: Record; }; amount?: number | null; currency?: string | null; proof?: { transactionHash: string; verificationMode: "recorded" | "rpc"; status: "recorded" | "verified"; chain?: string | null; token?: string | null; tokenAddress?: string | null; payerAddress?: string | null; payeeAddress?: string | null; amount?: string | null; rawAmount?: string | null; blockNumber?: number | null; verifiedAt: string; metadata?: Record; } | null; notes: string[]; createdAt: string; updatedAt: string; }; type ExternalHandoff = { label: string; url: string; notes: string[]; }; type MarketDisputeRecord = { id: string; orderId: string; machineId: string; status: "open" | "resolved"; reason: string; evidence: Record; createdAt: string; updatedAt: string; }; ``` ## Orders ### `GET /market/orders?machineId={id}` Lists orders for a machine. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListMarketOrdersQuery = { machineId: string }; type ListMarketOrdersResponse = ListResponse; ``` Errors: `VALIDATION_ERROR`, `NOT_FOUND`, `MACHINE_NOT_ACTIVE`, `MACHINE_NOT_ACTIVATED`. ### `POST /market/orders` Locks a service from the catalogue into an order. Status starts at `created`. The recommended payment rail is copied from the service into `order.payment`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateOrderRequest = { machineId: string; agentPairingId: string; serviceId: string; searchId?: string; // required when PEAQOS_REQUIRE_MARKET_QUOTES=true quoteId?: string; // required when PEAQOS_REQUIRE_MARKET_QUOTES=true operation?: string; input?: Record; budget?: { amount?: number; max?: number; currency?: string }; operatorCredentials?: { providers?: Record } & Record>; }; }; type CreateOrderResponse = ItemResponse; ``` Errors: `VALIDATION_ERROR`, `QUOTE_EXPIRED`, `AGENT_PAIRING_REQUIRED`, `AGENT_PAIRING_INACTIVE`, `AGENT_POLICY_DENIED`, `AGENT_SPEND_LIMIT_EXCEEDED`, `AGENT_DAILY_LIMIT_EXCEEDED`, `MACHINE_NOT_ACTIVE`, `MACHINE_NOT_ACTIVATED`, `NOT_FOUND`. ### `GET /market/orders/:orderId` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetMarketOrderResponse = ItemResponse; ``` ## Payment ### `POST /market/orders/:orderId/payment-intent` Creates or returns the payment record. Idempotent: returns the existing payment if one already exists. For `not-required` rails the order moves straight to `ready`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreatePaymentIntentRequest = { amount?: number; currency?: string; rail?: { type?: MarketPaymentRailType; chain?: string; token?: string; tokenAddress?: string; tokenMint?: string; // alias for tokenAddress on Solana escrowAddress?: string; externalUrl?: string; provider?: string; accountId?: string; walletAddress?: string; payerAddress?: string; payeeAddress?: string; transactionHash?: string; paymentIntentId?: string; metadata?: Record; }; payerAddress?: string; payeeAddress?: string; tokenAddress?: string; tokenMint?: string; transactionHash?: string; txHash?: string; }; type CreatePaymentIntentResponse = ItemResponse; ``` Errors: `NOT_FOUND`, `VALIDATION_ERROR` (unsupported rail, invalid address, rail not in service `supportedRails` when `PEAQOS_ENFORCE_SERVICE_PAYMENT_RAILS=true`). ### `POST /market/orders/:orderId/payment-proof` Records a payment proof. `recorded` mode stores operator attestation. `rpc` mode queries an EVM RPC for the transaction receipt and validates the ERC-20 Transfer log against expected token, sender, recipient, and raw amount. Solana proofs are always `recorded`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type RecordPaymentProofRequest = { transactionHash: string; // EVM 0x… 64 hex, or Solana base58 64–100. Aliases: txHash, transactionSignature, signature. chain?: string; // default "peaq" token?: string; // default "USDT" payerAddress?: string; payeeAddress?: string; tokenAddress?: string; // aliases: tokenMint, usdtContract; falls back to PEAQOS_USDT_CONTRACT amount?: string; rawAmount?: string; // integer in token base units tokenDecimals?: number; // 0..36, default 6 verificationMode?: "recorded" | "rpc"; // default rpc when RPC URL set rpcUrl?: string; metadata?: Record; }; type RecordPaymentProofResponse = ItemResponse; ``` Errors: `NOT_FOUND`, `ORDER_CLOSED`, `VALIDATION_ERROR`, `PAYMENT_RPC_REQUIRED` (rpc mode with no RPC URL), `PAYMENT_RPC_ERROR` (502), `PAYMENT_NOT_MINED` (409, receipt null), `PAYMENT_TX_FAILED` (402, receipt status not `0x1`), `PAYMENT_TRANSFER_NOT_FOUND` (400, no matching Transfer log). On successful verification the rail type upgrades from `wallet` to `wdk-usdt-transfer`. Payment status moves to `held`; order moves `created`/`payment_pending` → `ready`. ### `GET /market/orders/:orderId/payment` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type GetMarketPaymentResponse = ItemResponse; ``` ### `POST /market/orders/:orderId/payment/escrow-lock` Records an on-chain escrow lock. Payment → `held`. Order → `ready`. Idempotent for `held`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type EscrowLockRequest = { transactionHash: string; chain: string; escrowAddress: string; }; type EscrowLockResponse = ItemResponse; ``` Errors: `NOT_FOUND`, `ORDER_CLOSED`, `VALIDATION_ERROR`. ### `POST /market/orders/:orderId/payment/release` Releases held funds after `confirm`. Payment must be `release_pending`, order must be `confirmed`. Status → `released`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ReleasePaymentRequest = { transactionHash?: string; notes?: string; }; type ReleasePaymentResponse = ItemResponse; ``` Errors: `NOT_FOUND`, `ORDER_CLOSED`, `ORDER_NOT_DELIVERED`. Idempotent for `released` and `not_required`. ### `POST /market/orders/:orderId/payment/refund` Refunds payment. Status → `refunded`. Open orders move to `cancelled`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type RefundPaymentRequest = { transactionHash?: string; reason?: string; }; type RefundPaymentResponse = ItemResponse; ``` Errors: `NOT_FOUND`, `ORDER_CLOSED`. Idempotent for `refunded` and `not_required`. ## Execute, confirm, dispute ### `POST /market/orders/:orderId/execute` Materialises the order as a task plus route, runs the skill runtime, writes a `Run` and an `Outcome`. Order status moves `executing` → `delivered` (outcome `completed`) / `handoff` (outcome `external`) / `failed`. The HTTP status code is propagated from the inner execution result. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ExecuteOrderRequest = Record & { operatorCredentials?: { providers?: Record } & Record>; }; }; type ExecuteOrderResponse = { order: MarketOrderRecord; execution: { statusCode: number; outcome: OutcomeRecord; run: RunRecord; resolution: TaskResolution; }; }; ``` Errors: `ORDER_CLOSED` (order in `confirmed` / `disputed` / `cancelled`), `PAYMENT_REQUIRED` (when `PEAQOS_REQUIRE_PAYMENT_BEFORE_EXECUTE=true`, the production default, and payment is not in `not_required` / `held` / `release_pending` / `released` — except `intent_created` is allowed for `external` / `vault-stripe` rails). All downstream skill-runtime errors are surfaced as well. x402-rail services (Agentic Market, most pay.sh) execute through the `paidHttp` adapter. The execute call replays the original 402-challenged request with the operator-supplied payment headers (`x-payment`, `payment-signature`, `authorization`, `sign-in-with-x`). The payment intent response now preserves the full x402 `accepts[]` entries including `extra` (EIP-712 signing metadata: `{ name: "USD Coin", version: "2" }`-style) and `outputSchema`. Operators that build their own paid-HTTP adapters should pass headers under `operatorCredentials.providers[providerKey]`; raw headers are stripped from request bodies before forwarding and never persisted. ### `POST /market/orders/:orderId/confirm` Buyer confirms delivery. Order `delivered` / `handoff` → `confirmed`. Payment `held` → `release_pending`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ConfirmOrderResponse = { order: MarketOrderRecord; payment: MarketPaymentRecord | null; }; ``` Errors: `ORDER_NOT_DELIVERED`. ### `POST /market/orders/:orderId/dispute` Opens a dispute record. Order → `disputed`. Payment → `frozen`. Returns 201. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type DisputeOrderRequest = { reason: string; evidence?: Record; }; type DisputeOrderResponse = { order: MarketOrderRecord; payment: MarketPaymentRecord | null; dispute: MarketDisputeRecord; }; ``` Errors: `VALIDATION_ERROR` (missing reason). ## Related * [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) * [Machine Markets: Pairings](/peaqos/api-reference/machine-markets-pairings) * [Machine Markets: Skills, services, search](/peaqos/api-reference/machine-markets-discovery) * [Machine Markets: Orchestration](/peaqos/api-reference/machine-markets-orchestration) * [Machine Markets concept](/peaqos/concepts/machine-markets) * [Scale function](/peaqos/functions/scale) # Machine Markets API overview Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-overview Base path, access model, common shapes, error codes, and endpoints for the peaqOS Machine Markets API. The Machine Markets API is the HTTP/JSON interface for [Scale](/peaqos/functions/scale). It handles machine identity proofs, machine records, Machine Agent pairings, the skill registry, the service catalogue, and machine-aware market search. ## Base path All endpoints sit under `/api/v1` on the orchestrator host. The canonical peaq-managed endpoint is `https://orchestration.peaq.xyz`: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export PEAQOS_ORCHESTRATION_URL=https://orchestration.peaq.xyz # All paths below are relative to ${PEAQOS_ORCHESTRATION_URL}/api/v1 ``` `GET /health` is an exception: it lives at host root, not under `/api/v1`, and is exempt from API-key auth. Self-hosters can run their own orchestrator via Docker Compose and point `PEAQOS_ORCHESTRATION_URL` at it. ## Access model * The API is public unless the deployment enables API-key auth (`PEAQOS_REQUIRE_API_AUTH=true`). * Machine registration accepts `identityRef` as either a peaq DID (`did:peaq:0x...`) or a peaqOS machine public ID (`peaqos:machine:`). * When `PEAQOS_MACHINE_IDENTITY_VERIFICATION=required` (production default), every machine registration and machine-bound write verifies `identityRef` against peaqOS MCR data and requires a signed DID-controller challenge. * Machine Agent pairing is challenge-based. The agent signs a server-issued challenge with the wallet key behind `agentAddress` (EIP-191). The orchestrator verifies the signature, persists the pairing, and issues a signed session JWT as `pairingToken`. * Market search requires (a) an active, bonded peaqOS machine, (b) a verified `identityRef` with ownership proof, and (c) an active Machine Agent pairing. * When `PEAQOS_REQUIRE_AGENT_PAIRING_AUTH=true` (production default), market search must include the `x-agent-pairing-token` header. The token is a signed HS256 JWT with claims for `machineId`, `pairingId`, `sessionId`, `agentAddress`, `agentDid`, `delegationPolicyHash`, and expiry. Rotate it via the pairing sessions endpoint before `exp`. * The pairing token proves delegated permission. It does not prove machine activation. * Pass provider credentials per request under `providerCredentials`. They feed runtime discovery and are redacted before persistence. ## Payment model peaqOS does not collect marketplace payments. The payment-intent and payment-proof endpoints record provider/service payment state only when the selected adapter requires direct provider payment (x402, escrow, wallet rails). Operators or machines pay the selected provider directly through that provider's supported rail. pay.sh and Agentic Market services use request-scoped payment proofs: the API returns the provider's payment challenge from `payment-intent`, the operator agent signs or pays locally, and the resulting proof or header is sent to `payment-proof` and `execute`. Raw payment headers are never stored. ## Common envelopes ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListResponse = { items: T[]; nextCursor?: string; // omitted when there is no next page }; type ListQuery = { limit?: number; // 1..500, default 100 cursor?: string; // opaque base64url, do not parse }; type PaymentHeaders = { xPayment?: string; paymentSignature?: string; authorization?: string; signInWithX?: string; }; type ItemResponse = { item: T; }; type ErrorResponse = { error: { code: string; message: string; details?: unknown; }; }; ``` ## Error codes ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CommonErrorCode = // Auth | "AUTH_REQUIRED" | "AUTH_INVALID" | "AGENT_AUTH_REQUIRED" | "AGENT_AUTH_INVALID" | "AGENT_AUTH_EXPIRED" // Generic | "VALIDATION_ERROR" | "NOT_FOUND" // Machine | "MACHINE_NOT_ACTIVE" | "MACHINE_NOT_ACTIVATED" | "MACHINE_IDENTITY_EXISTS" | "MACHINE_IDENTITY_IMMUTABLE" | "MACHINE_IDENTITY_PROOF_REQUIRED" | "MACHINE_IDENTITY_PROOF_INVALID" | "MACHINE_IDENTITY_PROOF_EXPIRED" | "PEAQOS_IDENTITY_UNAVAILABLE" // Pairing | "AGENT_PAIRING_PROOF_REQUIRED" | "AGENT_PAIRING_PROOF_INVALID" | "AGENT_PAIRING_PROOF_EXPIRED" | "AGENT_PAIRING_UNAVAILABLE" | "AGENT_PAIRING_REQUIRED" | "AGENT_PAIRING_INACTIVE" | "AGENT_POLICY_DENIED" | "AGENT_SPEND_LIMIT_EXCEEDED" | "AGENT_DAILY_LIMIT_EXCEEDED" // Market orders | "OPEN_MARKET_ORDERS" | "QUOTE_EXPIRED" | "ROUTE_REQUIRED" | "ORDER_CLOSED" | "ORDER_NOT_DELIVERED" // Payments | "PAYMENT_REQUIRED" | "PAYMENT_RPC_REQUIRED" | "PAYMENT_RPC_ERROR" | "PAYMENT_NOT_MINED" | "PAYMENT_TX_FAILED" | "PAYMENT_TRANSFER_NOT_FOUND"; ``` ## Common types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ExecutionMode = "native" | "external-handoff"; type IntegrationStatus = | "native" | "credentials-required" | "partner-required" | "local-config-required" | "setup-required" | "docs-only"; type ServiceType = | "oracle.price-feed" | "compute.marketplace" | "compute.confidential" | "storage.object" | "data.location" | "network.partner-console" | "identity.proof-of-person" | "device.control" | "machine.commerce"; type ProviderCredentials = { // Per-adapter credentials. Shape is provider-specific and // documented per-adapter on robotic.sh. Always redacted // before request bodies are persisted. [providerKey: string]: Record; }; ``` ## Pagination Every list endpoint accepts `ListQuery`. Cursors are opaque — do not parse them. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} // First page GET /api/v1/market/services?limit=100 // Next page: pass the previous response's nextCursor exactly as returned GET /api/v1/market/services?limit=100&cursor=eyJ2IjoxLCJvZmZzZXQiOjEwMH0 ``` Rules: * First page omits `cursor`. * `nextCursor` is omitted when there is no next page. It is never `null`. * Pass `nextCursor` back unchanged on the next request. * Invalid `limit` (out of range or non-integer) or malformed `cursor` returns `400 VALIDATION_ERROR`. ## Health | Endpoint | Path | Description | | :--------------- | :------------------------ | :------------------------------------------------------------------------------- | | `GET /health` | host root (not `/api/v1`) | Process health. Returns `{ ok: true }`. Exempt from API-key auth. | | `GET /readiness` | `/api/v1/readiness` | Store, catalog, auth, runtime, and skill checks. Returns `{ status, checks[] }`. | ## Endpoint groups Identity challenges, machine CRUD, EIP-191 controller proofs. Challenge-based pairings, session JWT rotation, delegation policy. Browse the skill registry, the partner service catalogue, and the adapter setup catalogue; run market search. Order lifecycle, payment intent, escrow, execute, confirm, dispute. Runtime endpoints, machine-side agents, graph, tasks, routes, outcomes, runs, policies, audit events. ## Server configuration When self-hosting the orchestrator, these toggles change the contract callers see. Defaults match what the `.env.example` ships. | Variable | Default | Purpose | | :---------------------------------------------- | :----------------------------------------------------- | :--------------------------------------------------------------------------------------------- | | `PEAQOS_REQUIRE_API_AUTH` | `false` | Gate every `/api/v1/*` request with a deployment API key (`PEAQOS_API_KEY`/`PEAQOS_API_KEYS`). | | `PEAQOS_REQUIRE_AGENT_PAIRING_AUTH` | `true` | Require `x-agent-pairing-token` on market writes. | | `PEAQOS_MACHINE_IDENTITY_VERIFICATION` | `required` | Enforce DID-controller proof on machine writes. | | `PEAQOS_AGENT_PAIRING_VERIFICATION` | `required` | Enforce EIP-191 challenge proof on pairings. | | `PEAQOS_AGENT_PAIRING_SESSION_SECRET` | required in prod | HS256 secret for session JWTs and challenge HMAC. | | `PEAQOS_AGENT_PAIRING_CHALLENGE_TTL_MS` | `600000` | Pairing challenge TTL (10 min). | | `PEAQOS_AGENT_PAIRING_SESSION_TTL_MS` | `3600000` | Session token TTL (1 hour). | | `PEAQOS_REQUIRE_MARKET_QUOTES` | `false` | Reject `POST /market/orders` without `searchId` and `quoteId`. | | `PEAQOS_ENFORCE_SERVICE_PAYMENT_RAILS` | `false` | Reject `payment-intent` rails not in the service's `supportedRails`. | | `PEAQOS_REQUIRE_PAYMENT_BEFORE_EXECUTE` | `false` | Block `execute` unless payment is `held` / `release_pending` / `released` / `not_required`. | | `PEAQOS_REQUIRE_PAYMENT_RPC_VERIFICATION` | `false` | Force `verificationMode=rpc` on `payment-proof`. | | `PEAQOS_PAYMENT_RPC_URL` | falls back to `PEAQ_EVM_RPC_URL`, `PEAQOS_EVM_RPC_URL` | Primary RPC for proof verification. | | `PEAQOS_USDT_CONTRACT` / `PEAQOS_USDT_DECIMALS` | none / `6` | Default USDT contract and decimals for payment proof. | | `PEAQOS_MCR_API_URL` | `https://mcr.peaq.xyz` | peaqOS MCR endpoint for identity resolution. | # Machine Markets: Machine Agent pairings Source: https://docs.peaq.xyz/peaqos/api-reference/machine-markets-pairings Challenge, pair, rotate, repolicy, and revoke Machine Agents on the peaqOS Machine Markets API. A Machine Agent pairing binds a third-party AI agent (Claude, OpenAI, Virtuals, Teneo, or your own) to an activated, bonded peaqOS machine and attaches a delegation policy that bounds what the agent can do on the machine's behalf. Pairing is challenge-based: peaqOS issues a machine-bound challenge, the agent signs it with the wallet key behind `agentAddress` (EIP-191 `personal_sign`), and the orchestrator verifies the signature before persisting the pairing and issuing a signed session token. See the [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) for base path, auth model, and common envelopes. ## `AgentPairing` type ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type AgentPairing = { id: string; machineId: string; status: "active" | "paused" | "revoked"; agentAddress: string; agentDid: string | null; // did:pkh:eip155:1: when present agentProvider: string; // "anthropic" | "openai" | "virtuals" | ... agentRole: string; // free-form, e.g. "ops" | "trader" | "buyer" description?: string | null; verification: { method: "eip191"; signerAddress: string; // recovered signer; must match agentAddress verifiedAt: string; challengeExpiresAt: string; } | null; delegationPolicy: { allowedSkillKeys: string[]; deniedSkillKeys: string[]; allowedServiceIds: string[]; deniedServiceIds: string[]; perTransactionLimit?: number | null; dailySpendLimit?: number | null; currency?: string | null; }; hasAuthToken: boolean; tokenLastFour: string | null; sessionId: string | null; sessionTokenId: string | null; sessionIssuedAt: string | null; sessionExpiresAt: string | null; pairingToken?: string; // signed HS256 session JWT, returned once at create/rotate createdAt: string; updatedAt: string; }; ``` ## `AgentPairingChallenge` and `AgentPairingProof` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type AgentPairingChallenge = { challengeId: string; machineId: string; machineIdentityRef: string | null; agentAddress: string; agentDid: string; agentProvider: string; agentRole: string; message: string; // sign this with the key behind agentAddress expiresAt: string; verificationMethod: "eip191"; }; type AgentPairingProof = { challengeId: string; signature: string; }; ``` ## Endpoints ### `GET /machines/:machineId/agent-pairings` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListAgentPairingsResponse = ListResponse; ``` ### `POST /machines/:machineId/agent-pairings/challenges` Returns a fresh challenge for an agent. The Machine Agent signs `item.message` (EIP-191) with the wallet key behind `agentAddress`, then submits the signature to the create-pairing endpoint or the session-rotation endpoint. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateAgentPairingChallengeRequest = { agentAddress: string; agentDid?: string; agentProvider: string; agentRole: string; }; type CreateAgentPairingChallengeResponse = ItemResponse; ``` If `agentDid` is omitted, the orchestrator derives `did:pkh:eip155:1:` from the normalised `agentAddress`. If supplied, `agentDid` must be of the form `did:pkh:eip155::` whose address matches `agentAddress` (checksummed) — anything else returns `VALIDATION_ERROR`. ### `POST /machines/:machineId/agent-pairings` Creates a pairing. Requires `agentProof` when pairing verification is enabled (production default). Returns the pairing with a signed session JWT in `pairingToken`. The token is returned once at create; rotate via the sessions endpoint. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateAgentPairingRequest = { agentAddress: string; agentDid?: string; agentProvider: string; agentRole: string; description?: string; agentProof: AgentPairingProof; delegationPolicy?: Partial; }; type CreateAgentPairingResponse = ItemResponse; ``` Errors: `AGENT_PAIRING_PROOF_REQUIRED`, `AGENT_PAIRING_PROOF_INVALID`, `AGENT_PAIRING_PROOF_EXPIRED`, `AGENT_PAIRING_UNAVAILABLE`, `MACHINE_NOT_ACTIVE`, `MACHINE_IDENTITY_PROOF_REQUIRED`, `VALIDATION_ERROR`. ### `POST /machines/:machineId/agent-pairings/:pairingId/sessions` Rotates the session JWT for an active pairing. The agent gets a fresh challenge, signs it, and submits the new proof here. Returns the pairing with a new `pairingToken`. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type CreateAgentPairingSessionRequest = { agentProof: AgentPairingProof; }; type CreateAgentPairingSessionResponse = ItemResponse; ``` Default session TTL is 1 hour (`PEAQOS_AGENT_PAIRING_SESSION_TTL_MS`). Market writes reject expired tokens with `AGENT_AUTH_EXPIRED` and reject tokens whose `delegationPolicyHash` no longer matches the persisted policy. ### `PATCH /machines/:machineId/agent-pairings/:pairingId` Repolicy or pause/resume. Policy changes invalidate the current session token's `delegationPolicyHash`. Rotate the session via `POST .../sessions` after a policy change. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type UpdateAgentPairingRequest = { status?: "active" | "paused" | "revoked"; description?: string | null; delegationPolicy?: Partial; }; type UpdateAgentPairingResponse = ItemResponse; ``` ### `DELETE /machines/:machineId/agent-pairings/:pairingId` Revokes the pairing. The current session token is invalidated. Subsequent market-search calls with that token return `AGENT_AUTH_INVALID` or `AGENT_PAIRING_INACTIVE`. If no active pairing exists for a machine, market writes return `AGENT_PAIRING_REQUIRED` (409). If the pairing has open market orders (status not in `confirmed`, `cancelled`, or `failed`), the call returns `OPEN_MARKET_ORDERS` (409) with `orderIds` — cancel or settle those orders first, then retry. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type DeleteAgentPairingResponse = void; ``` ## Session JWT claims The `pairingToken` is an HS256 JWT signed by the orchestrator. Claims: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type AgentPairingSessionClaims = { iss: "peaqos-machine-market"; aud: "machine-agent"; typ: "agent-pairing-session"; ver: 1; jti: string; sid: string; // sessionId pairingId: string; machineId: string; machineIdentityRef?: string | null; agentAddress: string; agentDid: string; agentProvider: string; agentRole: string; delegationPolicyHash: string; delegationPolicy: { /* snapshot of policy at issuance */ }; iat: number; nbf: number; exp: number; }; ``` The orchestrator re-verifies the signature, expiry, pairing status, and `delegationPolicyHash` against the persisted policy on every market write. ## Delegation policy ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type DelegationPolicy = { allowedSkillKeys: string[]; // empty = allow all skills deniedSkillKeys: string[]; // wins over allowed allowedServiceIds: string[]; // empty = allow all listed services deniedServiceIds: string[]; // wins over allowed perTransactionLimit?: number | null; dailySpendLimit?: number | null; currency?: string | null; // ISO 4217 or token symbol }; ``` Enforcement is server-side. Spend-limit violations return `AGENT_SPEND_LIMIT_EXCEEDED` or `AGENT_DAILY_LIMIT_EXCEEDED`. Allow/denylist violations return `AGENT_POLICY_DENIED`. ## Related * [Machine Markets overview](/peaqos/api-reference/machine-markets-overview) * [Machine Markets: Machines & identity](/peaqos/api-reference/machine-markets-machines) * [Machine Markets: Skills, services, search](/peaqos/api-reference/machine-markets-discovery) * [Scale function](/peaqos/functions/scale) # API reference overview Source: https://docs.peaq.xyz/peaqos/api-reference/overview Base URL, authentication, rate limits, error envelope, and endpoint index for the peaqOS MCR API and the Machine Markets API. peaqOS exposes two HTTP/JSON APIs: * **MCR API**: reads, plus one signed write. Machine Credit Ratings, machine profiles, operator fleet data, NFT metadata, and Machine Cards, all backed by on-chain contracts on peaq, plus the [monetization opt-in toggle](/peaqos/api-reference/put-machine-monetization). Documented below. * **Machine Markets API**: powers [Scale](/peaqos/functions/scale). Machine identity proofs, machine records, Machine Agent pairings, the skill registry, the service catalogue, and machine-aware market search. See the [Machine Markets API overview](/peaqos/api-reference/machine-markets-overview). This page covers the MCR API. Writes against peaqOS state (registration, event submission, bridging) happen via the [peaqOS SDK](/peaqos/sdk-reference/sdk-js) and smart contracts. The one exception is the monetization toggle, which is a signature-verified write on this API. ## Tokenomics 2.0 machines `mcr.peaq.xyz` is the **Tokenomics 1.0** server. It addresses machines by `did:peaq:0x
` and integer machine IDs, and it serves the machines onboarded through `IdentityRegistry`. Machines activated under [Economics 2.0](/peaqos/concepts/economics-2-0) have derived `uint256` IDs and `did:peaq:` DIDs; this server does not know them. The 2.0 MCR server is live at `https://mcr-20.peaq.xyz` since 2026-09-05. The SDKs (0.6.0+) and CLI (0.0.8+) point their Tokenomics 2.0 monetization calls at it, and since `@peaqos/peaq-os-sdk` 0.7.0 / `peaq-os-sdk` 0.7.1 (2026-09-11) the query helpers (`queryMcr`, `queryMachine`, `queryOperatorMachines` and their Python equivalents) read it too on a `tokenomics20` client, addressing machines as `did:peaq:` and check its compatibility signal (`GET /.well-known/peaq-monetization`) before signing anything; that signal is published, so monetization reads and writes reach the server. The host exposes the same routes as the 1.0 server minus `/solana/*`, addressed by decimal machine ID: `GET /mcr/did:peaq:`, `GET /machines/`, `GET /machine//monetization`, `GET /operator/did:peaq:0x
/machines` (served from an ownership index synced to the finalized block; while it catches up the route answers 503 with a detail naming the sync position). Address-form DIDs return `400 Invalid machine DID format` there. The SDK query helpers (`queryMcr`, `queryMachine`, `queryOperatorMachines`) are still disabled in Tokenomics mode, so read 2.0 ratings over plain HTTP, use the SDK chain reads (`getMachineActivationState`, `getMachineManagementState`) and `peaqos machine status`, or look the machine up on the [Machine Explorer](https://machines.peaq.xyz). Everything below describes the 1.0 API on `mcr.peaq.xyz`. ## Quick start Pick the setup that matches your workflow: an AI-driven flow via the peaqOS skill, or direct HTTP calls. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-os-cli npx skills add peaqnetwork/peaq-os-skills ``` Auto-detects Claude Code, Cursor, or Windsurf. Then invoke `/peaqos` in Claude Code and ask for an MCR score, machine profile, or operator fleet — the skill picks the right CLI command and the CLI hits this API for you. To target a specific runtime, add `--agent claude-code | cursor | windsurf` — see the [peaqOS AI page](/peaqos/peaqos-ai). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl https://mcr.peaq.xyz/mcr/did:peaq:0xabc123... ``` Endpoint index below. No API key required: MCR data is public. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/mcr/query \ peaq_ros2_interfaces/srv/PeaqosQueryMcr \ "{did: 'did:peaq:'}" ``` `peaq_ros2_peaqos` exposes the MCR API as ROS 2 services. Full reference on [SDK: ROS 2](/peaqos/sdk-reference/ros2/overview); MCR-specific services are listed under [Services → MCR queries](/peaqos/sdk-reference/ros2/services#mcr-queries). ## Base URL Set the environment variable `PEAQOS_MCR_API_URL` to the root of the MCR API server: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export PEAQOS_MCR_API_URL=https://mcr.peaq.xyz ``` For local development against a self-hosted server, use `http://127.0.0.1:8000`. All endpoint paths in this reference are relative to that base URL. ## Authentication There are no API keys or tokens anywhere on this API. Every read is public: MCR scores, machine profiles, and metadata are public on-chain data. The single write, [PUT /machine/\{key}/monetization](/peaqos/api-reference/put-machine-monetization), authorizes the caller by verifying an EIP-191 signature carried in the request body. Still no key or token: the signature is the credential. ## Rate limits The API enforces a limit of **90 requests per minute per IP address**. Requests that exceed the limit receive a `429 Too Many Requests` response. For production deployments with multiple workers, the rate limiter backend can be swapped to Redis. ## Server configuration When self-hosting the MCR API, these environment variables tune caching, storage, the monetization write path, and test-mode behavior. Typical deployments need the contract addresses, RPC, and a persistent directory for the monetization state; the rest have workable defaults. | Variable | Default | Purpose | | :-------------------------- | :-------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `IDENTITY_REGISTRY_ADDRESS` | n/a | Primary contract address. When empty, the server boots in test mode: `/ready` returns 503 with `rpc_connected: false` and all `contracts.*` (including `admin_flags`) reported as `false`. | | `ADMIN_FLAGS_ADDRESS` | n/a | Optional `AdminFlags` contract. Influences MCR responses via `negative_flag` and admin trust overrides. When the service is initialised but this address is unset, `contracts.admin_flags` on `/ready` is **`true`**: the probe skips the optional check and the service stays ready. | | `MCR_CACHE_TTL` | `3600` | MCR response cache TTL in seconds. Set to `0` to disable caching entirely. | | `EVENT_STORE_DB_PATH` | `/tmp/peaqos_events.db` | SQLite path for the delta-sync event cache. Must resolve under `/tmp`, `/data`, or `/var/lib/peaqos`. | | `MONETIZATION_DB_PATH` | `/var/lib/peaqos/monetization.db` | SQLite path for monetization opt-in state. Unlike the rebuildable event cache, this is the single source of truth, so it must live on a persistent volume (allowed prefixes: `/data`, `/var/lib/peaqos`, and `/var/tmp` for local dev). The parent directory must already exist and be writable; the server does not create it and fails startup with guidance when it is missing. | | `CHAIN_ID` | `0` | EVM chain ID bound into the monetization signed message. `0` means adopt the live chain ID from the RPC at startup; a non-zero value that disagrees with the node fails startup loudly. | | `MONETIZATION_SIG_MAX_AGE` | `300` | Seconds a monetization signature stays valid in the past direction. | | `MONETIZATION_SIG_MAX_SKEW` | `30` | Seconds of future clock skew tolerated on a monetization signature. | | `SKIP_PROVISIONED_GATE` | `false` | Dev/demo toggle that skips the Provisioned gate (the 30-day revenue and activity history thresholds) on scoring. The "must be bonded" gate stays on regardless. **Never enable in production.** | ## Error envelope Most error responses use the same JSON shape: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "detail": "" } ``` The `detail` string describes the cause. Common values: | `detail` value | Meaning | | :---------------------------------- | :----------------------------------------------------------- | | `"Service not initialised"` | Server started without contract addresses (test mode) | | `"Chain unavailable"` | RPC call to the node or a contract failed | | `"Empty DID"` | DID path parameter is empty | | `"Invalid Ethereum address format"` | DID or address does not match `^0x[a-fA-F0-9]{40}$` | | `"Machine DID not found"` | DID address has no `machineId` attribute in the DID registry | | `"Machine not registered"` | Machine ID is not present in the IdentityRegistry contract | | `"Machine not found"` | Machine ID does not exist (machines endpoint) | | `"Machine wallet not found"` | Machine wallet is the zero address | | `"Token not found"` | NFT token ID does not map to any machine in MachineNFT | | `"Internal server error"` | Unhandled exception (logged server-side) | ### Coded envelope on the monetization endpoints The two monetization endpoints ([GET](/peaqos/api-reference/get-machine-monetization), [PUT](/peaqos/api-reference/put-machine-monetization)) nest a stable machine-readable code instead of a bare string: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "detail": { "code": "MACHINE_NOT_FOUND", "message": "machine not found" } } ``` Branch on `code`, never on `message`. One case returns neither shape: a body that is not valid JSON gets a `422` whose `detail` is a list of validation objects with no `code` at all. ## Status codes | Code | Meaning | | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `200 OK` | Request succeeded; JSON body returned | | `400 Bad Request` | DID is empty or the address does not match `^0x[a-fA-F0-9]{40}$`; on the monetization endpoints, a malformed body or key (`INVALID_REQUEST`) or a timestamp outside the freshness window (`TIMESTAMP_OUT_OF_WINDOW`) | | `401 Unauthorized` | Monetization PUT only: the signature bytes cannot be recovered to any address (`INVALID_SIGNATURE`) | | `403 Forbidden` | Monetization PUT only: the recovered signer is not the machine's owner, machine wallet, or on-chain operator (`UNAUTHORIZED_SIGNER`) | | `404 Not Found` | The requested DID, machine ID, or token ID does not exist on-chain | | `409 Conflict` | Monetization PUT only: machine not bonded or deactivated (opt-in gates), or a newer signature was already applied (`STALE_SIGNATURE`) | | `422 Unprocessable Entity` | Path, query, or request-body validation failed (FastAPI auto-generated) | | `429 Too Many Requests` | Rate limit exceeded. Responses include slowapi headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) | | `500 Internal Server Error` | Unhandled server exception | | `503 Service Unavailable` | Service not initialised or chain/RPC call failed | ## DID format Endpoints that accept a DID path parameter support two formats: * Full DID: `did:peaq:0xabc123...` * Raw address: `0xabc123...` The server strips the `did:peaq:` prefix internally before querying the chain. The monetization endpoints are the exception. Their `key` parameter takes a full DID or a decimal machine id, and rejects a raw address with `INVALID_REQUEST`. ## MCR API endpoints Machine Credit Rating score, rating, bond status, event counts, and revenue trend for a single machine. Full machine profile with data visibility-dependent fields (private, onchain, or public). Paginated list of machines registered under an operator, with per-machine MCR scores. The same MCR scorecard and operator listing, keyed by a Solana address bound to the peaq identity. NFT metadata for a MachineNFT token. Same response shape as /machine/. peaqOS Machine Card for a machine, including services, registrations, and operator info. Liveness and readiness probes for health checks and monitoring. Current monetization opt-in state for a machine: status, signer, and last update. Signed opt-in or opt-out toggle. The only signature-verified write on this API. ## Machine Markets API Separate surface, separate host (orchestrator deployment). Pairing, delegation policy, skill registry, service catalogue, and market search. Base path, access model, error codes, common types, and the full Machine Markets endpoint surface. # PUT /machine/{key}/monetization Source: https://docs.peaq.xyz/peaqos/api-reference/put-machine-monetization PUT https://mcr.peaq.xyz/machine/{key}/monetization Opt a machine into or out of monetization with a signed request. ## Endpoint ```http theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PUT /machine/{key}/monetization ``` Sets a machine's monetization state (opt in or opt out). This is the MCR API's only signature-verified write endpoint: the caller proves authority with an EIP-191 (`personal_sign`) signature over a canonical message. A machine's owner, its machine wallet, or its on-chain operator may sign (the same authorization triple as `EventRegistry.submitEvent`). No on-chain transaction is made. Opting **in** requires the machine to be bonded and not deactivated. Opting **out** is always allowed (a machine can always exit), so it skips those checks. ## Path parameters Machine DID (`did:peaq:0x...`) or a decimal machine ID (e.g. `42`). ## Request body `true` to opt in, `false` to opt out. Must be a JSON boolean (not `"true"` or `1`). Unix seconds when the message was signed. Must be a positive JSON integer inside the freshness window (see below). The EIP-191 signature: `0x` followed by exactly 130 hex characters (65 bytes). ## Canonical message The client must build and sign this exact string, byte for byte. Fields are LF-separated (`\n`) with no trailing newline: ``` peaq-mcr monetization registry: chain_id: machine_id: opted_in: timestamp: ``` * `registry` is the server's identity registry address, **lowercased**. * `chain_id` is the chain the server reads from. Together with the registry it binds the signature to one deployment, so a signature captured on one chain does not authorize the same action on another. * `machine_id` is the **numeric** machine ID. Even when the URL `key` is a DID, the signed message uses the resolved numeric ID. * `opted_in` is the lowercase string `true` or `false`, matching the body field. Sign it with `personal_sign` (JS) or `eth_account`'s `encode_defunct(text=message)` (Python). ## Freshness window The `timestamp` must fall within the freshness window (by default up to 300 seconds in the past and 30 seconds of future clock skew). Replaying an old signature after the window closes is rejected, and within the window a strictly newer `timestamp` is required to overwrite a previously applied decision (see `STALE_SIGNATURE`). ## Response **200 OK** | Field | Type | Description | | :----------- | :------ | :------------------------------------------------------------------------------------ | | `machine_id` | integer | The resolved numeric machine ID | | `status` | string | The stored state after this write: `"OPTED_IN"` or `"OPTED_OUT"` | | `signer` | string | The address recovered from the signature (EIP-55 checksum) that authorized the change | | `updated_at` | integer | Unix seconds when the server applied the change | ## Error responses Errors use a coded envelope: `{"detail": {"code", "message"}}`. Branch on `code`, never on `message`. The one exception is a body that is not valid JSON: that returns `422` with `detail` as a list of validation objects carrying no `code`. | Status | `code` | Condition | | :----- | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `INVALID_REQUEST` | Malformed body (wrong types, missing field), bad signature format, non-positive timestamp, unparseable `key`, or machine ID out of the int64 range | | 400 | `TIMESTAMP_OUT_OF_WINDOW` | `timestamp` is outside the freshness window | | 401 | `INVALID_SIGNATURE` | The signature bytes cannot be recovered to any address | | 403 | `UNAUTHORIZED_SIGNER` | The recovered signer is not the machine's owner, machine wallet, or on-chain operator | | 404 | `MACHINE_NOT_FOUND` | The `key` does not resolve to an existing machine | | 409 | `MACHINE_NOT_BONDED` | Opt-in only: the machine has not posted a stake bond | | 409 | `MACHINE_DEACTIVATED` | Opt-in only: the machine is deactivated | | 409 | `STALE_SIGNATURE` | A newer or equal `timestamp` has already been applied for this machine | | 500 | `INTERNAL_ERROR` | The state could not be persisted | | 503 | `CHAIN_UNAVAILABLE` | An on-chain read failed while resolving or authorizing | ## Example ```bash bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -X PUT "${PEAQOS_MCR_API_URL}/machine/42/monetization" \ -H 'Content-Type: application/json' \ -d '{ "opted_in": true, "timestamp": 1712000000, "signature": "0x<130 hex chars>" }' ``` ```javascript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const message = [ "peaq-mcr monetization", `registry: ${REGISTRY_ADDRESS.toLowerCase()}`, `chain_id: ${CHAIN_ID}`, `machine_id: ${machineId}`, `opted_in: true`, `timestamp: ${timestamp}`, ].join("\n"); const signature = await wallet.signMessage(message); // EIP-191 personal_sign await fetch(`${PEAQOS_MCR_API_URL}/machine/${machineId}/monetization`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ opted_in: true, timestamp, signature }), }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from eth_account import Account from eth_account.messages import encode_defunct import requests, time timestamp = int(time.time()) message = "\n".join([ "peaq-mcr monetization", f"registry: {REGISTRY_ADDRESS.lower()}", f"chain_id: {CHAIN_ID}", f"machine_id: {machine_id}", "opted_in: true", f"timestamp: {timestamp}", ]) signed = Account.sign_message(encode_defunct(text=message), private_key=key) requests.put( f"{PEAQOS_MCR_API_URL}/machine/{machine_id}/monetization", json={"opted_in": True, "timestamp": timestamp, "signature": signed.signature.to_0x_hex()}, ) ``` **Response** ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "machine_id": 42, "status": "OPTED_IN", "signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "updated_at": 1712000004 } ``` ## SDK and CLI support Both SDKs wrap this endpoint, building and signing the canonical message for you: see the [opt-in SDK reference](/peaqos/sdk-reference/monetization-opt-in) (`optIn` / `optOut` in JS, `opt_in` / `opt_out` in Python). The CLI exposes it as [`peaqos monetize opt-in | opt-out`](/peaqos/cli#peaqos-monetize). ## Related endpoints * [GET /machine//monetization](/peaqos/api-reference/get-machine-monetization) reads the current state without a signature. * [GET /mcr/](/peaqos/api-reference/get-mcr) returns the machine's credit rating. # Stream data marketplace Source: https://docs.peaq.xyz/peaqos/api-reference/stream-marketplace The buyer side of Stream: how a Context Provider discovers, purchases, and downloads machine data sold through peaqOS. This is the **buyer side** of [Stream](/peaqos/functions/stream): how a Context Provider discovers a dataset, purchases access, and downloads it. Producers don't call these routes — the [Edge Agent](/peaqos/sdk-reference/ros2/stream-agent) and the [`stream` SDK](/peaqos/sdk-reference/stream) wrap the producer side. The marketplace runs on the peaqOS orchestration backend, base path `/api/v1`. **Rolling out.** The marketplace API is still being deployed — the `/api/v1/stream/*` endpoints below may not be live on the production backend yet; treat the surface as **beta** (request and response shapes may still change). Since SDK **0.4.0**, payment supports two rails: an on-chain **transfer** (peaq, Base, or Solana — EVM proofs can be RPC-verified against the transaction receipt) and signed x402 authorizations. There is still no escrow for stream purchases; broader on-chain settlement is part of the [omnichain rollout](/roadmap). The supported client path is the SDK **purchases flow** — `createPurchase → createPurchasePaymentIntent → submitPurchasePaymentProof → getPurchaseDelivery` — plus P2P or S3 delivery: see [Stream: Distribution & P2P delivery](/peaqos/sdk-reference/stream-distribution). That flow has its own purchase lifecycle (`delivery_reserving → … → completed`) and auth headers (agent-pairing and machine-agent tokens), documented on the SDK page; the endpoints below are the earlier direct order surface and keep their own status model. ## The buyer journey Browse active listings (`GET /stream/listings`, filter by machine or storage) and fetch detail (`GET /stream/listings/:listingId`). A listing exposes the dataset's machine identity, topic, time range, chunk count, storage type, and price — never plaintext. Fetch the chunk manifests and verify the machine's Ed25519 signatures and that chunk hashes match the dataset's Merkle root. The trust model is in [Data streams](/peaqos/concepts/data-streams). Create an order (`POST /stream/orders`) with the `listingId`, your `buyerId`, and your **32-byte X25519 `buyerPublicKeyHex`** — the key your access grant will be wrapped to. Optionally select a chunk subset. The order starts `payment_pending`. Record payment (`POST /stream/orders/:orderId/payment`) with your payment reference. The order moves to `paid`. (See the settlement caveat above.) Fetch delivery (`GET /stream/orders/:orderId/delivery?buyerId=...`). Once the seller's agent has prepared access, this returns your access grant — the purchased chunk keys re-wrapped to your public key — plus a delivery descriptor (a local delivery URL, or a `walrus://` / `s3://` / `gdrive://` storage reference). In the SDK purchases flow, the equivalent `getPurchaseDelivery` call returns a [P2P delivery session](/peaqos/sdk-reference/stream-distribution#p2p-delivery) — transport, protocols, and a short-lived connect handoff. Download the encrypted chunks from storage — or receive them machine-to-machine with the SDK's `P2PDeliveryReceiver` — unwrap each chunk key with your X25519 private key, and decrypt chunk by chunk (XChaCha20-Poly1305). You only ever receive keys for the chunks you bought. ## Buyer endpoints | Method | Path | Purpose | | :----- | :--------------------------------- | :--------------------------------------------------------------------------------------------------- | | `GET` | `/stream/listings` | Discover active listings (filter by `machineId`, `storageProvider`). | | `GET` | `/stream/listings/:listingId` | Listing detail. | | `POST` | `/stream/orders` | Create an order; binds your `buyerId` + `buyerPublicKeyHex`. | | `GET` | `/stream/orders/:orderId` | Order status (`payment_pending → paid → access_ready / delivery_ready → completed`, or `cancelled`). | | `POST` | `/stream/orders/:orderId/payment` | Record payment (self-reported reference; see caveat). | | `GET` | `/stream/orders/:orderId/delivery` | Fetch the access grant + delivery descriptor. | Listings also surface in the [Machine Markets](/peaqos/concepts/machine-markets) catalogue as `data.stream` services (skill `stream-data-listing`), so an agent can reach the same datasets through the Machine Markets order flow. ## Authentication | Tier | Who | How | | :------- | :------------------- | :--------------------------------------------------------------------------------------------------- | | Platform | Every `/api/v1` call | `x-api-key` (or `Authorization: Bearer`). | | Buyer | Context Provider | Platform key, plus a per-order `buyerId` / `buyerPublicKeyHex` match. | | Agent | Producer | Per-agent token in the request body — see the [Edge Agent](/peaqos/sdk-reference/ros2/stream-agent). | ## Producing data This page is the buyer side. To *sell* data, run the [Stream Edge Agent](/peaqos/sdk-reference/ros2/stream-agent) or use the [`stream` SDK](/peaqos/sdk-reference/stream) — they sign, encrypt, chunk, list, and fulfil orders for you. ## Related * [Stream: Distribution & P2P delivery (SDK)](/peaqos/sdk-reference/stream-distribution) * [Stream function](/peaqos/functions/stream) * [Data streams concept](/peaqos/concepts/data-streams) * [Stream Edge Agent](/peaqos/sdk-reference/ros2/stream-agent) * [Machine Markets API](/peaqos/api-reference/machine-markets-overview) # peaqOS CLI Source: https://docs.peaq.xyz/peaqos/cli Drive activate, qualify, scale, and operator flows from your terminal. The peaqOS CLI wraps the SDK into a terminal surface for the most common machine flows: activating a machine under [Economics 2.0](/peaqos/concepts/economics-2-0), managing its lifecycle and subscription, submitting events, querying credit ratings, running operator-controlled fleets, and the full Scale loop: pairing an AI agent, searching the Machine Markets catalogue, placing and confirming orders. It's the same on-chain and orchestration path as the [JS](/peaqos/sdk-reference/sdk-js) and [Python](/peaqos/sdk-reference/sdk-python) SDKs, scripted. ## Install ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-os-cli peaqos --version ``` Python 3.10 or newer. (0.0.8 installs made before 2026-09-11 resolved `peaq-os-sdk` 0.6.0 or 0.7.0, which import only on 3.12; 0.7.1 runs on 3.10 again.) Ships to PyPI as `peaq-os-cli` and exposes the `peaqos` command. Wraps the [Python SDK](/peaqos/sdk-reference/sdk-python): same on-chain path, scripted. **CLI 0.0.8 (2026-09-04) is the Economics 2.0 release.** `peaqos activate` is one atomic transaction with a new flag set, `peaqos machine` is new, and `peaqos monetize` targets the 2.0 MCR. There is no Tokenomics 1.0 onboarding path left in the CLI; `peaqos qualify` and `peaqos show` cannot address 2.0 machines yet (see below). To drive 1.0 machines from the terminal, pin `pip install "peaq-os-cli<0.0.8" "peaq-os-sdk<0.6.0"` (Python 3.11 or newer). The CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError`. `peaqos --version` prints both the CLI and the underlying `peaq_os_sdk` versions on a single line. Global flags `-v` / `--verbose` and `-q` / `--quiet` toggle DEBUG and ERROR-only logging respectively (mutually exclusive; logs go to stderr). `--orchestration-url ` and `--orch-api-key ` are global overrides for the corresponding env vars on any `peaqos scale ...` invocation. ## Configure The CLI reads the same environment variables as the SDKs. Full table on [Install](/peaqos/install#environment-variables). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public export PEAQOS_PRIVATE_KEY=0x... # Economics 2.0 deployment record: peaq-mainnet or agung-2026-08-28. # Required by activate, machine, and monetize. The contract addresses travel with it. export TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet # Tokenomics 1.0 MCR API, read by qualify and show. Defaults to http://127.0.0.1:8000. export PEAQOS_MCR_API_URL=https://mcr.peaq.xyz ``` Other RPC endpoints are available. See [Public RPC endpoints](/peaqos/install#public-rpc-endpoints). Or scaffold a `.env` interactively with `peaqos init`. ## Commands ### `peaqos init` Interactive wizard that scaffolds a `.env` with the required peaqOS variables. Prompts for network, private key source (`paste`, `generate`, or `wallet`), RPC URL, the Economics 2.0 deployment ID (`TOKENOMICS_DEPLOYMENT_ID`, default `peaq-mainnet` on mainnet and `agung-2026-08-28` on testnet), MCR API URL, Gas Station URL, the six Tokenomics 1.0 contract addresses (labelled legacy, still required by the SDK constructor), and (optionally) `PEAQOS_ORCHESTRATION_URL` + `PEAQOS_ORCH_API_KEY` for Machine Markets. The orchestration key is masked in any echoed or logged output. The `wallet` path creates a new OWS vault wallet and writes `PEAQOS_OWS_WALLET=` instead of `PEAQOS_PRIVATE_KEY`. Writes `.env` with `0o600` permissions and auto-runs `whoami` to verify. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos init peaqos init --force # overwrite existing .env without prompt peaqos init --non-interactive # read all values from env vars ``` ### `peaqos whoami` Read-only command that prints the signing address, network, chain ID, RPC + MCR API URLs, the legacy contract addresses from `.env`, and a `Tokenomics 2.0:` block with the deployment ID, chain ID, and the five addresses the SDK record resolved (these come from the SDK, not from `.env`). Useful as a sanity check after `init`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos whoami ``` ### `peaqos activate` **Upgrade the SDK underneath the CLI.** `InfoDesk` re-pointed `MACHINE_BRIDGE_ADAPTER` on 2026-09-08 and `peaq-os-sdk` 0.7.1 (2026-09-11) carries the new address. A fresh `pip install peaq-os-cli` resolves 0.7.1 and passes preflight. An environment installed before 2026-09-11 fails every `peaq-mainnet` write with `PEER_MISMATCH` until you run `pip install -U peaq-os-sdk`. Onboard a machine in **one atomic transaction**. `MachineStateAndSync.activateMachine` mints the ERC-721, stores the DID document, bonds the subscription tier, and registers the home chain in a single call. Mirrors the [Activate](/peaqos/functions/activate) flow. Requires `TOKENOMICS_DEPLOYMENT_ID`. The transaction sender becomes the machine's **owner and bond payer**. The bond is quoted in PEAQ per tier at the oracle rate; `--payment` chooses what settles it. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Self-owned: your configured signer owns the machine and is its DID controller peaqos activate \ --machine-type Sensor \ --credential-subject-hex 0xdeadbeef \ --manufacturer 0x3333333333333333333333333333333333333333 \ --tier entry \ --did-document ./did.json # Machine-owned, operator-controlled: the machine key signs and pays, you become DID controller peaqos activate ... --for 0xMachine --machine-key ./machine.key # Preview the bond, the voucher credit, and the net PEAQ. Submits nothing. peaqos activate ... --dry-run # Scripted: --yes is the only non-interactive consent peaqos activate ... --json --yes # Settle the PEAQ-quoted bond in USDT, accepting 0.5% conversion movement peaqos activate ... --payment usdt --slippage-bps 50 ``` #### Flags | Flag | Required | Meaning | | :------------------------- | :---------- | :-------------------------------------------------------------------------------------- | | `--machine-type` | yes | Identity domain. Half of what fixes the permanent machine ID. | | `--credential-subject-hex` | yes | `0x`-prefixed identity-anchor bytes. The other half. | | `--manufacturer` | yes | Manufacturer address. Recorded on-chain, never verified by the contract. | | `--tier` | yes | `entry`, `basic`, or `pro`. | | `--did-document` | yes | Path to a UTF-8 JSON DID document (schema below). | | `--for` | no | Machine EOA address. Switches to machine-owned mode; requires `--machine-key`. | | `--machine-key` | no | File holding the machine's `0x`-prefixed hex private key. | | `--payment` | no | `peaq` (default) or `usdt`. Which asset settles the PEAQ-quoted bond. | | `--slippage-bps` | conditional | Accepted conversion movement, `0` to `10000`. Required for `usdt`, rejected for `peaq`. | | `--skip-funding` | no | Skip the balance check, 2FA, and Gas Station funding. | | `--dry-run` | no | Preview and stop. | | `--json` | no | One JSON object on stdout. Never implies consent. | | `--yes` / `-y` | no | Accept the displayed terms. The only non-interactive consent. | `--machine-type` and `--credential-subject-hex` alone determine the machine ID (`uint256(keccak256(abi.encode(machineType, credentialSubject)))`). Neither can change after activation, and the same pair can never be activated twice. Private keys must come from a file. Inline key flags are intentionally unsupported: a file keeps the key out of shell history and `ps` output. **Removed in 0.0.8:** `--doc-url`, `--data-api`, and `--visibility`. They wrote DID attributes after registration; 2.0 records the DID document atomically during activation. Passing any of them exits `1` with a plain error message (no `error_code`). Put documentation and API URLs into the `serviceEndpoints` array of `--did-document`. #### DID document schema Exactly three root fields, all required. Unknown fields, duplicate keys, and a root `id` or `controller` are rejected: `id` is computed on-chain and `controller` is set by the CLI from the mode. ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "verificationMethods": [ { "id": "#key-1", "methodType": "Ed25519VerificationKey2020", "controller": "0x1111111111111111111111111111111111111111", "publicKeyMultibase": "z6MkiExamplePublicKey" } ], "authentication": [0], "serviceEndpoints": [ { "id": "#telemetry", "serviceType": "TelemetryService", "serviceEndpoint": "https://machine.example/telemetry" } ] } ``` Each `authentication` entry is an index into `verificationMethods`. The CLI bounds-checks them because the contract stores them unchecked at mint. #### Ownership modes | Mode | Who signs and pays | Controller | | :------------------------------------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------- | | Self-owned (no `--for`) | Your configured signer owns the machine and pays the bond | Same address | | Machine-owned, operator-controlled (`--for` + `--machine-key`) | The **machine** signs, owns the NFT, and pays gas and the bond | Your operator address (`PEAQOS_PRIVATE_KEY`), which signs nothing in this mode | In machine-key mode the CLI prints the changed rights before asking: the machine wallet can transfer the NFT and rotate or clear the controller; the operator can run lifecycle, subscription, and DID actions but cannot transfer the NFT or change the controller. There is no operator-sponsored activation; `registerFor` has no 2.0 equivalent. #### Paying in USDT The bond, the voucher credit, and the net amount stay in PEAQ; only settlement differs. The CLI shows the USDT token, the quote, the accepted slippage, and the resulting **maximum USDT**, then submits that exact maximum. The allowance goes to `SubscriptionTokenProvisionPool`, not to `MachineSubscription`. A bond covered entirely by voucher credit converts and transfers nothing. #### Output, exit codes, and `error_code` Progress, the preview, and prompts go to stderr; stdout carries only the final summary (or one JSON object with `--json`). Machine IDs and every unbounded chain integer are **decimal strings** in JSON, never numbers. ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "status": "activated", "mode": "self-owned", "deployment_id": "peaq-mainnet", "chain_id": "3338", "machine_id": "57896044618658097711785492504343953926634992332820282019728792003956564819975", "tier": "entry", "owner": "0xDC5b20847F43d67928F49Cd4f85D696b5A7617B5", "controller": "0xDC5b20847F43d67928F49Cd4f85D696b5A7617B5", "bond_amount": "909153310068100128", "voucher_credit": "0", "net_peaq_amount": "909153310068100128", "transaction_hash": "0xabab...", "is_homed_locally": true } ``` The four CLI-wide exit codes apply. Once input validation has passed, every `activate` outcome is a JSON report (with `--json`) whose failures carry a stable `error_code`, bracketed in human output; success and preview reports set `error_code` to `null` and report a `status` instead: `preview`, `activated`, `already_active`, or `pending`. Input and flag errors before that point exit `1` with a plain message and no report. The codes to know: | Exit | `error_code` examples | | :--- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | none; `status` is `activated` or `already_active` | | `1` | `INVALID_INPUT`, `CANCELLED_BEFORE_SUBMIT` (confirmation declined), `SPONSORED_UNSUPPORTED`, `INVALID_TIER` | | `2` | `ORACLE_UNPRICED` (contracts reachable, no PEAQ price committed), `INSUFFICIENT_PEAQ`, `QUOTE_MOVED`, `TX_REVERTED`, `EVENT_MISMATCH`, `PENDING`, `ALREADY_ACTIVATED_RACE` | | `3` | `TOKENOMICS_NOT_CONFIGURED`, `DEPLOYMENT_UNKNOWN`, `CHAIN_MISMATCH`, `PEER_MISMATCH`, `ADDRESSES_UNSET` (network supported, contracts not deployed there) | #### Pending transactions and `peaqos.log` A submitted transaction whose receipt does not arrive is reported as `PENDING` at exit `2` with its hash, not as a failure. It may still mine. Every submitted hash is appended to `./peaqos.log` (mode `0600`) **before** the receipt wait. Re-running the same command reconciles the recorded hash instead of resubmitting; a hash with no receipt blocks resubmission regardless of age. Never submit a second activation for the same machine, and never delete `peaqos.log` while a transaction is outstanding. ### `peaqos machine` New in 0.0.8. Everything after onboarding: lifecycle, subscription payments, ERC-721 ownership, DID updates, and relocation status. Requires `TOKENOMICS_DEPLOYMENT_ID`. ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos machine status MACHINE_ID peaqos machine suspend MACHINE_ID peaqos machine resume MACHINE_ID peaqos machine subscription activate MACHINE_ID --tier TIER --payment RAIL [--slippage-bps N] peaqos machine subscription renew MACHINE_ID --payment RAIL [--slippage-bps N] peaqos machine approve MACHINE_ID OPERATOR peaqos machine approve-all OPERATOR --allow|--revoke peaqos machine transfer MACHINE_ID TO [--unsafe] [--data-hex HEX] peaqos machine did set-controller MACHINE_ID CONTROLLER peaqos machine did clear-controller MACHINE_ID peaqos machine did set-verification-methods MACHINE_ID --file PATH peaqos machine did set-authentication MACHINE_ID [--index N]... | --clear peaqos machine did set-services MACHINE_ID --file PATH peaqos machine relocation status MACHINE_ID --destination-rpc-url URL --destination-deployment-id ID ``` Every write accepts `--yes` and `--json`; every read accepts `--json`. Machine IDs are full-width `uint256`: pass them as canonical unsigned decimal (no `0x`, no leading zeros) and read them back as decimal strings. Every write runs the same sequence: validate locally, reconcile the journal (a pending transaction for the same action and machine blocks rather than repeats), ask the SDK for a preview (chain, contract, method, current state, intended effect), show it and ask, then submit once and record the hash before waiting for the receipt. Reruns reconcile the recorded hash and never resubmit. Details that matter: * **Who may sign.** Suspend, resume, renew, and DID updates: owner or controller. `set-controller` and `clear-controller`: owner only. Transfers: standard ERC-721 authority. Points and credits from a renewal land on the **owner** even when the controller pays. * **`approve-all` is not scoped to one machine.** It grants the operator every MachineRegistry machine the signer owns, including ones activated later. * **`transfer` is safe by default** (`safeTransferFrom`). `--unsafe` selects `transferFrom`, which can strand the NFT in an incompatible contract. Transfer does not rotate the DID controller. * **DID setters replace whole arrays.** The file or index list you pass is the complete new state. Authentication indices point into the verification-method array by position. `--index` and `--clear` are mutually exclusive and one is required. * **Renewal takes no `--tier`.** The contract renews at the stored tier and extends from the current period end, not from now. `--payment usdt` requires `--slippage-bps`. * **Relocation is read-only.** `relocation status` reports `pending`, `arrived`, `completed`, `cancelled`, or `conflicting`. Initiation and cancellation are absent until the protocol publishes a fee quote; relocation is also disabled on chain today. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos machine status 57896044618658097711785492504343953926634992332820282019728792003956564819975 --json peaqos machine suspend --yes peaqos machine subscription renew --payment usdt --slippage-bps 50 --yes peaqos machine transfer 0xRecipient --yes ``` Exit codes and `error_code` values are the ones documented under [`peaqos activate`](#peaqos-activate): one taxonomy for both. **`qualify` and `show` cannot address 2.0 machines in CLI 0.0.8.** The CLI accepts only `did:peaq:0x
` DIDs, while the SDK in Tokenomics mode (`peaq-os-sdk` 0.7.1, which now serves 2.0 queries from `mcr-20.peaq.xyz` and enables 2.0 event submission) accepts only `did:peaq:`. With `TOKENOMICS_DEPLOYMENT_ID` set, a decimal DID is rejected by the CLI and a hex DID by the SDK, so `qualify mcr`, `show machine` and `show operator machines` exit with a validation error. Mode-aware DID handling is in the next CLI release (0.0.9, in progress on 2026-09-11). To read 1.0 machines, unset `TOKENOMICS_DEPLOYMENT_ID` in a separate shell, or pin `peaq-os-cli<0.0.8` together with `peaq-os-sdk<0.6.0`. Until 0.0.9, submit 2.0 events and read 2.0 ratings through the SDKs. ### `peaqos qualify event` Submit a revenue or activity event to the EventRegistry. Wraps [`submit_event`](/peaqos/sdk-reference/sdk-python#submit_event). See the [Submit events guide](/peaqos/guides/submit-events) for trust levels and cross-chain patterns. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # HK$1.23 (123 cents) revenue event peaqos qualify event \ --machine-id 42 \ --type revenue \ --value 123 \ --ts 2026-04-23T12:00:00Z \ --trust self \ --source-chain peaq ``` `--value` is an ISO 4217 minor-unit integer: cents for USD/HKD, whole units for JPY/KRW/VND. `HK$1.23 → --value 123`, `¥100 → --value 100`. The CLI defaults `--currency` to `USD` for revenue events and to `""` for activity events; pass `--currency` explicitly to override. The MCR API converts the value to USD using FX at the event timestamp. | Flag | Required | Meaning | | :--------------- | :---------- | :--------------------------------------------------------------------------------------------------------------- | | `--machine-id` | yes | Positive integer machine ID. | | `--type` | yes | `revenue` or `activity`. | | `--value` | yes | Non-negative integer in the currency's minor unit. | | `--ts` | yes | Unix seconds or ISO 8601 with timezone (`Z` or `+hh:mm`). Must be on or before block time. | | `--currency` | no | ISO 4217 code, 3-10 uppercase alphanumerics (`^[A-Z0-9]{3,10}$`). Default: `USD` for revenue, `""` for activity. | | `--trust` | no | `self` (default), `onchain`, or `hardware`. | | `--source-chain` | no | `same` (default), `peaq`, or `base`. | | `--source-tx` | conditional | 32-byte hex tx hash. **Required** when `--trust onchain`. | | `--raw-data` | no | Path to a file; bytes are hashed and stored as the event data hash. | | `--metadata` | no | Path to a file; bytes are attached as on-chain metadata. Absent = empty bytes. | Human output on success: ``` Event submitted. Machine ID: 42 Type: revenue Value: 123 Trust: self-reported Tx: 0x3f4a8c1e2d9b7f05a6c3e8d1f4b2a7c9e0d5f3b1a8e2c6d9f7b4a1e3c5d8f2b4 Data Hash: 0xa1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890 ``` ### `peaqos qualify mcr` Fetch a machine's credit rating from the [MCR API](/peaqos/api-reference/get-mcr). Wraps [`query_mcr`](/peaqos/sdk-reference/sdk-python#query_mcr). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos qualify mcr did:peaq:0x9a5F1E244c15e491Ae571c5bF77fDD836ddc37C5 peaqos qualify mcr did:peaq:0x9a5F... --json ``` The DID must match `did:peaq:0x` plus 40 hex characters. `--json` emits the raw SDK response on stdout with no banner or prose: useful for `jq` and scripting. Human output: ``` MCR for did:peaq:0x9a5F1E244c15e491Ae571c5bF77fDD836ddc37C5 Rating: A Score: 82 / 100 Bond Status: bonded Events: Total: 150 Revenue: 120 Activity: 30 30-Day Revenue: up Last Updated: 2026-04-20T14:30:00Z FX Degraded: no ``` `revenue_trend` is one of `up`, `stable`, `down`, or `insufficient` (returned when there isn't enough revenue history to compute a trend). `FX Degraded:` reflects the top-level `mcr_degraded` field — `yes` when one or more scored events used a stale or unavailable FX snapshot, `no` otherwise. `--json` output (raw SDK `MCRResponse`): ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "did": "did:peaq:0x9a5F1E244c15e491Ae571c5bF77fDD836ddc37C5", "machine_id": 42, "mcr_score": 82, "mcr": "A", "mcr_degraded": false, "bond_status": "bonded", "negative_flag": false, "event_count": 150, "revenue_event_count": 120, "activity_event_count": 30, "revenue_trend": "up", "total_revenue": 1542075, "average_revenue_per_event": 12851, "last_updated": 1745152200 } ``` ### `peaqos show machine` Fetch a full machine profile: Machine ID, operator, DID attributes, MCR snapshot, recent events. Wraps [`query_machine`](/peaqos/sdk-reference/sdk-python#query_machine). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos show machine did:peaq:0xabc1230000000000000000000000000000000001 peaqos show machine did:peaq:0xabc... --json ``` ### `peaqos show operator machines` List the machines registered under a proxy operator, with MCR per machine. Wraps [`query_operator_machines`](/peaqos/sdk-reference/sdk-python#query_operator_machines). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos show operator machines did:peaq:0xProxyAddress peaqos show operator machines did:peaq:0xProxyAddress --json ``` ### `peaqos wallet` Manage OWS-format wallets in the local encrypted vault at `~/.ows/wallets/`. Requires the `[ows]` extra: `pip install 'peaq-os-cli[ows]'` (quote the bracketed extra so zsh does not glob it). The vault passphrase is read from `OWS_PASSPHRASE` when set, otherwise prompted interactively. Subcommands wrap the SDK static helpers documented on [Wallets (OWS)](/peaqos/wallets). | Subcommand | Purpose | | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `peaqos wallet create [--words 12\|24] [--json]` | Generate a new BIP-39 mnemonic and derive accounts for every supported chain. The mnemonic is not displayed; back up via `wallet export`. | | `peaqos wallet import --mnemonic [--index N] [--json]` | Import from an existing BIP-39 mnemonic (hidden prompt). `--index` selects the derivation index (default `0`). | | `peaqos wallet import --private-key-file [--json]` | Import a raw EVM private key from file. peaq-only; other chains synthesize on read where possible. | | `peaqos wallet list [--json]` | Compact table of all wallets: Name, ID, peaq Address, Key Type, Created. | | `peaqos wallet show [--json]` | Wallet metadata plus the full multi-chain address table. | | `peaqos wallet export ` | Reveal the recovery phrase / private key. Requires interactive confirmation. Secret prints to stdout; warning prints to stderr. | | `peaqos wallet delete ` | Securely delete a wallet (file overwritten with random bytes before unlink). Requires confirmation. | | `peaqos wallet use ` | Set the active wallet. Writes `PEAQOS_OWS_WALLET=` into `.env` in the current directory. Subsequent commands that call `load_client()` will sign with this wallet. | When `PEAQOS_OWS_WALLET` is set, `load_client()` resolves the wallet from the vault using `OWS_PASSPHRASE` and skips `PEAQOS_PRIVATE_KEY` entirely. If both are set, the wallet wins. ### `peaqos stream` The data-stream command group. The crypto core is offline: `publish` turns a source file into signed, encrypted chunks on disk; `grant` re-wraps the chunk keys for a buyer; `consume` decrypts and reassembles the original data on the buyer's side. Around it sits the paid flow, new in CLI `v0.0.6`: `distribute` waits for a buyer's payment confirmation and delivers access files to S3, `pay` transfers tokens to the seller on peaq, Base, or Solana, and `payproof` submits proof for a transfer completed elsewhere. Discovery and ordering happen through the [Stream data marketplace](/peaqos/api-reference/stream-marketplace); the SDK-level [P2P delivery channel](/peaqos/sdk-reference/stream-distribution#p2p-delivery) has no CLI flag yet. #### `peaqos stream publish` Chunk, encrypt, and sign a data file into a local output directory. Each chunk gets a fresh key wrapped (X25519) for three recipients — owner, operator, machine — and the chain is signed Ed25519. Writes `chunk-{i}.json` (envelope) + `chunk-{i}.bin` (ciphertext) per chunk, plus a `manifest.json` (`peaq.stream.chunks.v1`). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos stream publish \ --input ./telemetry.bin \ --output-dir ./out \ --owner-public-key 0x<64hex> \ --operator-public-key 0x<64hex> \ --machine-public-key 0x<64hex> \ --signing-key-file ./machine-ed25519.key \ --machine-did did:peaq:0xMACHINE \ --machine-key-id did:peaq:0xMACHINE#keys-1 ``` Required: `--input` (file path or `http(s)` URL), `--output-dir` (created if missing), `--owner-public-key` / `--operator-public-key` / `--machine-public-key` (X25519, 64 hex), `--signing-key-file` (Ed25519 private key file), `--machine-did`, `--machine-key-id`. Optional: `--chunk-size` (bytes, default `262144`), `--json` (emit the manifest to stdout), and S3 upload — `--s3 s3://bucket/prefix/`, `--s3-region`, `--s3-endpoint` (for MinIO / R2 / S3-compatible stores). S3 needs the extra (`pip install "peaq-os-cli[s3]"`) and credentials via `PEAQOS_S3_ACCESS_KEY_ID` + `PEAQOS_S3_SECRET_ACCESS_KEY` (or the standard boto3 chain); each chunk's `storageRef` is rewritten to its `s3://` URI. The human summary goes to stderr; the manifest path prints to stdout for piping. Exit codes: `0` success, `1` validation (bad key hex, wrong length, missing input file, `StreamValidationError`/`StreamSigningError`), `2` URL download or S3 upload failure. #### `peaqos stream grant` Grant a buyer decryption access to a published chunk chain — fully offline. Reads the chunk envelopes, unwraps each chunk key with the owner's X25519 private key, re-wraps for the buyer, and writes `peaq.stream.buyer-access.v1` files (sharded by size). This is a local re-key, not an on-chain access grant. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos stream grant \ --chunk-dir ./out \ --buyer-public-key 0x<64hex> \ --buyer-id did:peaq:0xBUYER \ --owner-private-key-file ./owner-x25519.key \ --output-dir ./buyer-access ``` Required: `--chunk-dir` (published envelopes), `--buyer-public-key` (X25519), `--buyer-id`, `--owner-private-key-file`, `--output-dir`. Optional: `--max-file-size` (bytes per access file, default `512000`), `--json` (summary with per-file listing). Exit codes: `0` success, `1` validation (invalid keys, no chunk files, malformed chunk, `--max-file-size <= 0`), `2` key-commitment mismatch — wrong owner key. #### `peaqos stream consume` Buyer-side: decrypt a purchased chunk chain and reassemble the original data. Reads the chunk envelopes, the encrypted `.bin` blobs, and the buyer access files (filtered to `--buyer-id`), verifies the chain (unless `--skip-verify`), decrypts each chunk with the buyer's X25519 private key, and writes the result to `--output`. Two input modes: * **Local** (offline) — `--chunk-dir`, `--access-dir`, and `--data-dir` point at directories already on disk. * **Remote** (`v0.0.6`+) — `--download-url` points at an HTTP/HTTPS **self-contained release package** bundling the envelopes, `.bin` blobs, and access files, exposed as a `manifest.json` file listing or a ZIP archive. The package is downloaded into a work directory (`--work-dir`, or a temp dir cleaned up after success unless `--keep-files`; preserved on any error for debugging), then the same verify → decrypt → reassemble pipeline runs. `--download-url` is mutually exclusive with the three directory flags. `--download-url` does **not** consume the pre-signed URL from `peaqos stream distribute` directly — that URL delivers only the **first** buyer-access file, while the chunk envelopes and ciphertext stay behind each chunk's `storageRef`. A full distribute → consume roundtrip needs a self-hosted bundle as described above; a buyer-side S3 receiver is not shipped yet. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Local mode — directories already on disk peaqos stream consume \ --chunk-dir ./out \ --access-dir ./buyer-access \ --data-dir ./out \ --buyer-private-key-file ./buyer-x25519.key \ --buyer-id did:peaq:0xBUYER \ --output ./recovered.bin # Remote mode — fetch a self-contained release bundle peaqos stream consume \ --download-url "https://bundles.example.com/releases/ord-001/" \ --buyer-private-key-file ./buyer-x25519.key \ --buyer-id did:peaq:0xBUYER \ --output ./recovered.bin ``` Required: `--buyer-private-key-file` (buyer X25519 private key), `--buyer-id` (must match the `recipientId` in the access files), `--output` (reassembled plaintext path), and — in local mode — `--chunk-dir` (envelopes), `--access-dir` (buyer access files), `--data-dir` (encrypted `.bin` blobs). Optional: `--download-url` (remote release package; a query token on the URL is preserved when fetching each file), `--work-dir` / `--keep-files` (remote mode only), `--skip-verify` (skip chain verification; debugging only), `--json` (summary to stdout: `output`, `totalBytes`, `chunkCount`, `sourceHash`, `buyerId`, `verified`). Exit codes: `0` success, `1` validation (empty `--buyer-id`, unreadable key, missing input files or dirs, `--download-url` combined with a directory flag or with a non-http(s) scheme), `2` decryption, integrity, or download failure (HTTP error, timeout, invalid ZIP). The buyer-side messages are specific: a wrong or ungranted key gives `Decryption failed for chunk — access not granted for this buyer private key`; a mismatched `--buyer-id` gives `No buyer access for chunk ()`; a tampered chunk gives `Data integrity check failed for chunk — plaintext hash mismatch`. #### `peaqos stream distribute` Seller-side, new in `v0.0.6`. Listen for a buyer's payment confirmation, then automatically generate buyer access files (same re-key as `stream grant`) and deliver them to S3 under `{prefix}{buyer_id}/`, returning a pre-signed download URL. Polls `--confirmation-url` every `--poll-interval` seconds (default `30`) until the endpoint reports a confirmed payment or `--timeout` seconds (default `3600`) elapse. The confirmation endpoint must return JSON with at least `status`, `buyer_id`, and `buyer_public_key_hex`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos stream distribute \ --chunk-dir ./out \ --owner-private-key-file ./owner-x25519.key \ --confirmation-url https://api.example.com/orders/ord-001/status \ --order-id ord-001 \ --delivery s3 \ --s3 s3://my-bucket/distributes/ ``` Required: `--chunk-dir` (published envelopes from `stream publish`), `--owner-private-key-file` (the X25519 key used at publish time — one line of 64 hex, optional `0x` prefix), `--confirmation-url`, `--order-id`, `--delivery` (currently only `s3`), and `--s3` (bucket path). Optional: `--poll-interval`, `--timeout`, `--s3-region`, `--s3-endpoint` (MinIO / R2 / S3-compatible), `--presign-expiry` (seconds, default `3600`), `--max-file-size` (bytes per access file, default `512000`), `--json`. S3 needs the extra (`pip install "peaq-os-cli[s3]"`) and credentials via `PEAQOS_S3_ACCESS_KEY_ID` + `PEAQOS_S3_SECRET_ACCESS_KEY` or the standard boto3 chain. Exit codes: `0` success, `1` validation (no chunk files, unreadable key, non-positive interval/timeout, `--delivery s3` without `--s3`), `2` confirmation timeout or S3 upload failure, `3` `boto3` missing. In the SDK, the same loop is [`PollingConfirmationProvider` + `distributeData`](/peaqos/sdk-reference/stream-distribution#seller-getting-paid-and-preparing-access) — where a [P2P delivery channel](/peaqos/sdk-reference/stream-distribution#p2p-delivery) can replace S3. #### `peaqos stream pay` Buyer-side, new in `v0.0.6`. Transfer tokens on-chain to a seller — native or ERC-20/SPL on `peaq`, `base`, or `solana` — and optionally submit the transaction hash as payment proof in the same run. With `--confirmation-url`, proof is submitted right after the transfer; without it, only the transfer executes and the CLI prints the matching `peaqos stream payproof` command. The tx hash is always written to stdout before the proof step, so it survives a failed proof submission. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Base USDC transfer with proof submission peaqos stream pay \ --seller-address 0xSeller... \ --amount 1.0 \ --chain base \ --order-id order-002 \ --rpc-url https://mainnet.base.org \ --token-address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \ --confirmation-url https://api.example.com/payments/proof ``` Required: `--seller-address` (EVM `0x...` or Solana base58), `--amount` (human-readable, e.g. `"10.5"`), `--chain`, `--order-id`. Optional: `--confirmation-url`, `--token-address` (ERC-20 contract or SPL mint; omit for the native token), `--token-decimals` (override for tokens outside the well-known registry), `--rpc-url` (required for `base` and `solana`), `--private-key-file` (falls back to `PEAQOS_PRIVATE_KEY`), `--json` (`proof` is `null` when no confirmation URL was given). Exit codes: `0` success, `1` validation or signing failure (messages never contain key material; Solana support needs `pip install "peaq-os-sdk[solana]"`), `2` insufficient balance, revert, or proof HTTP failure, `3` missing config. #### `peaqos stream payproof` Buyer-side, new in `v0.0.6`. Submit payment proof for a transfer completed outside `peaqos stream pay`, or retry a proof step that failed. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos stream payproof \ --tx-hash 0xabc123... \ --order-id order-001 \ --confirmation-url https://api.example.com/payments/proof \ --chain peaq \ --payer-address 0xPayer... \ --payee-address 0xSeller... \ --amount 10.5 ``` Required: `--tx-hash` (EVM hash or Solana signature), `--order-id`, `--confirmation-url`, `--chain`, `--payer-address`, `--payee-address`, `--amount` (must match the original transfer). Optional: `--token`, `--token-address`, `--json`. Exit codes: `0` success, `1` validation, `2` proof HTTP failure, `3` missing config. ### Solana payments There is no `peaqos solana` command group, but since `v0.0.6` the CLI signs Solana transfers natively where it matters: `peaqos stream pay --chain solana` sends native or SPL transfers itself (requires `--rpc-url` and, for SPL, the mint via `--token-address`; install with `pip install "peaq-os-sdk[solana]"`). Solana-quoted **Machine Market** orders are still paid externally — complete the SPL transfer with your own Solana wallet, then pass `--payment-tx-hash` to `peaqos scale order` so the orchestrator can verify it against the quote. ### `peaqos monetize` Manage a machine's **Economics 2.0 monetization** decision in the MCR: `status` is a public read; `opt-in` and `opt-out` are signed, off-chain decisions. Thin wrappers over the SDK's [opt-in client](/peaqos/sdk-reference/monetization-opt-in): the SDK runs the compatibility check, EIP-191 signing, retries, HTTP, and response validation; the CLI adds parsing, prompts, and output. **Live on `peaq-mainnet` since 2026-09-05.** The 2.0 MCR at `https://mcr-20.peaq.xyz` publishes the `/.well-known/peaq-monetization` signal and serves the mirrored 2.0 machines; `peaqos monetize status ` returns `PENDING` for a machine that has never opted in (checked 2026-09-05 14:00 UTC, reads only). `agung-2026-08-28` has no paired MCR and exits `3` with `DEPLOYMENT_UNAVAILABLE`. To toggle monetization on a Tokenomics 1.0 machine, pin `pip install "peaq-os-cli<0.0.8" "peaq-os-sdk<0.6.0"`, which talks to `mcr.peaq.xyz`. The CLI pin alone pulls SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError` before it reaches the server. ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos monetize status KEY [--timeout-seconds 30.0] [--max-unavailable-retries 2] [--unavailable-retry-delay-seconds 1.0] [--json] peaqos monetize opt-in KEY [same options] [--yes] [--json] peaqos monetize opt-out KEY [same options] [--yes] [--json] ``` `KEY` is a canonical decimal machine ID or `did:peaq:`. Address DIDs (`did:peaq:0x…`), leading zeros, signs, hex, and exponents are rejected locally: there is no translation from a 1.0 address DID to a 2.0 machine ID. Every command requires `TOKENOMICS_DEPLOYMENT_ID`. The SDK resolves the MCR URL, chain ID, `MachineRegistry`, and API version from that deployment and the server's live compatibility signal (`GET /.well-known/peaq-monetization`). `PEAQOS_MCR_API_URL`, `IDENTITY_REGISTRY_ADDRESS`, and `PEAQOS_RPC_URL` are not read; an explicit `--api-url` exits `3`. `status` needs no signer; writes need `PEAQOS_PRIVATE_KEY`, and the recovered address must be the machine's current **owner or DID controller** (a machine-wallet-only key or a 1.0 operator key is not authorized). Before a write the CLI reads the current state; an already-satisfied state returns without a prompt, signature, or PUT. Only `503 MACHINE_UNAVAILABLE` is retried. A timeout during a PUT is ambiguous (the MCR may have applied it): rerun the same command, which reads first and sends no second PUT if the state is already there. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos monetize opt-in did:peaq:57896044618658097711785492504343953926634992332820282019728792003956564819975 peaqos monetize status 57896044618658097711785492504343953926634992332820282019728792003956564819975 --json ``` ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "machine_id": "57896044618658097711785492504343953926634992332820282019728792003956564819975", "status": "OPTED_IN", "signer": "0x7099...79C8", "updated_at": 1783944004 } ``` 2.0 state starts at `PENDING`; Tokenomics 1.0 decisions and signatures are not imported. | Situation | Exit | | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :--- | | Invalid or non-canonical `KEY` or option value; declined prompt; non-TTY write without `--yes` | `1` | | Signer not owner or controller (`UNAUTHORIZED_SIGNER`); ineligible opt-in (`MACHINE_NOT_BONDED`, `MACHINE_DEACTIVATED`); network, timeout, or API rejection | `2` | | Missing or unknown `TOKENOMICS_DEPLOYMENT_ID`; incompatible or unreachable MCR (`MONETIZATION_API_INCOMPATIBLE`); explicit `--api-url` | `3` | ### `peaqos monetize provision` Provision an **opted-in** machine as a compute provider node from a schema-driven manifest, entirely from the terminal. Thin wrappers over the SDK's [manifest runner](/peaqos/sdk-reference/provisioning): every command, secret redaction, and verification probe runs inside the SDK; the CLI adds prompts, terminal rendering, and the resume state file. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos monetize provision run --machine [--mode manual|auto] [--grant-sudo] peaqos monetize provision preflight [--inputs ] peaqos monetize provision verify [--state-file ] [--json] ``` * `` is the manifest provider key (for example `akash`). Requires `PEAQOS_MANIFEST_REPO_URL` or `--manifest-repo`. * `run` walks the full flow: monetization pre-check (anything but `OPTED_IN` stops before the manifest is even fetched), manifest fetch pinned by sha256, input collection (`--inputs` file plus hidden prompts for secrets), blocking pre-flight, provisioning with a state checkpoint after each step, and verification probes that alone decide success. * `--mode manual` (default) confirms each command; `--mode auto` runs unattended and requires `--grant-sudo`, scoped to the manifest's `allowedCommands`. Owner-action handoffs (funding, DNS, signing) always pause, even with `--yes`. * `--machine` takes a decimal machine ID or `did:peaq:` (0.0.8; address DIDs are rejected), so the machine wallet address for the manifest's commission context must come from `PEAQOS_MACHINE_WALLET_ADDRESS`. Unset it before provisioning a different machine. The monetization pre-check goes through the 2.0 SDK read and is blocked by the same missing 2.0 MCR as `peaqos monetize` today. * `--resume` continues an interrupted run from the state file (default `./peaqos-provision-state.json`, written atomically with `0600`). Non-secret inputs are restored; secrets are re-prompted, never persisted. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Full manual run PEAQOS_MACHINE_WALLET_ADDRESS=0x9a5F...37C5 peaqos monetize provision run akash \ --machine 57896044618658097711785492504343953926634992332820282019728792003956564819975 --inputs ./akash-inputs.yaml # Re-check a provisioned node against the pinned manifest peaqos monetize provision verify akash --json | jq '.[] | select(.passed == false)' ``` ### `peaqos scale` Machine Market orchestration commands. Pair an AI agent to an activated machine, search the curated catalogue, and drive the full purchase loop. The `peaqos scale` surface (CLI `v0.0.5`+): `agent pair`, `machine onboard` (plus `machine status` and `machine list`), `search`, and the `scale order` family — place (dispatched from a service UUID), `status`, `list` with cursor pagination, `received`, `dispute`. CLI `v0.0.6` adds the x402 payment rail to order placement. #### Setup Two new env vars, both optional in `load_client()`. Override at any point with the root-level flags `--orchestration-url` / `--orch-api-key`. | Env var | CLI override | Purpose | | :------------------------- | :-------------------------- | :------------------------------------ | | `PEAQOS_ORCHESTRATION_URL` | `--orchestration-url ` | Machine Markets API base URL | | `PEAQOS_ORCH_API_KEY` | `--orch-api-key ` | Platform API key (`x-api-key` header) | `peaqos init` prompts for both during the wizard and writes them as active `.env` lines (not commented placeholders). The API key is entered at a hidden prompt (not echoed to the terminal), then written to `.env` in plaintext, so treat the file as a secret: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Machine Market orchestration PEAQOS_ORCHESTRATION_URL=https://orchestration.peaq.xyz PEAQOS_ORCH_API_KEY= ``` `peaqos init --non-interactive` reads both from existing env vars. The pairing token returned by `agent pair` is the credential for agent-side commands. Save it once to a single-line file with `chmod 600` and reference it via `--pairing-token-file`. #### `peaqos scale machine onboard` Operator-facing. Walks a machine through the four-step orchestration onboard: request an identity challenge, sign with the DID controller key, register the machine with proof attached, then activate (unless `--skip-activate`). Hits `POST /api/v1/machine-identity/challenges` and `POST /api/v1/machines`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos scale machine onboard \ --identity-ref did:peaq:0xMachineWallet \ --display-name "rover-014" \ --owner-id op_xyz \ --machine-type qvac \ --runtime-profile machine-runtime-agent ``` Required: `--identity-ref`, `--display-name`, `--owner-id`, `--machine-type`, `--runtime-profile`. Optional: `--capabilities` (CSV), `--skill-keys` (CSV), `--labels` (`KEY=VALUE,...`), `--identity-signature-file`, `--identity-key-file`, `--skip-activate`, `--yes`, `--json`. Supply exactly one of the two signing inputs: `--identity-signature-file` if you have already signed the challenge externally (one-line hex signature), or `--identity-key-file` if you want the CLI to sign in-process with the DID controller's private key. Either is enough; supplying both is rejected. Exit codes: `0` happy path, `1` input error, `2` server / proof error (`MACHINE_IDENTITY_EXISTS`, `MACHINE_IDENTITY_PROOF_INVALID`, `PEAQOS_IDENTITY_UNAVAILABLE`). #### `peaqos scale agent pair` Operator-facing. Pairs an AI agent to a machine via a three-step challenge-sign flow. Returns a one-time signed-JWT `pairingToken`. Internally the command: 1. Calls `client.orchestration.createAgentPairingChallenge(machineId, params)` for a server-issued challenge keyed to `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`. 2. The Machine Agent signs the returned challenge message (EIP-191) with the wallet key behind `agentAddress`. Supply the signature via `--agent-signature-file `. 3. Calls `client.orchestration.createAgentPairing(machineId, paramsWithProof)` to persist the pairing and issue the session JWT. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos scale agent pair \ --machine-id mach_61f043950cf1 \ --agent-address 0xagent...0101 \ --agent-did did:pkh:eip155:1:0xagent...0101 \ --agent-provider teneo \ --agent-role machine-market-buyer \ --agent-signature-file ~/.peaqos/agent-challenge.sig \ --per-tx-limit 5 --daily-limit 20 --currency USD \ --allowed-skills agentic-market-claude-inference,pay-sh-gemini \ --allowed-service-ids svc_abc \ --denied-service-ids svc_xyz ``` Required: `--machine-id`, `--agent-address`, `--agent-provider`, `--agent-role`. Optional: `--agent-did`, `--agent-signature-file` (path to pre-signed EIP-191 challenge signature; required with `--json`, otherwise the CLI prompts interactively), `--description`, `--per-tx-limit`, `--daily-limit`, `--currency`, `--allowed-skills` (CSV), `--denied-skills` (CSV), `--allowed-service-ids` (CSV), `--denied-service-ids` (CSV), `-y`/`--yes` to skip the confirmation prompt, `--json` for raw JSON (implies `--yes`). Preconditions: `PEAQOS_ORCH_API_KEY` set. Machine already active in peaqOS or the server returns `MACHINE_NOT_ACTIVATED` and the CLI exits 2. Output adds `Agent DID`, `Session ID`, `Session Expires`, and `Verification` lines to the existing pairing summary. The token prints exactly once on stdout, never written to `peaqos.log` or `--verbose` output. Pipe to a file: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos scale agent pair ... --json | jq -r .pairing_token > ~/.peaqos/agent.token chmod 600 ~/.peaqos/agent.token ``` Session tokens expire (default 1 hour). Rotate directly via `client.orchestration.createAgentPairingSession(...)` from your own tooling, signing a fresh challenge. #### `peaqos scale search` Agent-facing. Posts a market search and returns ranked service quotes. Hits `POST /api/v1/market/search` plus `GET /api/v1/market/searches/:searchId`. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos scale search \ --machine-id mach_1 \ --service-type oracle.price-feed \ --pairing-token-file ~/.peaqos/agent.token \ --budget-amount 1 --budget-currency USD \ --native-only ``` Required: `--machine-id`, `--service-type`, `--pairing-token-file`. Optional: `--agent-pairing-id`, `--operation`, `--capabilities` (CSV), `--region`, `--max-results`, `--budget-amount`, `--budget-max`, `--budget-currency`, `--native-only`, `--allow-handoff`, `--provider-credentials` (path to JSON), `--json`. Preconditions: machine active with verified `identityRef`, active agent pairing, pairing token in the token file. Provider credential file contents never appear on stdout, stderr, or in log output. Output: * Human mode: ranked quote table with columns `Service ID`, `Quote ID`, `Operation`, `Provider`, `Score`, `Execution`, `Integration`. Footer prints `Search ID: msearch_...` plus a concrete next-step hint: `peaqos scale order --search-id --quote-id `. * Empty results print `No matching services found.` with the same aligned `Search ID:` line and broaden-your-search guidance. * `--json` emits the full `MarketSearch` envelope. The raw `MarketSearchRequest` is constructed from typed SDK data classes, not raw dicts. The `order` group is dynamic-dispatch. Any first token that is not `status`, `list`, `received`, or `dispute` is interpreted as a service UUID and routed to the placement flow. `peaqos scale order foo-bar` reads as "place an order for service `foo-bar`." #### `peaqos scale order ` Agent-facing. End-to-end purchase. Hits the [`POST /market/orders` + payment intent + execute](/peaqos/api-reference/machine-markets-orders) flow. Four payment paths chosen from the `MarketPayment` returned at order create: * **No payment** — create → execute (2 steps). * **Wallet payment** — create → intent → transfer → proof → execute (5 steps). If `PEAQOS_OWS_WALLET` is set the CLI auto-signs the ERC-20 transfer via OWS; otherwise it prompts for a pasted tx hash (or use `--payment-tx-hash`). * **Escrow / handoff** — same five steps with the escrow rail. * **x402** (`v0.0.6`+) — create → intent → sign → proof → execute → confirm (6 steps), for paid-HTTP Agentic Market providers (e.g. Wolfram Alpha over USDC on Base). The CLI signs the provider's payment challenge locally with the active wallet (`client.account` — the OWS wallet when `PEAQOS_OWS_WALLET` is set, otherwise the local key) and hands the signed `PAYMENT-SIGNATURE` header to peaqOS, which pays the provider during execute. **No separate on-chain transfer**, no tx-hash prompt; delivery is confirmed automatically in step 6. If execution fails after proof is recorded, the error reports the current payment status so you can check whether the authorization is held. Set `PEAQOS_ORDER_STEP_DELAY_SEC` to a non-negative number of seconds to pause between placement steps (demos, eventually-consistent order state); unset means no delay. Required: `--machine-id`, `--agent-pairing-id`, `--pairing-token-file`. Optional: `--search-id`, `--quote-id`, `--operation`, `--budget-amount`, `--budget-currency`, `--input ` (JSON object file — there is no `@file` shorthand), `--provider-credentials ` (JSON file with provider creds; never logged), `--payment-tx-hash`, `--payment-chain` (CAIP-2 alias: `base`, `peaq`, `agung`, `ethereum`/`eth`, `polygon`, `arbitrum`, `optimism`, `bsc`, `solana`), `--payment-token`, `--skip-payment`, `-y`/`--yes`, `--json`. `PEAQOS_RPC_URL` overrides the built-in payment RPC list (peaq uses public Agung wss-async by default). Error codes: `QUOTE_EXPIRED`, `EXECUTION_UNSUPPORTED`, `PAYMENT_REQUIRED`, `PAYMENT_RPC_ERROR`, `PAYMENT_TX_FAILED`, `PAYMENT_TRANSFER_NOT_FOUND`, `ORDER_CLOSED`, `ORDER_NOT_DELIVERED`, `NOT_FOUND`. `--json` does not silence progress. Progress prints to stderr; the JSON envelope goes to stdout. Global `--quiet` suppresses stderr. #### `peaqos scale order status ` Returns current state of the purchase. Read-only platform auth. Hits [`GET /market/orders/:orderId`](/peaqos/api-reference/machine-markets-orders) plus the payment lookup. Optional: `--json` for the `{ order, payment }` envelope. Missing payment record (`NOT_FOUND`) is not an error — `payment` is `null` in the JSON envelope. Missing order ID exits 1. #### `peaqos scale order list --machine-id ` Lists orders for a machine. Hits [`GET /market/orders?machineId=...`](/peaqos/api-reference/machine-markets-orders). Required: `--machine-id`. Optional: `--limit ` (1-500; outside range exits 1), `--cursor ` (from a prior `next_cursor`; never logged), `--json`. Output behaviour: * Human mode: one page, `N order(s) shown.` footer. When `next_cursor` is set, the CLI prints a copy-paste hint: `Next page: peaqos scale order list --machine-id [--limit N] --cursor `. * `--json` without `--limit`: auto-paginates all pages and emits a flat root-level JSON array. * `--json` with `--limit`: emits a single-page envelope `{ "items": [...], "next_cursor": ... }`. The CLI surfaces `next_cursor` (snake\_case) on stdout; the wire field is `nextCursor`. #### `peaqos scale order received ` Confirms delivery and releases escrow. Requires `--pairing-token-file`. Hits [`POST /market/orders/:orderId/confirm`](/peaqos/api-reference/machine-markets-orders). Optional: `--json`. Status moves to `confirmed`, payment to `release_pending`. Error codes: `ORDER_NOT_DELIVERED`, `ORDER_CLOSED`, `AGENT_AUTH_INVALID`. Missing order ID or token file exits 1. #### `peaqos scale order dispute ` Raises a dispute. Requires `--reason` and `--pairing-token-file`. Hits [`POST /market/orders/:orderId/dispute`](/peaqos/api-reference/machine-markets-orders). Optional: `--evidence ` (JSON object file), `-y`/`--yes` to skip the `Raise dispute? [y/N]` prompt, `--json` (also skips the prompt). Status moves to `disputed`, payment to `frozen`. Error codes: `ORDER_CLOSED`, `AGENT_AUTH_INVALID`. Missing order ID, reason, or token file exits 1. ## Exit codes Every subcommand funnels SDK and network exceptions through a single error handler that raises with a stable exit code. | Exit code | Meaning | | :-------- | :------------------------------------------------------------------------------------------------------------------------------- | | `0` | Success | | `1` | User / validation error (bad flag, invalid DID, cap or rate limit) | | `2` | Network, RPC, or on-chain error (connection failure, HTTP, revert) | | `3` | Configuration error (missing env var, invalid private key file, unknown or missing `TOKENOMICS_DEPLOYMENT_ID`, incompatible MCR) | `peaqos activate` adds a stable `error_code` to every failure after input validation (see [`peaqos activate`](#peaqos-activate)). `peaqos machine` and `peaqos monetize` print the error message only, also with `--json` (checked on 0.0.8 with `machine status` and `monetize status` against unknown machine IDs). ## See also The peaqOS agent skill that drives these CLI flows from any AI agent. The TypeScript / Python API the CLI wraps. The MCR API that `get-mcr`, `get machine`, and `operator machines` hit. # Smart contracts Source: https://docs.peaq.xyz/peaqos/concepts/contracts Architecture, addresses, and responsibilities of every peaqOS contract. peaqOS runs on five on-chain layers. The [Economics 2.0](/peaqos/concepts/economics-2-0) contract set (machine registry, subscriptions, price oracle, trust validator staking) is what new machines activate against since 2026-09-01. The Tokenomics 1.0 registry and staking contracts still serve machines onboarded before that. Both sets live on peaq chain as UUPS upgradeable proxies. Bridging is LayerZero V2; smart accounts follow ERC-4337; and low-level DID, batch, and WPEAQ operations are peaq chain precompiles. ## Architecture ``` Economics 2.0 (peaq chain, UUPS proxies, since 2026-09-01) InfoDesk (parameters, peers, allowlists) MachineStateAndSync ──activateMachine──► MachineRegistry (ERC-721, machineId == tokenId, DID document) │ MachineSubscription (tier bond, grace, runoff, credits) ◄── PriceOracle └──────────────────────────────► CrossChainMirror ◄──► MachineBridgeAdapter (relocation, disabled) SubscriptionTokenProvisionPool (USDT settlement) · TrustValidatorStaking · CoordinationFeeCollector EventRegistry (2.0) · ExecutionCostReserve · MachineMigrationHub (legacy mirroring) Tokenomics 1.0 (peaq chain, UUPS proxies, machines onboarded before 2026-09-01) IdentityRegistry ──owns──► IdentityStaking │ ▲ │ (gates on) │ stakeFor() └─────► EventRegistry │ └─────► MachineNFT Cross-chain (peaq ↔ Base, LayerZero V2) MachineNFTAdapter (peaq) ◄──► MachineNFTBase (Base) ERC-4337 (peaq chain) MachineAccountFactory ──deploys──► MachineSmartAccount (BeaconProxy) Precompiles (peaq chain, fixed addresses) DID (0x...800) · Batch (0x...805) · WPEAQ (0x...809) ``` * Which set a client talks to is a configuration choice: the SDKs enter Tokenomics 2.0 mode when you pass `tokenomics20: { deploymentId }` (JS) or `tokenomics20=Tokenomics20Config(deployment_id=...)` (Python); the CLI reads `TOKENOMICS_DEPLOYMENT_ID`. Economics 2.0 addresses are never environment variables: they ship inside the SDK's deployment record and are verified against `InfoDesk.peer(role)` before every write. * Core contracts use UUPS upgradeable proxies (OpenZeppelin 5.x) with ERC-7201 namespaced storage. Future upgrades preserve state. * Identity NFT (minted by IdentityRegistry) and Machine NFT (minted by MachineNFT) are separate ERC-721 spaces with independent `tokenId` sequences linked by `machineId`. See [Machine NFT](/peaqos/concepts/machine-nft#ownership-semantics). * IdentityRegistry implements **ERC-8004** for DID-anchored machine metadata; the JSON Machine Card is served by the MCR API. ## peaq mainnet addresses ### Economics 2.0 (peaq mainnet) Deployment record `peaq-mainnet` (chain ID 3338), deployed 2026-09-01 at block 11,446,386. All are ERC-1967 proxies: `MachineBridgeAdapter` started as a plain deployment and was re-pointed by `InfoDesk` to a proxy on 2026-09-08 at block 11,541,867. `@peaqos/peaq-os-sdk` 0.7.0 and `peaq-os-sdk` 0.7.1 carry the new address; earlier releases fail Economics 2.0 writes at preflight with `PEER_MISMATCH`. The implementations behind `InfoDesk`, `MachineStateAndSync`, `MachineBridgeAdapter` and `CrossChainMirror` were upgraded on 2026-09-09; the proxy addresses below did not change. The seven marked SDK are the roles the SDK snapshot carries and verifies against `InfoDesk.peer(role)`; the other six are read by peaq's own services. Not environment variables. | Contract | Address | Role | | :----------------------------- | :------------------------------------------- | :------------------------------------------------------------------------------- | | InfoDesk | `0x6C7426ada37212D94477Ac4CFcA43A38177D6A82` | SDK. Parameters, peers, trust validator allowlist, PEAQ token pointer | | MachineRegistry | `0x64b93Cc29b251fAFa83BD110cDB1C24207f85536` | SDK. ERC-721 machines (`machineId == tokenId`) and DID documents | | MachineStateAndSync | `0xcD1917FB2a56459AcA34FDD118e08776dE0890f2` | SDK. `activateMachine`, suspend, resume, relocation orchestration | | MachineSubscription | `0x9e37AD189c334C92e6B8a812Ca4c02f35Ac43895` | SDK. Tier bonds, renewals, grace and runoff, credits. Allowance spender for PEAQ | | CrossChainMirror | `0x71DCB313977d6884212395505f081a2991Bfe8E5` | SDK. Home-chain records and mirrored data | | MachineBridgeAdapter | `0x791087c35484b567c53f392a624d2e4BaDcC53DF` | SDK. LayerZero relocation transport (disabled) | | SubscriptionTokenProvisionPool | `0x7088Cf400081428a41a4287B75AC633071c0e92A` | SDK. USDT settlement of the PEAQ bond. Allowance spender for USDT | | PriceOracle | `0x96E6a3BAB0254E7f25B0cdE50C3cBF39272cd46B` | Daily PEAQ/USD stamps by the trust validator node | | TrustValidatorStaking | `0x38cfa22Da649F650Cbe7278A9B5141380968920e` | Permissionless trust validator staking (not exposed in SDKs) | | CoordinationFeeCollector | `0xCaD48Dc0Eb650C9a0B799e36627D9de18f5a9Dc2` | Per-tier coordination fees, x402 (fee configured at 0) | | ExecutionCostReserve | `0xC2F13a8310432f27701F59f060417e986ae24e82` | Gas reserve for system functions | | EventRegistry (2.0) | `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e` | Events for 2.0 machines (SDK event submission not wired yet) | | MachineMigrationHub | `0x06EbC22B08229fb796788DB1AAE9675bfC4e22ba` | Mirrors Tokenomics 1.0 machines into 2.0 (operated by peaq) | The PEAQ token the bond is paid in is not a deployed contract: `InfoDesk.peaqToken()` resolves to the native-balance precompile `0x0000000000000000000000000000000000000809`. ### Tokenomics 1.0 (peaq mainnet) Set these in your environment. The SDK reads them from `fromEnv()` / `from_env()`. The SDK constructor still requires them in Tokenomics 2.0 mode, although activation reads none of them. #### Core | Contract | Variable | Address | | :--------------- | :-------------------------- | :------------------------------------------- | | IdentityRegistry | `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` | | IdentityStaking | `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` | | EventRegistry | `EVENT_REGISTRY_ADDRESS` | `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` | | MachineNFT | `MACHINE_NFT_ADDRESS` | `0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9` | #### Precompiles | Contract | Variable | Address | | :------- | :------------------------- | :------------------------------------------- | | DID | `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` | | Batch | `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` | | WPEAQ | n/a | `0x0000000000000000000000000000000000000809` | #### Optional | Contract | Variable | Address | Needed for | | :-------------------- | :-------------------------------- | :------------------------------------------- | :----------------------------------------------------------- | | MachineAccountFactory | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x4A808d5A90A2c91739E92C70aF19924e0B3D527f` | `deploySmartAccount` / `getSmartAccountAddress` | | MachineNFTAdapter | `MACHINE_NFT_ADAPTER_ADDRESS` | `0x9AD5408702EC204441A88589B99ADfC2514AFAE6` | `bridgeNft` from peaq | | AdminFlags | `ADMIN_FLAGS_ADDRESS` | `0x1c5f33fBEE6BA38ed9bDE247C1Ba89A2116C25f1` | MCR API server (negative-flag reads + admin trust overrides) | ## Base mainnet addresses Needed when bridging **into** peaq from Base. Pass as `baseNftAddress` / `base_nft_address` to `bridgeNft` / `bridge_nft`. | Contract | Address | | :------------------------------ | :------------------------------------------- | | MachineNFTBase (LayerZero ONFT) | `0xee8A521eA434b11F956E2402beC5eBfa753Babfa` | ## Solana mainnet addresses ### Economics 2.0 (Solana mainnet) First five programs deployed 2026-09-09, upgraded in place and joined by four more on 2026-09-11 (program IDs unchanged). Protocol chain ID 5, LayerZero EID 30168. Nine Anchor programs mirror the peaq contracts of the same name; all keep the deployment wallet as upgrade authority and as administrator of each program's root account. | Program | Program ID | Counterpart on peaq | | :----------------------- | :--------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | | InfoDesk | `UzabYW77ox4AJzsMiiFqA89Vm5aWeyy3PSW9LtaYnjG` | `InfoDesk` subset: chain registry, currency whitelist, tokenomics parameters, bridging flags | | MachineBridgeAdapter | `CEqQYLMm6WKHyvBZRU91N2kkQV1J2V3HxLW8McFQ681H` | `MachineBridgeAdapter` plus the mirror stores. LayerZero OApp; its store PDA `5MKZ41yuSg46fTt4ma29J69aCbw93eyVQ9XKW89vxJ6i` is the peer peaq's adapter points at | | MachineRegistry | `AgR8exgW2mwpf4v2PpojYuDHXVBThZtenS6XYjV4JnUD` | `MachineRegistry`: Solana-native machine identity and DID documents | | CoordinationFeeCollector | `7DsHEYDpvvZY3BgCLS2wAYrftGAQuUWLibQijeYULMBh` | `CoordinationFeeCollector`: fees paid in SOL or a whitelisted SPL mint | | MachineStateAndSync | `DbhzPvFghnmpw6YmZsCBuVU6u1sjbc1FfzPDqi4VjoZL` | `MachineStateAndSync` destination half: relocation state | | EventRegistry | `AWMkhnccpjuMgUDqWfFG38DCiKTqC3KsLhrzivEPWb77` | `EventRegistry` (2.0): per-machine activity and revenue events for Solana machines, deployed 2026-09-11 | | PriceOracle | `FpfGfjbkSm5yBuV5d5HLsr6wybm1EvgHoqcTyTFZG12v` | `PriceOracle`: USD price reference for the coordination fee, deployed 2026-09-11 | | TreasuryPool | `EzppvStr1VorTrNEDGWwgSGnM9xum3uAc6W1vCFnr4Ey` | Treasury sink for coordination fees (native SOL and SPL), deployed 2026-09-11 | | TrustValidatorStaking | `BSDXwYicwx3NZdD7Mpe147jzWqUYmAzR8PptqH5NU1wM` | `TrustValidatorStaking`, home chain 1 (peaq), deployed 2026-09-11 | Configuration at deployment: tier fees $0.02 / $0.20 / \$40 as on peaq (a fourth tier exists in the config and is disabled), native SOL whitelisted as currency, bridging and relocation flags false. On the peaq side `MachineBridgeAdapter` has the Solana store PDA registered as its LayerZero peer (receive gas 400,000, maximum message size 10,000) and holds a 105 PEAQ fee reserve. Nothing in this table is reachable through the SDKs or the CLI yet, and the 2.0 MCR server does not read the Solana `EventRegistry` yet, so a Solana machine has no rating. Activating a machine on Solana needs PEAQ bonding for Solana machines, which is in development; see [Omni-chain](/peaqos/concepts/omni-chain#solana) for the status of each capability. ## Agung testnet addresses ### Economics 2.0 (agung testnet) Deployment record `agung-2026-08-28` (chain ID 9990). Same seven SDK roles as mainnet. Agung has no paired 2.0 MCR, so monetization calls fail with `DEPLOYMENT_UNAVAILABLE` there. | Contract | Address | | :----------------------------- | :------------------------------------------- | | InfoDesk | `0x72b66AF120c55371cA6e5Ce38D4e10dF1bA30Bb1` | | MachineRegistry | `0x538da35489B1F0035799a8351E67835ae4ABb377` | | MachineStateAndSync | `0xA7D33726232aae30622643a18047f0Ca3e159da9` | | MachineSubscription | `0xaDCc5dD8CD57E3198B881c3388F25D2fD69B5aDc` | | CrossChainMirror | `0xd5F8d5944c5d488B1cD698D642FC8156a8CD0c0c` | | MachineBridgeAdapter | `0x0406e7bE522AEa626aee54f6258D415215e59F76` | | SubscriptionTokenProvisionPool | `0x8A3692Ad0AcF79BB89413514Af9957d95032e19B` | ### Tokenomics 1.0 (agung testnet) Use these for development on agung. The contract surface matches mainnet; only the deployed addresses differ. #### Core | Contract | Variable | Address | | :--------------- | :-------------------------- | :------------------------------------------- | | IdentityRegistry | `IDENTITY_REGISTRY_ADDRESS` | `0x9E9463a65c7B74623b3b6Cdc39F71be7274e5971` | | IdentityStaking | `IDENTITY_STAKING_ADDRESS` | `0x55f336714aDb0749DbFE33b057a1702405564E3d` | | EventRegistry | `EVENT_REGISTRY_ADDRESS` | `0x2DAD8905380993940e340C5cE6d313d5c2780040` | | MachineNFT | `MACHINE_NFT_ADDRESS` | `0xB41C2A4f1c19b6B06beaAce0F5CD8439e77C4b1c` | #### Precompiles Identical to mainnet: same fixed addresses on every peaq runtime. | Contract | Variable | Address | | :------- | :------------------------- | :------------------------------------------- | | DID | `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` | | Batch | `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` | | WPEAQ | n/a | `0x0000000000000000000000000000000000000809` | #### Optional | Contract | Variable | Address | Needed for | | :-------------------- | :-------------------------------- | :------------------------------------------- | :----------------------------------------------------------- | | MachineAccountFactory | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x65a4DfEB799dFf8CF15f13816d648a7805d6b1F9` | `deploySmartAccount` / `getSmartAccountAddress` | | AdminFlags | `ADMIN_FLAGS_ADDRESS` | `0x4181a2Aa34aFb247450FfcBd65be5aBD4Cbee658` | MCR API server (negative-flag reads + admin trust overrides) | **Bridging cannot be exercised on agung.** LayerZero deprecated the agung endpoint (EID `40299`): DVNs and executors are no longer active, so `bridgeNft` / `bridge_nft` cannot relay. No agung `MachineNFTAdapter` appears in peaq deployment records — the address previously listed here (`0x63fD…8De22`) is actually the MachineAccountFactory's `UpgradeableBeacon` (verifiably the factory's nonce-2 contract creation). Exercise the bridge on peaq mainnet ↔ Base mainnet only. ## Economics 2.0 contracts ### MachineRegistry The 2.0 machine registry: one ERC-721 per machine where the token ID **is** the machine ID (`uint256(keccak256(abi.encode(machineType, credentialSubject)))`), plus the machine's DID document (controller, verification methods, authentication, service endpoints). Standard ERC-721 transfers and approvals are how a machine changes owner. Minting happens only inside `MachineStateAndSync.activateMachine`; there is no public mint. **SDK methods:** `getMachineOwner`, `computeMachineId`, `transferMachine`, `safeTransferMachine`, `approveMachine`, `setMachineApprovalForAll`, `setMachineController`, `setMachineVerificationMethods`, `setMachineAuthentication`, `setMachineServiceEndpoints` ### MachineStateAndSync Orchestrates the one-transaction activation (`activateMachine`, `activateMachineWithUsdt`), voluntary suspend and resume, per-machine and protocol-wide pause flags, and relocation. Never holds tokens. **SDK methods:** `activateMachine`, `activateMachineWithUsdt`, `previewMachineActivation`, `suspendMachine`, `resumeMachine`, `getMachineActivationState`, `getMachineAvailability` ### MachineSubscription Prices and collects the tier bond (`requiredPeaqAmount(tier)`), stores each machine's subscription (tier, period, bonded amount), applies grace and runoff, and tracks bonding reward and voucher credits. The allowance spender for PEAQ bonds. Settlement functions are called by the trust validator node. **SDK methods:** `activateExistingMachine`, `renewMachine`, `previewMachineRenewal`, `getMachineSubscription` ### InfoDesk Owner-set protocol parameters (tier prices in USD, grace and runoff durations, epoch length, fee rates, burn and treasury addresses), the peer registry the SDK verifies against, the trust validator allowlist, and `peaqToken()`. ### PriceOracle Daily currency prices committed by trust validator nodes (`commitPrices`), read by `MachineSubscription` to convert USD tier prices to PEAQ. Public read: `getLatestPrice(bytes32 currency)`. ### CrossChainMirror and MachineBridgeAdapter Record the machine's home chain and carry relocation messages over LayerZero V2. Relocation is disabled (`InfoDesk.isBridgingEnabled()` false) and the SDKs expose status reads only. **SDK methods:** `getMachineRelocationStatus` ### SubscriptionTokenProvisionPool Converts USDT into the PEAQ bond when a caller settles with `--payment usdt` / `activateMachineWithUsdt` / `renewMachineWithUsdt`. The allowance spender for USDT. ### TrustValidatorStaking, CoordinationFeeCollector, ExecutionCostReserve, EventRegistry (2.0), MachineMigrationHub Live on mainnet, operated by peaq or reserved for later releases; none is exposed in the SDKs or the CLI today. See [Economics 2.0](/peaqos/concepts/economics-2-0). ## Tokenomics 1.0 contracts These serve machines onboarded before 2026-09-01. New activations do not use them. ### IdentityRegistry Central machine identity registry of Tokenomics 1.0. Mints an ERC-721 Identity NFT to each machine on registration and orchestrates bonding through `IdentityStaking.stakeFor()`. Implements **ERC-8004** so that each Identity NFT's `tokenURI` resolves to the machine's DID-anchored Machine Card. DID attributes themselves live on the peaq DID precompile (W3C DID), written by the SDK via `writeMachineDIDAttributes` and read by the MCR API. Tracks per-machine status (`None` → `Pending` → `Verified` / `Rejected` / `Deactivated`). Supports self-registration and proxy registration. **SDK methods:** `registerMachine`, `registerFor` **Key state:** `minBond` (currently 1 PEAQ), `nextMachineId`, `operatorOf`, `machineStatus` ### IdentityStaking Bond token storage. Tokens are staked at registration time via `stakeFor()` and held permanently; no withdrawal path is exposed. Only authorized callers (IdentityRegistry) can initiate stakes. Supports pause / unpause by owner. **SDK interaction:** Indirect. `registerMachine` / `registerFor` route the bond through here automatically. ### EventRegistry On-chain store for revenue (type `0`) and activity (type `1`) events. Gates event submission on (a) the machine being registered in IdentityRegistry, and (b) the machine being bonded in IdentityStaking. Authorization rule: `msg.sender` must be the machine wallet or the operator. Stores a `keccak256` hash of the raw data; payloads stay off-chain. **SDK methods:** `submitEvent`, `batchSubmitEvents` **Concept:** [Events](/peaqos/concepts/events) ### MachineNFT LayerZero V2 ONFT representing a machine's financial profile. Minted in a separate `mintNft` call after registration: the Machine NFT `tokenId` is independent from the Identity NFT `tokenId`. `tokenURI` resolves to the MCR API's `/metadata/{token_id}` endpoint, which returns the Machine Card. **SDK methods:** `mintNft`, `tokenIdOf` **Concept:** [Machine NFT](/peaqos/concepts/machine-nft) ## Cross-chain contracts These are the Tokenomics 1.0 Machine NFT bridge. Economics 2.0 machines relocate through `MachineBridgeAdapter` and `CrossChainMirror` instead, which is disabled today. ### MachineNFTAdapter (peaq) Wraps `MachineNFT` for LayerZero V2 bridging. Lock/unlock pattern: locks the NFT on peaq while the mirror exists on the destination chain. Needed only when bridging **from** peaq. Peers: Base (EID 30184) and Solana (EID 30168, program `HraxgdzfcAi3AnxRP5sGSrXGAb9ZT1tZTMNuh9vQLxTu`). The Solana lane is live on mainnet since 2026-08-21 and operated with peaq's tooling; the SDKs accept only `"base"` as a destination today. See [Omni-chain: Solana](/peaqos/concepts/omni-chain#solana). **SDK methods:** `bridgeNft` / `bridge_nft` when `source` is `"peaq"` ### MachineNFTBase (Base) Standard `ONFT721` on Base. Burn/mint pattern: NFT is burned when bridged back to peaq. Needed when bridging **from** Base. **SDK methods:** `bridgeNft` / `bridge_nft` when `source` is `"base"` (pass the address as `baseNftAddress` / `base_nft_address`) ## ERC-4337 smart accounts ### MachineAccountFactory CREATE2 factory for deploying `MachineSmartAccount` BeaconProxy instances. Standard ERC-4337 flow: smart accounts run through the canonical EntryPoint at `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789` (same address across all EVM networks). **SDK methods:** `deploySmartAccount` / `deploy_smart_account`, `getSmartAccountAddress` / `get_smart_account_address` ### MachineSmartAccount The shared BeaconProxy implementation behind deployed smart accounts. Handles owner/machine RBAC and broad machine execution authority. Users never interact with the implementation address directly; calls go to the deployed proxy. ## AdminFlags Optional peaq-chain contract read by the MCR API server (not the SDK). Holds admin-set flags that modify MCR responses: per-machine `negative_flag`s and [trust-level](/peaqos/concepts/trust-levels) overrides. MCR consumers see the adjusted score in their API response. When the contract isn't configured, `contracts.admin_flags` on [`GET /ready`](/peaqos/api-reference/health) returns `false` and the service serves unmodified MCR. See [API overview](/peaqos/api-reference/overview#server-configuration). ## Precompiles (peaq chain) | Precompile | Address | Purpose | | :--------- | :------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DID | `0x0000000000000000000000000000000000000800` | Read/write DID attributes. Used for Machine Card metadata and proxy-operator fleet attributes | | Batch | `0x0000000000000000000000000000000000000805` | Atomic multi-call for bonding (registration + stake) and proxy DID writes | | WPEAQ | `0x0000000000000000000000000000000000000809` | The native PEAQ ERC-20 precompile, which peaqOS contracts reference as `WPEAQ` (`IWPEAQ`); used by IdentityStaking as the staking token. Not the standalone WPEAQ ERC-20 token — see [PEAQ Token Contracts](/peaqchain/learn/peaq-token-contracts) | ## LayerZero endpoints Used by the bridge adapters. You don't set these directly; the SDK handles them. | Network | LayerZero V2 endpoint | EID | | :------------- | :--------------------------------------------- | :------ | | peaq mainnet | `0x6F475642a6e85809B1c36Fa62763669b1b48DD5B` | `30302` | | Base mainnet | `0x1a44076050125825900e736c501f859c50fE728c` | `30184` | | Solana mainnet | `76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6` | `30168` | The SDK exports the EIDs as `LAYER_ZERO_EIDS` (JS) / `LAYERZERO_EIDS` (Python) for reference. ## Upgrade pattern All Economics 2.0 contracts (`MachineBridgeAdapter` since its 2026-09-08 re-point), and all Tokenomics 1.0 core contracts (IdentityRegistry, IdentityStaking, EventRegistry, MachineNFT), use OpenZeppelin **UUPS upgradeable proxies** with ERC-7201 namespaced storage. Implementation slots are kept distinct by namespace, so future upgrades won't collide with existing state. MachineSmartAccount uses a **BeaconProxy** pattern: a single implementation upgrade simultaneously applies to every deployed smart account. ## See also Wire the contract addresses into `.env` for the SDK. Every method that reads or writes these contracts. Off-chain reads derived from the on-chain state. # Data streams Source: https://docs.peaq.xyz/peaqos/concepts/data-streams How peaqOS Stream makes machine data sellable: signed packages, tamper-evident chunk chains, per-chunk encryption, and buyer access grants. No plaintext leaves the machine unprotected. Selling machine data has a trust problem on both sides. A buyer needs to know the data really came from a specific machine and was not tampered with, *before* paying. A seller needs to hand over exactly what was bought and nothing more, without leaking the rest or trusting a middleman to hold the plaintext. [Stream](/peaqos/functions/stream) solves both with signatures, hashes, and per-chunk encryption. Trust comes from the machine's signature and verifiable hashes — no shared secrets, no trusted intermediary. ## Roles | Role | Description | | :--------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | **Machine** | The robot or device generating data. Holds a peaqID and signing key from [Activate](/peaqos/functions/activate). | | **Machine Owner / Proxy Operator** | Sets the data policy, runs the Edge Agent, lists data for sale, and configures recovery recipients. | | **Context Provider (buyer)** | Buys access, verifies it, and normalizes the data into datasets for consumption such as AI training. DataHive is one. | | **peaqOS backend** | Stores policies, manifests, receipts, and access grants, and emits audit events. It does **not** store raw plaintext data. | ## The trust model **1. Signed data packages.** As data is captured, the machine bundles it with its identity — peaqID, timestamp, schema version, sequence number — and signs it. Anyone can verify the signature using the public key derived from the machine's DID, with no account or login. Field rules are applied *before* signing, so a buyer can detect tampering without the seller having to reveal protected fields. **2. Field-level rules.** A Data Event Map defines, per topic and field, whether to `include`, `exclude`, `encrypt`, or `anonymize` (by `hash`, `generalize`, or `redact`). Sensitive values are protected before they ever leave the machine. **3. Chunks and chunk chains.** Continuous data is grouped into bounded chunks — by size, message count, or time window. Each chunk links to the previous one, forming a chunk chain: reordering, gaps, or edits become detectable. A chunk is the unit a buyer purchases. **4. Per-chunk encryption.** Each chunk is encrypted under its own random key. That key is then wrapped separately for each authorized recipient using their public key. Granting access to a new buyer re-wraps the key to *their* key — the chunk data is never re-encrypted, and no master key is ever shared. This is envelope encryption. **5. Manifests and datasets.** Each chunk gets a signed manifest recording its hashes (ciphertext and a plaintext commitment), storage reference, and encryption metadata — never the data itself. Chunks roll up into a dataset for one topic and time range, with a Merkle root committing to every chunk. The owner lists the dataset; buyers verify against the manifests before paying. ## The chunk envelope A chunk is stored and shared as a self-describing envelope: the encrypted bytes plus everything needed to verify and (with the right key) decrypt them. ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "chunkId": "…", "previousChunkId": "…", "index": 0, "encryptedDataHash": "sha256:…", "plaintextHash": "sha256:…", "storageRef": "walrus://…", "encryption": { "algorithm": "xchacha20-poly1305", "nonce": "…", "keyCommitment": "sha256:…", "keyRecipients": [ { "recipientId": "…", "recipientType": "owner", "algorithm": "x25519-sealedbox", "publicKeyHex": "…", "wrappedKeyHex": "…" } ] }, "signature": { "algorithm": "ed25519", "machineDid": "did:peaq:0x…", "keyId": "did:peaq:0x…#keys-1", "publicKeyHex": "…", "value": "…" } } ``` The encrypted bytes live off-chain — local disk, owner storage, Walrus (`walrus://`), or a cloud adapter. The backend tracks only the manifest: hashes, storage reference, and the per-recipient wrapped keys. Adding a buyer to `keyRecipients` is how an access grant is issued — no data is re-encrypted. ## Buyer–seller flow The Edge Agent captures allowed topics, applies field rules, and groups messages into encrypted chunks with signed manifests. Chunks for a topic and time range roll up into a dataset with a Merkle root. The owner signs and lists it. A buyer browses the listing, fetches the dataset and chunk manifests, verifies the machine's signatures, and checks chunk hashes against the Merkle root. Optional sample chunks can be inspected before purchase. The buyer submits their public key and pays. After payment, an access grant re-wraps the purchased chunk keys to the buyer's key. The backend records the grant and an audit event. The buyer receives the encrypted chunks — fetched from storage, or streamed [directly machine-to-machine over P2P](/peaqos/sdk-reference/stream-distribution#p2p-delivery) — unwraps the chunk keys with their private key, and decrypts only the chunks they bought, one at a time. Over P2P, every chunk's hash, signature, and chain link is verified *before* decryption. Owner and operator **recovery recipients** can be configured before chunks are created: the chunk key is also wrapped to their keys, so data stays recoverable if the machine goes offline. ## Build it The cryptographic operations — signing, chunk chains, encryption, key wrapping — are exposed by the [`stream` SDK module](/peaqos/sdk-reference/stream) for JavaScript and Python. Selling and shipping the result — purchases, payment rails, S3 and P2P delivery channels — is the [distribution surface](/peaqos/sdk-reference/stream-distribution). The on-machine [Edge Agent](/peaqos/sdk-reference/ros2/stream-agent) runs this pipeline automatically from a Data Event Map. Buyers and Context Providers integrate through the [Stream data marketplace API](/peaqos/api-reference/stream-marketplace). ## Related * [Stream function](/peaqos/functions/stream) * [Stream SDK reference](/peaqos/sdk-reference/stream) * [peaqID](/peaqos/concepts/peaqid) * [Machine Markets](/peaqos/concepts/machine-markets) # Economics 2.0 Source: https://docs.peaq.xyz/peaqos/concepts/economics-2-0 How machine activation, subscription bonds, credits, and trust validators work under peaq Economics 2.0, and what is live on peaq mainnet. Economics 2.0 (Tokenomics 2.0 in the code) is the contract set that prices machine activation in USD, bonds PEAQ per machine on a subscription tier, and settles the network's economics through trust validators. It went live on peaq mainnet on 2026-09-01 and replaces the fixed 1 PEAQ bond of the earlier `IdentityRegistry` model. The paper is at [peaq.xyz/economics-2-0](https://www.peaq.xyz/economics-2-0); this page documents what the deployed contracts do. Live figures (activated machines, PEAQ bonded, tier prices at the current oracle rate) are on the [Machine Explorer](https://machines.peaq.xyz/economics). ## What is live | Piece | State on 2026-09-04 | Where | | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | | 13 Economics 2.0 contracts | Deployed on peaq mainnet 2026-09-01 (block 11,446,386); two implementations upgraded 2026-09-03, five more on 2026-09-04 at 10:51 UTC (MachineRegistry, MachineStateAndSync, MachineSubscription, SubscriptionTokenProvisionPool, TrustValidatorStaking). Addresses unchanged | [Contracts](/peaqos/concepts/contracts#economics-2-0-peaq-mainnet) | | Price oracle | First PEAQ/USD stamp 2026-09-03 11:20 UTC; one peaq-run trust validator node commits daily | [Trust validators](#trust-validators) | | Machine activation | Live through `peaq-os-sdk` 0.6.0+, `@peaqos/peaq-os-sdk` 0.6.0+, `peaq-os-cli` 0.0.8+ | [Activate](/peaqos/functions/activate) | | Machine management (suspend, resume, renew, transfer, DID updates) | Live in the same versions | [CLI: machine](/peaqos/cli#peaqos-machine) | | USDT settlement of the bond | Live (`--payment usdt`) | [Activate](/peaqos/functions/activate) | | Legacy machine mirroring | Operated by peaq for partner fleets; 240,397 machines mirrored as of 2026-09-06, currently owned by `MachineMigrationHub` | [Legacy machines](#legacy-machines) | | Agung testnet deployment | Live (`agung-2026-08-28`), same contract surface | [Contracts](/peaqos/concepts/contracts#economics-2-0-agung-testnet) | | 2.0 MCR API (ratings and monetization for 2.0 machines) | Live at `mcr-20.peaq.xyz` since 2026-09-05: monetization opt-in through the SDKs and CLI, ratings over HTTP by decimal machine ID. The SDK query helpers are still disabled in Tokenomics mode | [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines) | | Cross-chain machine relocation | Disabled on chain (`InfoDesk.isBridgingEnabled()` is false); SDKs expose status reads only. Solana is registered as the first peer as of 2026-09-09 | [Omni-chain](/peaqos/concepts/omni-chain) | | Trust validator staking (permissionless) | Contract live, no validators staked; not exposed in SDKs or CLI | [Trust validators](#trust-validators) | | Block reward split (treasury 70 / staking 30) | Enacted 2026-09-06 11:36 UTC (runtime spec 113, block 11,512,213) | [Tokenomics](/peaqchain/learn/tokenomics#economics-2-0) | ## Activation and the bond One transaction to `MachineStateAndSync.activateMachine` mints the machine's ERC-721 in `MachineRegistry`, stores its DID document, bonds PEAQ in `MachineSubscription` on a tier, and records peaq as the home chain in `CrossChainMirror`. The sender is the owner and the payer. The bond is quoted in USD per tier and converted to PEAQ at the oracle rate when you call: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} bondAmount = MachineSubscription.requiredPeaqAmount(tier) // tierPriceUsd(tier) / latest PEAQ price voucherCredit = min(pendingVoucherCredit(owner), bondAmount) netPeaqAmount = bondAmount - voucherCredit // what leaves your wallet ``` Only the net amount is transferred (spender `MachineSubscription`). The machine's recorded bond stays at the full amount. A voucher can cover the whole bond, in which case nothing is transferred and the machine is still bonded in full. **The bond is not withdrawable.** It stays bonded while the subscription is renewed, and unwinds through grace and runoff when it is not. ### Tiers | Tier | Value | USD price per period, as configured in `InfoDesk` on 2026-09-09 | Bond quoted on mainnet on 2026-09-09 at 17:45 UTC | | :---- | :---- | :-------------------------------------------------------------- | :------------------------------------------------ | | Entry | `0` | \$0.02 | 0.670 PEAQ | | Basic | `1` | \$0.20 | 6.701 PEAQ | | Pro | `2` | \$40.00 | 1,340.224 PEAQ | Tier prices are protocol parameters set by governance (`InfoDesk.setTierPriceUsd`). Basic was raised from $0.04 to $0.20 on 2026-09-06 at 14:15 UTC and Pro from $0.06 to $40 on 2026-09-07 at 17:39 UTC (both `TierPriceUsdSet`); Entry is unchanged since deployment. The on-chain prices now match the economics paper ($0.02 / $0.20 / \$40 per year). When governance changes a price, the quote changes with it. Always preview before you spend: `peaqos activate ... --dry-run`, `previewMachineActivation`, or `preview_machine_activation`. The PEAQ figure moves with the oracle price every day. Tiers also carry points that weight bonding rewards per operator. Higher tiers earn more points per machine. ### Paying in USDT `--payment usdt` (CLI) or `activateMachineWithUsdt` / `activate_machine_with_usdt` (SDKs) settles the PEAQ-quoted bond in USDT through `SubscriptionTokenProvisionPool`, which performs the conversion. You approve a maximum USDT amount derived from the quote and your accepted slippage; the call never spends more than that maximum. The bond, the voucher credit, and the net amount stay denominated in PEAQ. ## Subscription lifecycle Parameters read from `InfoDesk` on peaq mainnet on 2026-09-04: | Parameter | Value | | :------------------------- | :----------------------------------------------------- | | Subscription period | 365 days | | Grace | 14 days after the period ends. No decay. | | Runoff | 14 days after grace. The bond decays linearly to zero. | | Epoch (settlement cadence) | 12 hours | ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Active ──period ends──► Grace (14 d) ──► Runoff (14 d, linear decay) ──► Terminated ▲ │ │ └──────── renew ─────────┴────────────────────┘ ``` * **Renew** at any point in Active, Grace, or Runoff. Renewal uses the stored tier and extends from the stored period end, not from now. It cures Grace or Runoff immediately. * **Runoff** moves the decayed part of the bond half to the burn address and half to the treasury, one step per daily settlement. As of 2026-09-09 the burn address in `InfoDesk` is unset, so the burn half does not reduce supply yet. * **Terminated** machines are retired: the NFT is burned and the subscription record deleted. A machine that is relocating is never terminated mid-flight. * **Fraud termination** (protocol owner only) forfeits the whole bond immediately, 50% burn and 50% treasury. * **Suspend / resume** is a voluntary pause by the owner or controller. It does not change the subscription. ## Credits, not payouts Economics 2.0 never pays PEAQ to a wallet. Two kinds of credit accrue to the machine **owner** and are redeemed against the next bond: | Credit | Earned by | Redeemed on | | :------------------------ | :--------------------------------------------------------- | :-------------------------- | | Bonding reward credit | Holding active, bonded machines (weighted by tier points) | Renewal | | Onboarding voucher credit | Reported service revenue during the trailing 90-day window | Activation of a new machine | Both are funded from the same surplus (`epochPool`, whatever PEAQ sits in `MachineSubscription` beyond bonds and committed credits) and split by one protocol parameter each epoch. A controller who pays for a renewal does not receive the credit; it lands on the owner. ## Machine ID and DID ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} machineId = uint256(keccak256(abi.encode(machineType, credentialSubject))) did = did:peaq: ``` The machine ID is the ERC-721 token ID. It is a full `uint256` (`bigint` in JavaScript, `int` in Python, a decimal string on every wire). There is no mapping from legacy sequential IDs or from `did:peaq:0x
`: a legacy machine that migrates gets a new 2.0 ID. See [peaqID](/peaqos/concepts/peaqid). ## Ownership and control | Role | Rights | | :---------------------------- | :----------------------------------------------------------------------------------------- | | Owner (holds the NFT) | Transfer the NFT, set or clear the controller, everything the controller can do | | Controller (DID `controller`) | Suspend, resume, renew, update verification methods, authentication, and service endpoints | Transferring the NFT moves ownership and retains the controller. There is no operator-sponsored activation: the account that signs `activateMachine` owns the machine and pays the bond. Fleets use machine-owned, operator-controlled activation; see [Fleet onboarding](/peaqos/guides/proxy-operator-fleet). ## Trust validators Three different roles are easy to confuse. Keep them apart: | Role | What it is | State | | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | | Validators (chain) | The collators that produce peaq blocks through the `parachainStaking` pallet | Unchanged by Economics 2.0. See [Becoming a validator](/peaqchain/build/advanced-operations/node-operations/becoming-a-validator/introduction). | | Trust validator node | An allowlisted account (`InfoDesk.isTrustValidator`) that commits currency prices to `PriceOracle` daily and runs the settlement functions (`settleLifecycle`, `settleEpoch`, `settleRevenueWindow`) | One node, run by peaq. First price committed 2026-09-03 11:20 UTC. | | Trust validator staking | Permissionless staking and delegation in `TrustValidatorStaking` | Contract live on mainnet, no validators staked as of 2026-09-09. Not exposed in the SDKs or CLI. | Until the oracle holds a price, `activateMachine` reverts with `PriceNotAvailable`. Prices are 8-decimal USD values keyed by `keccak256("PEAQ")`. ## Coordination fees and x402 `CoordinationFeeCollector` can charge a per-tier USD coordination fee on machine services and accepts x402-style payments through EIP-3009 `transferWithAuthorization`. The fee is configured at 0 for every tier on 2026-09-04, so no coordination fee is collected today. Not exposed in the SDKs or CLI. ## Legacy machines Machines onboarded under Tokenomics 1.0 (`IdentityRegistry`, 1 PEAQ native bond, separate `MachineNFT`) keep working on the 1.0 contracts and the 1.0 MCR API. `MachineMigrationHub` mirrors them into Economics 2.0: peaq mirrors a legacy machine as a 2.0 machine owned by the hub, and the hub pays the tier bond. Handing the permanent 2.0 ID to the machine's owner is a later step. Mirroring is operated by peaq for partner fleets and is not exposed in the SDKs or the CLI. Mirroring started on 2026-09-04; as of 2026-09-06, 240,397 mirrored machines are owned by the hub, and peaq moved part of them from Entry to Basic after the Basic price change (`SubscriptionTierPushed`). `MachineSubscription.totalBondedAmount()` was 1,821,599 PEAQ on 2026-09-09 at 17:45 UTC. A mirrored machine's legacy 1 PEAQ bond stays in `IdentityStaking`; the 2.0 tier bond is bonded in addition. ## Cross-chain relocation Economics 2.0 relocates a whole machine record between registered chains through `MachineBridgeAdapter` and `CrossChainMirror` (LayerZero V2 transport). This differs from the 1.0 Machine NFT bridge, which moves only the NFT. Relocation is switched off on peaq mainnet and agung (`InfoDesk.isBridgingEnabled()` returns false), and the SDKs expose relocation **status reads only**. The 1.0 `bridgeNft` / `bridge_nft` path throws `MACHINE_RELOCATION_UNAVAILABLE` in Tokenomics mode. See [Omni-chain](/peaqos/concepts/omni-chain). Solana mainnet is the first registered relocation peer: peaq's `MachineBridgeAdapter` points at the Solana adapter (protocol chain 5, LayerZero EID 30168) as of 2026-09-09, and the Solana program set is live (five programs on 2026-09-09, nine since 2026-09-11). Both flags stay false on both chains, so no machine can relocate yet. Program IDs are on [Smart contracts](/peaqos/concepts/contracts#economics-2-0-solana-mainnet). ## Related * [Activate](/peaqos/functions/activate): the one-transaction onboarding * [Smart contracts](/peaqos/concepts/contracts): addresses on peaq mainnet and agung * [Tokenomics](/peaqchain/learn/tokenomics#economics-2-0): supply, inflation, the block reward split (70 / 30 since spec 113) and the wallet consolidation * [CLI: machine](/peaqos/cli#peaqos-machine): lifecycle, subscription, ownership, and DID commands # Events Source: https://docs.peaq.xyz/peaqos/concepts/events Revenue and activity events that feed a machine's credit history. Events are the atomic units of a machine's financial history. The EventRegistry contract stores them onchain; the MCR scoring pipeline reads them to compute credit ratings. ## Two event types | Type | Value | Description | | :------- | :---- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Revenue | `0` | The machine earned money. `value` carries the amount as a minor-unit integer (cents for USD/HKD, whole units for JPY/KRW) in the event's `currency`; the MCR scoring pipeline normalizes to USD cents via FX at the event timestamp. Single-event `submitEvent` resolves an omitted `currency` to `"USD"` for revenue and `""` for activity; batch submit requires an explicit `currency` per event. | | Activity | `1` | The machine performed an action (telemetry, data generation, task completion). `value` may be 0. `currency` must be empty (`""`); non-empty currency on an activity event reverts `InvalidCurrencyShape`. | Both SDKs export these as constants: ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { EVENT_TYPE_REVENUE, // 0 EVENT_TYPE_ACTIVITY, // 1 } from "@peaqos/peaq-os-sdk"; ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.constants import ( EVENT_TYPE_REVENUE, # 0 EVENT_TYPE_ACTIVITY, # 1 ) ``` ## MachineEvent schema Every event stored in the EventRegistry follows this structure: The SDKs expose the stored struct as `MachineEvent` (with `dataHash` / `data_hash` as a 32-byte `keccak256`). The SDK input type `SubmitEventParams` takes `rawData` / `raw_data` bytes instead; the SDK computes the hash for you before the call. | Field | Type | Description | | :-------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `machineId` | `uint256` | Machine identity from IdentityRegistry. Primary key linking event to machine. | | `eventType` | `uint8` | `0` = revenue, `1` = activity. | | `value` | `uint256` | Amount earned (revenue) or metric value (activity). May be 0 for activity events. Revenue events express `value` as an ISO 4217 minor-unit integer (cents for USD/HKD, whole units for JPY/KRW) in the event's `currency`. | | `currency` | `string` | Revenue events: 3-10 char uppercase alphanumeric ISO 4217 code (e.g. `"USD"`, `"HKD"`, `"JPY"`). Activity events: empty string `""`. Validated on-chain. | | `timestamp` | `uint256` | Unix timestamp when the event occurred, not when it was submitted. | | `dataHash` | `bytes32` | `keccak256(raw_data)`. Raw data stays off-chain; hash proves integrity. | | `trustLevel` | `uint8` | `0` = self-reported, `1` = on-chain verifiable, `2` = hardware-signed. | | `sourceChainId` | `uint256` | Chain where the original activity occurred (e.g., 3338 for peaq, 8453 for Base). 0 for off-chain events. | | `sourceTxHash` | `bytes32` | Transaction hash on the source chain. Combined with `sourceChainId`, creates a verifiable cross-chain link. Null for off-chain events. | | `metadata` | `bytes` | Arbitrary bytes stored on-chain alongside the event (4096-byte cap). The MCR API server parses JSON metadata into the `event_data[].metadata` object surfaced on `/machine/{did}` for machines with `data_visibility: onchain`; the SDK only writes raw bytes. | ## Cross-chain revenue accounting `sourceChainId` and `sourceTxHash` together form a cross-chain audit trail. When a machine earns revenue on Base, the proxy submits the event to the EventRegistry on peaq with `sourceChainId = 8453` and the Base transaction hash. Any verifier can look up the transaction on the source chain and confirm the event happened. | Scenario | `sourceChainId` | `sourceTxHash` | | :--------------------- | :-------------- | :-------------------- | | Revenue earned on Base | `8453` | Base transaction hash | | Revenue earned on peaq | `3338` | peaq transaction hash | | Off-chain telemetry | `0` | null (`bytes32(0)`) | ## Data integrity via `dataHash` Raw event data stays with the project (in their database, behind their API). The `dataHash` stored onchain is `keccak256(raw_data)`, computed by the SDK before submission. This keeps gas costs at 32 bytes per event regardless of payload size, while anyone can fetch the raw data from the project's `data_api` (set in the machine's DID) and verify `keccak256(fetched_data) == dataHash`. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { computeDataHash } from "@peaqos/peaq-os-sdk"; const rawData = new Uint8Array([1, 2, 3]); const hash = computeDataHash(rawData); // hash === "0x..." (keccak256) ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.utils import compute_data_hash raw_data = b"revenue data" data_hash = compute_data_hash(raw_data) # data_hash is 32 bytes (keccak256) ``` ## Authorization EventRegistry reads IdentityRegistry to verify submit authority. `msg.sender` must be one of: | Path | Address | Use case | | :------------- | :--------------------------- | :-------------------------------------------------------------------------------------------------------------------- | | NFT owner | Identity NFT holder | The account that holds the Identity NFT submits directly. | | Machine wallet | `machineWalletOf(machineId)` | The machine's registered wallet submits. Typical for self-managed flows where the machine signs its own transactions. | | Operator | `operatorOf(machineId)` | Assigned operator submits on behalf of the machine. Only valid when an operator has been set (non-zero). | All three paths produce identical on-chain records: the `machineId` in the stored event always identifies the machine, regardless of who submitted. ## Validation The JS SDK exports `validateSubmitEventParams`, which checks all nine fields before any chain interaction: ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { validateSubmitEventParams } from "@peaqos/peaq-os-sdk"; validateSubmitEventParams({ machineId: 1, eventType: 0, value: 100, // $1.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000), rawData: new Uint8Array([1, 2, 3]), trustLevel: 1, sourceChainId: 8453, sourceTxHash: "0xabc...def", metadata: new Uint8Array([]), }); ``` Key validation rules: * `eventType` must be 0 or 1 * `value` must be a non-negative integer (subunit, per ISO 4217 minor units) * `trustLevel` must be 0, 1, or 2 * `sourceChainId` must be a supported chain (0, 3338, or 8453) * When `trustLevel` is 1 (on-chain verifiable), `sourceTxHash` is required * `rawData` must be non-empty when provided (null is allowed, empty array is not) * `currency` on revenue events must match `^[A-Z0-9]{3,10}$`; on activity events it must be `""` * `timestamp` must be a positive integer; the EventRegistry contract additionally rejects `timestamp > block.timestamp` (`FutureTimestamp` revert) * `metadata` is capped at 4096 bytes on-chain (`MetadataTooLarge` revert above the limit) ## Cross-links * [JS SDK `validateSubmitEventParams`](/peaqos/sdk-reference/sdk-js#validatesubmiteventparams) documents every validation rule * [JS SDK `computeDataHash`](/peaqos/sdk-reference/sdk-js#computedatahash) documents the hashing utility * [Machine Credit Rating](/peaqos/concepts/machine-credit-rating) shows how events feed into a machine's credit history # Gas Station Source: https://docs.peaq.xyz/peaqos/concepts/gas-station 2FA-gated faucet that funds fresh machine wallets on peaq chain so they can bond and register. Gas Station is a 2FA-gated faucet that funds fresh machine wallets on peaq chain so they can bond and register. ## Why Gas Station exists A newly generated machine keypair has zero balance. Before it can register an identity (which requires bonding 1 PEAQ), it needs native tokens for gas. Gas Station solves this bootstrap problem: the owner enrolls in 2FA once, then funds each machine wallet with a single SDK call. ## 2FA setup flow Call `setupFaucet2FA` with the owner's address. The faucet returns an `otpauthUri` for manual entry and a `qrImageUrl` for scanning. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const setup = await client.setupFaucet2FA( ownerAddress, "https://depinstation.peaq.xyz" ); console.log(setup); // setup.otpauthUri : paste into authenticator app // setup.qrImageUrl : render immediately, expires in ~2 minutes ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} setup = client.setup_faucet_2fa( owner_address="0xOwner...", faucet_base_url="https://depinstation.peaq.xyz", ) print(setup) # setup["otpauth_uri"] : paste into authenticator app # setup["qr_image_url"] : render immediately, expires in ~2 minutes ``` Open any TOTP-compatible authenticator (Google Authenticator, Authy, 1Password). Scan the QR code or manually enter the `otpauthUri`. The QR image expires after approximately 2 minutes. Submit the current TOTP code from the authenticator app to activate 2FA for this owner address. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} await client.confirmFaucet2FA( ownerAddress, "https://depinstation.peaq.xyz", "123456" // TOTP code from authenticator ); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.confirm_faucet_2fa( owner_address="0xOwner...", faucet_base_url="https://depinstation.peaq.xyz", two_factor_code="123456", ) ``` ## Funding a machine wallet After 2FA is active, fund any machine wallet with `fundFromGasStation`. The response is a discriminated union: either `success` (transfer landed) or `skipped` (wallet already funded). ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await client.fundFromGasStation( { ownerAddress: "0xOwner...", targetWalletAddress: machineAddress, chainId: "peaq", twoFactorCode: "654321", }, "https://depinstation.peaq.xyz" ); if (result.status === "success") { console.log("Funded:", result.txHash, result.fundedAmount); } else { // status === "skipped" console.log("Already funded:", result.currentBalance); } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} result = client.fund_from_gas_station( owner_address="0xOwner...", target_wallet_address=machine_address, chain_id="peaq", two_factor_code="654321", faucet_base_url="https://depinstation.peaq.xyz", ) if result["status"] == "success": print("Funded:", result["tx_hash"], result["funded_amount"]) else: # status == "skipped" print("Already funded:", result["current_balance"]) ``` ### Success response JS (camelCase). The SDK retains the idempotency key on the response so callers can persist it for retry recovery: | Field | Type | Description | | :------------- | :------------------- | :----------------------------------------------------------- | | `status` | `"success"` | Transfer completed | | `requestId` | string | Idempotency key (echoed from request, or SDK-generated UUID) | | `txHash` | `0x`-prefixed hex | Transaction hash of the funding transfer | | `fundedAmount` | string (decimal wei) | Amount transferred | Python (snake\_case). The SDK does not echo `request_id` on the response. If you need it for retry recovery, pass it in explicitly and hold the value yourself: | Field | Type | Description | | :-------------- | :------------------- | :--------------------------------------- | | `status` | `"success"` | Transfer completed | | `tx_hash` | `0x`-prefixed hex | Transaction hash of the funding transfer | | `funded_amount` | string (decimal wei) | Amount transferred | ### Skipped response JS (camelCase): | Field | Type | Description | | :--------------- | :------------------- | :------------------------------------ | | `status` | `"skipped"` | Wallet already has sufficient balance | | `requestId` | string | Idempotency key | | `currentBalance` | string (decimal wei) | Target wallet's current balance | | `minGasBalance` | string (decimal wei) | Faucet-configured minimum threshold | Python (snake\_case): | Field | Type | Description | | :---------------- | :------------------- | :------------------------------------ | | `status` | `"skipped"` | Wallet already has sufficient balance | | `current_balance` | string (decimal wei) | Target wallet's current balance | | `min_gas_balance` | string (decimal wei) | Faucet-configured minimum threshold | ## Rate limits and caps Gas Station enforces three independent throttles to prevent abuse, plus a per-chain minimum balance threshold that decides whether a funding request transfers anything at all: | Limit | Scope | Surfaces as | Description | | :------------------ | :--------- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rate limit | Per owner | `RATE_LIMITED` | Too many funding requests in too short a window. Back off and retry. | | Daily funding cap | Per owner | `CAP_EXCEEDED_OWNER` | Total funded amount per owner per day. | | Daily funding cap | Per wallet | `CAP_EXCEEDED_WALLET` | Total funded amount per target wallet per day. | | Minimum gas balance | Per chain | `skipped` response | When the target wallet already holds at least this much, the faucet returns `skipped` with `currentBalance`/`current_balance` and `minGasBalance`/`min_gas_balance` instead of transferring. | Exact cap values, rate-limit windows, and the minimum gas balance are configured server-side and are subject to change. Branch on the error codes (or the `skipped` status) rather than hard-coding numbers. ## Error codes Gas Station defines 20 error codes. Not every endpoint can raise every code; the subset depends on what the endpoint actually does. ### `POST /2fa/setup`: `setupFaucet2FA` / `setup_faucet_2fa` | Code | Meaning | | :---------------------- | :--------------------------------------------------------------- | | `INVALID_OWNER_ADDRESS` | Owner address failed server-side format validation (setup-only). | | `INVALID_PAYLOAD` | Request body did not match the spec (e.g. unknown `format`). | | `QR_GENERATION_FAILED` | The faucet failed to render the QR image (setup-only). | | `INTERNAL_ERROR` | Unhandled server-side failure. | ### `POST /2fa/confirm`: `confirmFaucet2FA` / `confirm_faucet_2fa` | Code | Meaning | | :------------------- | :-------------------------------------------------------- | | `INVALID_2FA` | TOTP code did not match the secret. | | `2FA_NOT_CONFIGURED` | Owner never completed setup. Call `setupFaucet2FA` first. | | `2FA_LOCKED` | Owner locked after too many invalid attempts. | | `INTERNAL_ERROR` | Unhandled server-side failure. | ### `POST /faucet/fund`: `fundFromGasStation` / `fund_from_gas_station` | Code | Meaning | | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------- | | `INVALID_2FA` | TOTP code did not match. | | `2FA_NOT_CONFIGURED` | Owner never completed setup. | | `2FA_NOT_ACTIVE` | Setup done but never confirmed. Call `confirmFaucet2FA` first. | | `2FA_LOCKED` | Owner locked after too many invalid attempts. | | `DUPLICATE_REQUEST` | A request with this `requestId` is still in flight. | | `REQUEST_ALREADY_PROCESSED` | This `requestId` has already completed. | | `RATE_LIMITED` | Faucet rate limit exceeded. | | `CAP_EXCEEDED_OWNER` | Owner daily funding cap exceeded. | | `CAP_EXCEEDED_WALLET` | Wallet daily funding cap exceeded. | | `INVALID_PAYLOAD` | Request body validation failed server-side. | | `INVALID_OWNER_ADDRESS` | Malformed `ownerAddress`. | | `INVALID_TARGET_ADDRESS` | Malformed `targetWalletAddress`. | | `INVALID_CHAIN_ID` | Unsupported `chainId`. | | `INVALID_REQUEST_ID` | The supplied `requestId` was not a valid UUID. | | `TRANSFER_FAILED` | On-chain transfer failed. | | `CHAIN_RPC_ERROR` | Faucet's upstream RPC provider failed. | | `QR_NOT_FOUND` | QR token does not exist (rare; surfaces when a stale QR is referenced). | | `QR_EXPIRED` | QR token has expired. | | `INTERNAL_ERROR` | Unhandled server-side failure. The SDK also raises this code locally for non-JSON bodies and unexpected envelope shapes. | For SDK-owned message strings and HTTP status codes per code, see [errors](/peaqos/sdk-reference/errors). ## Cross-links * [JS SDK faucet methods](/peaqos/sdk-reference/sdk-js#gas-station) documents `setupFaucet2FA`, `confirmFaucet2FA`, and `fundFromGasStation` * [Python SDK faucet methods](/peaqos/sdk-reference/sdk-python#gas-station) documents the Python equivalents * [Self-managed onboarding guide](/peaqos/guides/self-managed-onboarding) shows Gas Station in the full registration flow * [Errors reference](/peaqos/sdk-reference/errors) lists all error codes with HTTP status codes # Machine Credit Rating Source: https://docs.peaq.xyz/peaqos/concepts/machine-credit-rating A Moody's-style score that summarizes a machine's creditworthiness from its on-chain history. Machine Credit Rating (MCR) is a letter rating, modeled on traditional credit agencies, that summarizes a machine's financial standing from its on-chain history. Any application can query a machine's MCR through the public [MCR API](/peaqos/api-reference/overview): no authentication, no permissions. It travels with the machine across chains. ## Rating scale MCR ratings use a familiar AAA-to-NR scale. The integer `mcr_score` (0-100, rounded) maps to a letter via fixed thresholds: | Rating | `mcr_score` | Meaning | | :-------------- | :---------- | :------------------------------------------------- | | **AAA** | ≥ 95 | Highest credit quality | | **AA** | ≥ 85 | Very high credit quality | | **A** | ≥ 75 | High credit quality | | **BBB** | ≥ 60 | Investment grade | | **BB** | ≥ 45 | Below investment grade | | **B** | ≥ 30 | Minimum rated | | **NR** | 0 | Not rated. Score below 30, or machine is unbonded | | **Provisioned** | 0 | Newly registered. Not enough history to score yet. | Both `NR` and `Provisioned` return `mcr_score = 0`; the `mcr` letter and `bond_status` field on `/mcr/{did}` distinguish the two. ## Lifecycle: Provisioned graduation A freshly registered, bonded machine is **Provisioned**: on-chain, but not yet rated. It graduates onto the rated scale once it has both: * **Enough events:** a sustained set of qualifying events. Activity-only machines (sensors, drones without revenue) can graduate via activity alone; revenue is not required. * **Enough history:** the first-to-last event span covers a sustained operating window. Until both gates are cleared the machine returns `mcr: "Provisioned"` with `mcr_score: 0`, regardless of how strong the underlying activity is. Ratings are not assigned. They're earned. A machine's standing reflects what it has done on-chain. ## How the score is computed The MCR server fetches every event from the `EventRegistry`, normalizes revenue values to USD cents via FX at the event's timestamp, and combines them into a `0`–`100` integer score that maps to a letter rating. The exact weighting is not published. The score is built from a small set of inputs: **Bond.** Every bonded machine receives a baseline contribution. An unbonded machine returns `(0, "NR")` immediately. Bond is the floor for any score at all. **Revenue and activity.** Two factors derived from the event history. Each rewards consistency (regular cadence), depth (sustained activity over many events), and tenure (longer-running machines). Revenue is daily-aggregated: events are summed per UTC calendar day, and only days that meet a minimum economic threshold count toward revenue. **Trust level.** Per-event trust tiers, with revenue events weighted by economic value. Higher tiers contribute more; `0` self-reported, `1` on-chain verifiable, `2` hardware-signed. See [Trust levels](/peaqos/concepts/trust-levels). A long-running self-reported machine earns a small graduation bonus on its trust contribution after sustained operation, but it remains below the on-chain tier. **Freshness.** Recency decay applied to the trust contribution. A machine that hasn't reported recently scores progressively lower; a sustained dormancy gap clamps freshness at a floor until the machine demonstrates renewed activity over a recovery window. Recovery is earned, not granted by a single recent event. **Negative event flag.** While an active flag is set on the machine, the trust contribution is reduced for a fixed window from the flag timestamp; the penalty expires automatically after the window closes. The final score is clamped to `[0, 100]` and rounded to an integer before mapping to a letter. ## Revenue trend `/mcr/{did}` exposes a `revenue_trend` field with one of four values, derived from short-term vs longer-term moving averages over qualifying revenue events: | Value | Meaning | | :--------------- | :---------------------------------------------------------- | | `"up"` | Recent revenue is materially above the longer-term baseline | | `"stable"` | Recent revenue tracks the longer-term baseline | | `"down"` | Recent revenue is materially below the longer-term baseline | | `"insufficient"` | Not enough qualifying revenue events to compute a trend | ## Multi-currency revenue Revenue events submitted in any ISO 4217 currency get FX-normalized to USD by the MCR server before scoring. Pass `currency` directly to `submitEvent` and supply `value` as the currency's minor-unit integer (cents for USD/HKD, whole units for JPY/KRW); the server fetches the historical rate at the event's `timestamp` and stores both `origin_value` and the computed `usd_value` (USD cents). Two distinct degradation paths show up on the wire: * **Unsupported currency.** The per-event `amount_status` becomes `"unsupported_currency"`. The revenue value is ignored when scoring (treated as \$0). This is a partner-input error, not infra failure, so it does **not** flip `mcr_degraded`. * **FX infrastructure failure.** When the MCR server has to fall back to a stale snapshot (`fx_source = "stale_latest"`) or has no FX at all (`fx_source = "default_usd_fx_outage"`), the event's `amount_status` becomes `"fx_unavailable"` **and** the top-level `mcr_degraded: true` flag flips on `/mcr/{did}`, so consumers can distinguish a degraded score from a quiet machine. See [Submit events: currency and value units](/peaqos/guides/submit-events#currency-and-value-units). ## Cross-links * [Events](/peaqos/concepts/events): the revenue and activity records that feed MCR * [Trust levels](/peaqos/concepts/trust-levels): what each `trust_level` means at submit time * [GET /mcr/\{did}](/peaqos/api-reference/get-mcr): fetch a machine's current rating * [Qualify function](/peaqos/functions/qualify): the function that exposes MCR end-to-end # Machine Markets Source: https://docs.peaq.xyz/peaqos/concepts/machine-markets Machine Agents, skills, services, and search. How an AI agent paired to a machine actually buys what the machine needs. Machine Markets is the orchestration layer behind [Scale](/peaqos/functions/scale). It pairs an AI agent to an activated, bonded machine, gives that pairing a policy with spend limits and an allow/denylist, and exposes a curated catalogue of capabilities and provider services the agent can search against using the machine's context. The agent's legitimacy is the machine's: peaqID, Machine NFT, MCR, and trust level all carry through. ## Roles | Role | Description | | :--------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Machine** | An activated peaqOS device with a peaqID, Machine NFT, smart account, and bond. The machine is the principal. Funds, identity, and reputation belong to it. | | **Proxy Operator / Machine Owner** | Human (or org) that controls the machine. Funds the wallet, pairs the agent, sets the policy, can revoke or repair at any time. | | **Machine Agent** | Third-party AI agent (Claude, OpenAI, Virtuals, Teneo, your own) given delegated, bounded authority over the machine's smart account. peaq does not provide the agent. | | **Machine-Side Runtime Agent** | Optional process running on the device that registers local runtime endpoints. Used for skills that execute on the machine rather than at a remote provider. | | **Service Provider** | External entity (QVAC, agentic.market services over x402, pay.sh services, etc.) whose service is registered in the catalogue and consumed by Machine Agents. | ## Core objects ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type Machine = { id: string; displayName: string; status: "draft" | "active" | "degraded" | "blocked" | "archived"; ownerId: string; identityRef: string; // did:peaq:0x... or peaqos:machine: identityProof?: { /* EIP-191 controller signature, see API ref */ } | null; machineType: string; runtimeProfile: string; capabilities: string[]; labels: Record; policyIds: string[]; skillKeys: string[]; createdAt: string; updatedAt: string; }; type AgentPairing = { id: string; machineId: string; status: "active" | "paused" | "revoked"; agentAddress: string; agentDid: string | null; agentProvider: string; // "anthropic" | "openai" | "virtuals" | ... agentRole: string; // free-form, e.g. "ops" | "trader" | "buyer" description?: string | null; verification: { method: "eip191"; signerAddress: string; verifiedAt: string; challengeExpiresAt: string; } | null; delegationPolicy: { allowedSkillKeys: string[]; deniedSkillKeys: string[]; allowedServiceIds: string[]; deniedServiceIds: string[]; perTransactionLimit?: number | null; dailySpendLimit?: number | null; currency?: string | null; }; hasAuthToken: boolean; tokenLastFour: string | null; sessionId: string | null; sessionTokenId: string | null; sessionIssuedAt: string | null; sessionExpiresAt: string | null; pairingToken?: string; // signed HS256 session JWT, returned once at create/rotate createdAt: string; updatedAt: string; }; ``` ## Skills vs services Two things look similar. They aren't. * A **skill** is a capability schema. `storage.object` says "an object-storage service exposes these operations". Skills are the contract. * A **service** is a concrete instance of a skill from a specific provider. "Exa search via agentic.market" is a service. Services are what you actually buy. Service types: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} oracle.price-feed compute.marketplace storage.object data.location network.partner-console identity.proof-of-person device.control machine.commerce ``` Execution modes: * **Native**: peaqOS executes the skill server-side or on a registered machine-side runtime endpoint. No human handoff. * **External handoff**: the orchestrator returns a structured handoff (URL, contact, setup steps) and the agent or operator completes the purchase out of band. Used when a provider integration is partner-required or credentials-required. Integration status surfaces how ready a service is to execute end-to-end: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} native | credentials-required | partner-required | local-config-required | setup-required | docs-only ``` ## Identity proof flow Machine registration with the orchestrator is gated by a DID-controller signature. The flow: 1. Call `POST /machine-identity/challenges` with the machine's `identityRef`. The orchestrator looks up the controller addresses from peaqOS MCR data and returns a short-lived `challengeId` plus a `message`. 2. Sign `message` with the controller's private key using EIP-191 `personal_sign`. 3. Submit `{ challengeId, signature }` as `identityProof` when calling `POST /machines`. The orchestrator verifies the signature recovers to one of the controller addresses before persisting. The stored proof is re-checked against MCR data on subsequent machine-bound writes. The proof expires. Re-challenge when it does. ## Pairing flow Pairing an agent to a machine follows the same challenge-sign-verify pattern, scoped to the agent's wallet key: 1. Call `POST /machines/:machineId/agent-pairings/challenges` with `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`. The orchestrator returns an `AgentPairingChallenge` with a `challengeId`, a machine-bound `message`, and an `expiresAt`. 2. The Machine Agent signs `message` (EIP-191 `personal_sign`) with the wallet key behind `agentAddress`. 3. Call `POST /machines/:machineId/agent-pairings` with `agentProof: { challengeId, signature }` and the delegation policy. The orchestrator verifies the signature recovers to `agentAddress`, persists the pairing with `verification` metadata, and returns the `AgentPairing` with a signed HS256 session JWT in `pairingToken`. Store the token client-side and send it as `x-agent-pairing-token` on market writes. 4. Tokens expire (default 1 hour). Rotate by issuing a fresh challenge and calling `POST /machines/:machineId/agent-pairings/:pairingId/sessions` with the new proof. A policy change invalidates the current token's `delegationPolicyHash`, so rotate after every `PATCH` to the delegation policy. ## Order lifecycle A successful purchase walks the order state machine: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} created -> payment_pending -> ready -> executing -> delivered -> confirmed ``` Branches: `disputed` if the buyer raises a dispute after delivery; `cancelled` if a refund settles before delivery; `failed` if execution errors; `handoff` if the service ships as an external handoff rather than a native skill run. Each successful execution writes an `Outcome` and a `Run`. Outcomes record the service result; runs record the execution metadata (skill, route, status). Both are queryable via `GET /runs` and `GET /machines/:machineId/outcomes`. Payments follow their own state machine, coupled to the order: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} intent_created -> held -> release_pending -> released \-> refunded \-> frozen (on dispute) ``` `not_required` short-circuits the payment lifecycle for free or pre-authorised services. ## Payment rails The orchestrator quotes services in the rail the provider supports: | Rail | When it shows up | | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x402` | [agentic.market](https://agentic.market) services and most [pay.sh](https://pay.sh) services. Agent wallet responds to an HTTP 402 challenge. Execute goes through the `paidHttp` adapter — orchestrator replays the request with the agent's payment headers. | | `mpp` | Solana micro-payment protocol. Used by some pay.sh services on Solana. | | `vault-stripe` / `external` | Card-mediated or external handoff. The orchestrator returns a structured handoff and the agent or operator settles out of band. | | `wallet` / `wdk-usdt-transfer` | Direct USDT transfer on peaq. `wallet` upgrades to `wdk-usdt-transfer` once the proof is RPC-verified against the ERC-20 Transfer log. | | `onchain-escrow` / `escrow` | Funds locked into a service-specified escrow contract. | | `not-required` | Free or pre-authorised services. | | `offchain-record` | Off-chain attestation, recorded but not RPC-verified. | Payment proof modes: * **`recorded`**: operator attestation. The orchestrator stores the proof but does not verify on-chain. * **`rpc`**: the orchestrator queries an EVM RPC for the transaction receipt and the ERC-20 Transfer log, validating sender, recipient, token, and amount. Solana proofs are always `recorded`. Set `PEAQOS_PAYMENT_RPC_URL` (or `PEAQ_EVM_RPC_URL` / `PEAQOS_EVM_RPC_URL`) to enable RPC verification by default. ## Delegation, bounded The orchestration layer enforces the delegation policy server-side. Spend limits, allow/denylists, and skill restrictions are checked at request time, not just at pairing time. Mutating the policy through `PATCH /machines/:machineId/agent-pairings/:pairingId` takes effect immediately for new calls. Revoking a pairing (`DELETE`) terminates the agent's authority at once. ## Multichain shape Scale keeps peaq canonical for identity and registry state. Across supported chains: * peaqID, Machine NFT, and the Service Registry stay on peaq * Machine NFTs and smart accounts deploy on supported chains (Base first) * IdentityLite, DIDLite, and StakingLite mirror peaq state on each supported chain (Agung and Base Sepolia at launch); a dedicated MCR oracle on satellites is reserved * Machine Agents can pay across chains using the chain and token a service quotes in The orchestrator speaks `eip155:*` CAIP-2 identifiers through the SDK wallet layer. See [Wallets (OWS)](/peaqos/wallets) and the [Omni-chain concept](/peaqos/concepts/omni-chain) for the satellite-chain contract surface (DIDLite, IdentityLite, StakingLite). ## Related * [Scale function](/peaqos/functions/scale) * [Machine Markets API](/peaqos/api-reference/machine-markets-overview) * [Activate](/peaqos/functions/activate) * [Qualify](/peaqos/functions/qualify) # Machine NFT Source: https://docs.peaq.xyz/peaqos/concepts/machine-nft LayerZero V2 ONFT that anchors a machine's identity and links to its credit history. Machine NFT is the ERC-721 that represents a machine on peaq. It anchors a machine's identity and links to its credit history. ## Economics 2.0: one NFT, one ID Machines activated since 2026-09-01 under [Economics 2.0](/peaqos/concepts/economics-2-0) get **one** ERC-721 in `MachineRegistry`, minted inside the same `activateMachine` transaction that stores the DID document and bonds the tier. The token ID **is** the machine ID: `uint256(keccak256(abi.encode(machineType, credentialSubject)))`, a full-width value, and the DID is `did:peaq:`. There is no separate Identity NFT, no `mintNft` call, and no `tokenIdOf` lookup. Transferring the NFT moves ownership of the machine (the account that can rotate the DID controller and transfer again) and leaves the controller in place. Cross-chain movement of a 2.0 machine is a whole-record relocation through `MachineBridgeAdapter`, which is disabled on chain today. Everything below this line describes the **Tokenomics 1.0** model that machines onboarded before 2026-09-01 still use, and whose LayerZero V2 ONFT bridge to Base and Solana is live. ## What the Machine NFT represents (Tokenomics 1.0) The Machine NFT is a financial digital twin, separate from the Identity NFT that IdentityRegistry mints at registration. It carries revenue history, Machine Credit Rating, documentation links, and bond status in its metadata. The two tokens live in different token spaces and are linked by the on-chain `machineId`: | Token | Contract | Token ID | Purpose | Transferability | | :----------- | :--------------- | :--------------------------------------------- | :----------------------------------------------------------- | :--------------------------------------------------------------------------- | | Identity NFT | IdentityRegistry | Equal to `machineId` | On-chain identity credential, governs protocol authorization | Soulbound: transfers revert with `IdentityNFTSoulbound` | | Machine NFT | MachineNFT | Independent tokenId (not equal to `machineId`) | Financial digital twin, carries MCR and revenue metadata | Transfer moves the financial representation; identity stays with the machine | Selling or bridging the Machine NFT does not affect the machine's DID, event submission rights, or protocol authorization. **Machine ID ≠ Machine NFT token ID.** The `registerMachine` SDK method (`register()` on IdentityRegistry) and `registerFor` SDK method (`registerFor(machineAddress)` on IdentityRegistry) both return a `machineId`. The `mintNft(machineId, recipient)` SDK method (`mint(machineId, recipient)` on MachineNFT) results in a different token ID. MachineNFT has its own auto-incrementing tokenId sequence. Read the Machine NFT token ID back with `tokenIdOf(machineId)`. ## Machine Card The Machine Card is a peaqOS registration document that follows the ERC-8004 registration file pattern and is served by the MCR API at `/machines/{machine_id}`: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "type": "peaqos:registration:v1", "name": "Machine #42", "description": "peaqOS machine", "did": "did:peaq:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", "active": true, "services": [ { "name": "web", "endpoint": "https://example.com/machines/42/api" } ], "data_visibility": "private", "documentation_url": "https://example.com/machines/42", "operator": "did:peaq:0xOperatorAddress", "bond_status": "bonded", "event_count": 150, "registrations": [ { "type": "peaqos:registration:v1", "machineId": 42, "machineRegistry": "eip155:3338:0xIdentityRegistryAddress" } ] } ``` For the full MCR score and revenue summary, use [GET /machine/](/peaqos/api-reference/get-machine) or [GET /mcr/](/peaqos/api-reference/get-mcr); both are computed live by the MCR API on each request. ## Ownership semantics Unlike the Identity NFT, the Machine NFT is **not** minted automatically during registration. It's a two-step flow owned by the operator or machine: The proxy or machine calls **IdentityRegistry**'s `register()` (proxy uses `registerFor(machineAddress)`) and receives a `machineId`. IdentityRegistry simultaneously mints the Identity NFT to `machineAddress` with `tokenId == machineId`. SDK helpers: `registerMachine` / `registerFor`. The operator or machine then calls **MachineNFT**'s `mint(machineId, recipient)`. MachineNFT assigns a new, independent `tokenId`. Read the assigned token ID back with `tokenIdOf(machineId)`. SDK helper: `mintNft`. Call `writeMachineDIDAttributes` to store `machineId` and `nftTokenId` on the machine's DID, binding the identity to its financial twin. These attribute writes must be signed by the machine's own key (the DID subject); the proxy can't write them on the machine's behalf. Registration does not write them for you. ## Metadata flow `tokenURI()` returns a URL pointing to the MCR API. The API reads onchain events and DID attributes, computes the current MCR score, and returns the full machine profile JSON. A consumer or marketplace calls `tokenURI(tokenId)` on the **MachineNFT** contract and receives the metadata URL. The consumer calls [`GET /metadata/{token_id}`](/peaqos/api-reference/get-metadata) on the MCR API. The API reads onchain events and DID attributes, computes the current MCR score, and returns the same shape as [`GET /machine/{did}`](/peaqos/api-reference/get-machine): the full machine profile. The `baseURI` is updatable by the protocol admin. If the API domain changes, only the contract's `baseURI` is updated. No token migration needed. The lighter [Machine Card](#machine-card) (ERC-8004 registration document) lives at a separate endpoint: [`GET /machines/{machine_id}`](/peaqos/api-reference/get-machine-card). ## Cross-chain portability **Supported routes: peaq ↔ Base (SDK and CLI) and peaq ↔ Solana (live on chain since 2026-08-21, operated with peaq's tooling; the SDKs accept only `"base"` as a destination today).** Bridging is mainnet-only: LayerZero V2 has no DVN routes from agung, so `bridgeNft` / `bridge_nft` cannot be exercised against the testnet. The Machine NFT implements the LayerZero V2 ONFT standard. peaq is the home chain: its NFT supply is canonical and uses a **lock/unlock** adapter (`MachineNFTAdapter`); destination chains use the standard **burn/mint** pattern. A token is either transferable on peaq or represented by exactly one live twin on one spoke, never both. Canonical Machine NFT contract. Minting and DID linking happen here. Bridging out locks the NFT in `MachineNFTAdapter`; bridging back unlocks it. Bridged destination via LayerZero V2 ONFT (EID 30184). Bridging in mints; bridging back to peaq burns. Bridged destination via LayerZero V2 (EID 30168). The twin is a Metaplex Core asset in collection `4Xa1sDAHNWJ4WMkV9ZYvycbinkf8iwwdwNmYRxqZDPtz`, not an SPL token; its address changes on every crossing, so key on the peaq token ID. Return legs need a v0 transaction with an address lookup table.
peaq is the home chain. peaq → spoke locks on peaq + mints on the spoke; spoke → peaq burns on the spoke + unlocks on peaq.
Key properties: * **Home chain**: peaq. Minting happens on peaq after machine onboarding. * **Cross-chain**: LayerZero V2 bridges peaq ↔ Base and peaq ↔ Solana via the `MachineNFTAdapter` lock/unlock pattern on peaq paired with burn/mint on the destination. The Solana program is `HraxgdzfcAi3AnxRP5sGSrXGAb9ZT1tZTMNuh9vQLxTu`; its `tokenURI` equivalent points at the same MCR API metadata. * **Metadata**: `tokenURI()` resolves to the same MCR API URL regardless of which chain holds the NFT. * **Identity independence**: The machine's peaqID, DID attributes, and event submission stay on peaq regardless of where the NFT sits. ## Cross-links * [peaqID](/peaqos/concepts/peaqid) is the DID linked to the Machine NFT via the `nftTokenId` attribute * [GET /metadata/](/peaqos/api-reference/get-metadata) returns the full NFT metadata JSON * [Activate function](/peaqos/functions/activate) handles the minting flow during onboarding # Omni-chain Source: https://docs.peaq.xyz/peaqos/concepts/omni-chain How peaqOS mirrors identity, DID, and credit state from peaq to every supported chain via signed-push Lite contracts and an off-chain Signer Daemon. **Omni-chain V1 shipped with Scale.** Lite contracts and the Signer Daemon are live across supported EVM chains; per-chain proxy addresses live in the launch manifest. **Solana joined in August 2026**: see [Solana](#solana) below for what is live there and what is not. peaq stays canonical. Every other chain runs a thin, read-only mirror of peaq state, kept in sync by a Signer Daemon that watches finalized peaq events, packages them into EIP-712 batches, signs them with a per-chain push key, and pushes them to satellite Lite contracts. Two things this page does not cover. [Economics 2.0](/peaqos/concepts/economics-2-0) relocates whole machine records between registered chains through `MachineBridgeAdapter` and `CrossChainMirror` over LayerZero V2; that mechanism is switched off on chain today and the SDKs expose status reads only. And the 1.0 Machine NFT bridge (peaq ↔ Base, peaq ↔ Solana) is on the [Machine NFT](/peaqos/concepts/machine-nft#cross-chain-portability) page. This means a contract or app on Base, Ethereum, Polygon, or any other supported chain can resolve a machine's peaqID, identity, MCR, and bond status without an RPC hop to peaq, and without trusting an off-chain oracle on top. ## What the mesh looks like ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq home chain ┌────────────────────────┐ │ IdentityRegistry │ │ peaq DID precompile │ │ IdentityStaking │ │ MCR pipeline │ └──────────┬─────────────┘ │ finalized events ▼ ┌────────────────────────┐ │ Signer Daemon │ │ (off-chain, peaq-side) │ │ batches + EIP-712 sign │ └─┬────────┬─────────────┘ │ │ ┌──────────┘ └──────────┐ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Base satellite│ │ other chains │ │ IdentityLite │ │ IdentityLite │ │ DIDLite │ │ DIDLite │ │ StakingLite │ │ StakingLite │ └───────────────┘ └───────────────┘ ``` Domain separator: `name = "PeaqosLite"`, `version = "1.0.0"`. Each Lite is identified by `(chainId, verifyingContract)` per EIP-712. ## DIDLite DIDLite mirrors the peaq DID precompile's per-attribute records onto every satellite chain. Consumers on the satellite resolve any peaq DID account's attributes (including the locked `"peaqID"` attribute) without an extra cross-chain hop. Records are written exclusively by signed batches from the Signer Daemon; consumers only read. ### Public consumer views ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function readAttribute(address didAccount, bytes calldata attrName) external view returns ( bytes memory value, uint32 validity, uint64 lastUpdatedHomeBlock, uint64 lastBatchAcceptedAtTs ); function readAttributeRaw(address didAccount, bytes calldata attrName) external view returns (DIDAttributeRecord memory record, uint64 lastBatchAcceptedAtTs); // admin/debug only; ignores the pause gate; returns soft-deleted records as-is function lastHomeBlockApplied() external view returns (uint64); struct DIDAttributeRecord { bytes value; // raw bytes from peaq; empty after soft-delete uint32 validity; // peaq DID validity field uint64 lastUpdatedHomeBlock; bool removed; // soft-delete sentinel } ``` `readAttribute` reverts `LitePaused` while the Lite is paused. `readAttributeRaw` is an admin/debug carve-out that ignores the gate. `lastHomeBlockApplied()` is intentionally not pause-gated so consumers can still read the global watermark when reads are paused. To resolve a peaqID, compose: `readAttribute(didAccount, "peaqID")`. The orchestrator does not ship a dedicated `peaqIDOf` view; clients decode the 32-byte value themselves. ### Consumer view errors * `AttributeNotFound(address didAccount, bytes attrName)` * `AttributeRemoved(address didAccount, bytes attrName, uint64 removedAtHomeBlock)` * `LitePaused()` ## IdentityLite IdentityLite mirrors the peaq `IdentityRegistry` per-machineId record. Consumers gate writes that depend on having seen a specific peaq approval by reading `lastCursorPacked()`. ### Public consumer views ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function getIdentity(uint256 machineId) external view returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt); // pause-gated; lastBatchAcceptedAt == 0 is the cold-start sentinel function getIdentityRaw(uint256 machineId) external view returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt); // admin/debug only; ignores pause gate function lastCursor() external view returns (uint64 blockNumber, uint32 txIndex, uint32 logIndex); // unpacked; NOT pause-gated (cross-Lite read invariant) function lastCursorPacked() external view returns (uint128); // packed = (block << 64) | (txIndex << 32) | logIndex // NOT pause-gated function lastUpdatedHomeBlock(uint256 machineId) external view returns (uint64); // per-record cursor; pause-gated // Home-style accessors (additive to getIdentity) function getOwnerIfExists(uint256 machineId) external view returns (address owner_, bool exists_); function operatorOf(uint256 machineId) external view returns (address); function getMachineStatus(uint256 machineId) external view returns (MachineStatus); function getMachineURI(uint256 machineId) external view returns (string memory); struct IdentityRecord { address owner; // immutable post-Registered (soulbound on peaq) address operator; // mutable; address(0) clears string machineURI; // immutable post-Registered MachineStatus status; uint64 lastHomeBlock; } enum MachineStatus { None, Pending, Verified, Rejected, Deactivated } // Ordinals match peaq IdentityRegistry. Do not reorder. ``` ### Consumer view errors * `MachineNotFound(uint256 machineId)` * `LiteUninitialized(address lite)` — not raised by the Lite itself. Read `(record, lastAt)` and `require(lastAt > 0, LiteUninitialized(address(this)))`. * `LitePaused()` ### Cold-start pattern The Lite returns `(record, 0)` when it has never accepted a batch, rather than reverting, so callers choose the policy: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} (IdentityRecord memory rec, uint64 lastAt) = identityLite.getIdentity(machineId); require(lastAt > 0, LiteUninitialized(address(identityLite))); require(rec.status == MachineStatus.Verified, "not verified"); ``` ## StakingLite StakingLite mirrors the peaq `IdentityStaking` per-machine stake record. Consumers gate authorisation or service eligibility on stake state without crossing chains. ### Public consumer views ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function getStake(uint256 machineId) external view returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_); // pause-gated; lastBatchAcceptedAt_ == 0 is the cold-start sentinel function getStakeRaw(uint256 machineId) external view returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_); // admin/debug only; ignores pause gate function isStaked(uint256 machineId) external view returns (bool); // pause-gated function isAuthorized(address wallet) external view returns (bool isAuth, uint64 lastBatchAcceptedAt_); // pause-gated; cold-start sentinel applies function totalStaked() external view returns (uint256); // pause-gated; aggregate of applied Staked.amount events ``` Same cold-start pattern as IdentityLite — `require(lastBatchAcceptedAt_ > 0, LiteUninitialized(address(this)))` before trusting reads. StakingLite has its own pause flag and its own EIP-712 schema (`StakingEvent`), but shares the `PeaqosLite` domain and the cross-language daemon parity gate. ## EIP-712 schemas ### 7-field IdentityEvent ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} IdentityEvent(uint8 kind,uint256 machineId,address subject,string machineURI,uint64 homeBlockNumber,uint32 txIndex,uint32 logIndex) ``` `txIndex` and `logIndex` were added vs the earlier 5-field shape so the on-chain cursor can be sub-block-precise. The daemon-side encoder is bit-identical to Solidity (parity-gated by the EIP-712 fixture suite). ### DIDEvent ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} DIDEvent(uint8 kind,address didAccount,bytes attrName,bytes attrValue,uint32 validity,uint64 homeBlockNumber) ``` Both `bytes` fields are pre-hashed with `keccak256(bytes(...))` per EIP-712 dynamic-bytes rule. `kind` ordinal: `0 = Add, 1 = Update, 2 = Remove`. Removes must carry `value.length == 0` and `validity == 0`. ### Batch envelope (shared) ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Batch(uint64 nonce,uint64 deadline,uint8 schemaVersion,bytes32 eventsRoot,uint64 cursorLo_block,uint32 cursorLo_txIndex,uint32 cursorLo_logIndex,uint64 cursorHi_block,uint32 cursorHi_txIndex,uint32 cursorHi_logIndex) ``` Schema version is per-Lite. A typehash bump requires lockstep upgrade of both the Lite and the daemon. ## Signer Daemon Off-chain. Python package, one instance per `(home, satellite, Lite)` triple. V1 shipped 2026-05-21 with a **six-daemon fleet** across two pipelines: three for peaq mainnet (home) → Agung (satellite) and three for Agung (home) → Base Sepolia (satellite). Each daemon binds to one `LITE_NAME` (`IdentityLite | DIDLite | StakingLite`), one push key (`currentSigner` on the Lite), one `HEALTH_PORT`, and one `CURSOR_FILE_PATH`. Six unique push keys total — reuse triggers nonce races. A direct peaq mainnet → Base Sepolia pipeline is registered in the SDK satellite registry but not yet rolled out as a daemon deployment. | Property | Behaviour | | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Finality | GRANDPA-finalized only. No `latest-N` fallback. Daemon freezes if GRANDPA stalls rather than serve unfinalized state. | | Batching | Critical events flush immediately; non-critical aggregate up to `flush_after_sec=30` or `max_batch_size=50`. `MAX_BATCH_SIZE` env override bounded `[1, 50]`; required `=10` for DIDLite (byte-payload events exceed Agung block gas at 50). | | Nonce ordering | Per-chain EVM nonce lock + per-Lite monotonic batch nonce. `AlreadyApplied` → skip-advance. `NonceOutOfOrder` → backfill missing nonce(s). | | Restart catchup | On boot, `resume_cursor = max(localCursor, liteCursor)`. Prevents replay after a crash between Lite acceptance and local commit. | | Pause retries | `WritesPaused` from `applyBatch` triggers exponential backoff `(1, 5, 15, 60, 300)s` rather than fatal exit, so admin pauses do not crash the fleet. | | Poison events (DIDLite) | Orphan `Update`/`Remove` events whose `Add` predates the deploy block trigger `AttributeDoesNotExist` or `AttributeAlreadySoftDeleted`. The daemon decodes the revert, isolates the offending event, pushes the rest of the batch, and increments `poison_event_skipped_per_lite[lite]` on `/health`. Under partial-history replay the satellite is best-effort cache, not authoritative. | | Cross-language parity | EIP-712 typehashes are constants in Solidity (`EVENT_TYPEHASH_V1`, `BATCH_TYPEHASH_V1`) and in Python. A Hardhat task dumps canonical fixtures; Python recomputes identical hashes (17 fixtures, all 65-byte signatures bit-identical). | | Hot-wallet floor | `HOT_WALLET_MIN_BALANCE_WEI` (default `0.02` native). Below the floor the daemon stops signing and `/health.hot_wallet_low[chain_id] = true`. | ### Health endpoint Each daemon exposes a loopback-only `/health` (default port unique per instance, conventionally 8080–8085): ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { signer_key_id, last_push_at, hot_wallet_low: { [chain_id]: bool }, queue_depth, poison_event_skipped_per_lite: { [lite_name]: int } } ``` Operator runbooks (env vars, deploy steps, monitoring, troubleshooting) for the Signer Daemon will live in the Operate section: `signer-daemon-deploy`, `signer-daemon-monitoring`, and `signer-daemon-troubleshooting` (coming soon). ## Pause and emergency model Every Lite has two independent pause flags and one emergency flag, all `external onlyOwner`: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pauseLite(string reason) // pause user-facing reads unpauseLite() // resume reads (blocked while in emergency) pauseApplyBatch(string reason) // pause inbound writes unpauseApplyBatch() // resume writes (allowed even in emergency) setSigner(address newSigner) // routine PUSH_KEY rotation emergencyRotatePushKey(address) // incident-response rotation exitEmergencyMode() // requires currentSigner != snapshot adminReplayEvents(...) // owner-driven replay/rewind ``` Routine `setSigner` rotation opens a `GRACE_BLOCKS = 600` (\~1h) window where the previous PUSH\_KEY remains valid so in-flight signed batches do not fail mid-flight. Emergency rotation does not keep the previous key valid. ### EmergencyMode EmergencyMode is two booleans plus a snapshot address, not an enum: ```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}} bool inEmergencyMode; address emergencyEnteredBySigner; // snapshot at first incident pause bool litePaused; // read pause bool applyBatchPaused; // write pause ``` A first `pauseLite` or `pauseApplyBatch` sets `inEmergencyMode = true` and snapshots `emergencyEnteredBySigner = currentSigner`. To exit, the owner calls `emergencyRotatePushKey(newKey)` (rotation must actually change the signer) then `exitEmergencyMode()`. Pause flags are not auto-cleared. ### Hot/cold key collapse defense The cold key is `owner()` (admin and upgrade authority). The hot key is `currentSigner` (the PUSH\_KEY on the daemon server). A single transaction must never collapse them onto the same address. Enforcement points: * `initialize(owner_, pushKey_)` reverts if `pushKey_ == owner_`. * `setSigner(newSigner)` reverts if `newSigner ∈ {owner(), pendingOwner()}`. * `emergencyRotatePushKey(newPushKey)` reverts on the same membership check. * `transferOwnership` / `_transferOwnership` reject `newOwner ∈ {currentSigner, previousSigner}`. * `renounceOwnership` is permanently disabled. A stolen daemon key cannot also seize upgrade authority. ## Staleness policy The Lite does **not** staleness-revert. SDK and ops layers apply staleness gates. Consumers should compare `lastBatchAcceptedAt` to `block.timestamp` and reject reads older than their own SLO. The Lite only blocks cold-start via the consumer-side sentinel pattern. ## Soft delete (DIDLite) Removed DID attributes stay in storage with `removed = true` and `value = ""`. `readAttribute` reverts `AttributeRemoved(...)` for these. `readAttributeRaw` returns them as-is for admin/debug. ## What an integrator does 1. **Resolve a peaqID on a satellite chain.** Call `readAttribute(didAccount, "peaqID")` and decode the returned `bytes` as a `bytes32`. Always pair with `require(lastBatchAcceptedAtTs > 0)` to handle cold start. 2. **Read identity status.** `getIdentity(machineId)` returns the full record. Same cold-start check. Or use the granular home-style getters: `getOwnerIfExists`, `operatorOf`, `getMachineStatus`, `getMachineURI`. 3. **Read stake state.** `StakingLite.getStake(machineId)`, `isStaked(machineId)`, `isAuthorized(wallet)`, or `totalStaked()`. 4. **Gate on a specific peaq approval.** Read `IdentityLite.lastCursorPacked() >= myExpectedCursor` before letting a satellite action depend on it. Use the unpaused `lastCursor()` view if you need the unpacked form. 5. **Apply your own staleness SLO.** Compare `lastBatchAcceptedAt` to `block.timestamp`. The Lite intentionally does not enforce one. ## Solana Solana is the first non-EVM satellite. Four Anchor programs for the Tokenomics 1.0 mirrors are deployed on Solana mainnet-beta (and devnet): a signer daemon pushes finalized peaq mainnet state into three of them, and the fourth is a trustless address-binding registry. Since 2026-09-09 the Economics 2.0 programs are deployed as well, nine since 2026-09-11 (addresses on [Smart contracts](/peaqos/concepts/contracts#economics-2-0-solana-mainnet)). What is reachable today: | Capability | Status on 2026-09-09 | How to use it | | :------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | | Resolve a Solana address to a peaq machine and its MCR | **Live, public API** | [`GET /solana/mcr/{solana_addr}`](/peaqos/api-reference/get-solana-mcr) and `GET /solana/operator/{solana_addr}/machines` on `mcr.peaq.xyz` | | Bind a Solana wallet to a peaq address | **Live on chain**, permissionless dual-signed `bind` / `revoke` on the binding program. No SDK or CLI surface yet; peaq runs the binding for partner fleets | Program `GrsCoPkeLUAoXjuwPfCcuch77Chco1Gx95zCCQNYGqG6` | | Read peaq machine identity (owner, operator, status) on Solana | **Live**, mirror pushed from peaq mainnet in batches, not on a fixed schedule (the last push before this snapshot was on 2026-09-05), so the Solana copy can lag peaq by hours or days. Read-only; no SDK package published yet, so consumers derive the PDA themselves | Program `AWGibJPvQnj8mhfK8QWdUhvJBySCcwYadqAdtj2N137m`, PDA `["identity", keccak256(machineId)]` | | Read peaq machine stake on Solana | **Live**, same batches as the identity mirror | Program `4PyqZKtej7CWcV1jZ5h5FMd8EoR5edTTaZxAxEQmVDvs`, PDA `["stake", keccak256(machineId)]` | | Read peaq DID attributes on Solana | **Deployed**, stores the keccak hash of each attribute value (not the value). The stream has not received new batches since mid-August 2026; do not rely on it for freshness | Program `AMQgyqNcWSJy6cK2wSmXRykgjoPhSroKkjKtN8Y11WMc` | | Bridge a Machine NFT peaq → Solana and back | **Live on mainnet since 2026-08-21** (LayerZero V2, Metaplex Core twin). Operated with peaq's tooling; the SDKs accept only `"base"` as a destination today | [Machine NFT](/peaqos/concepts/machine-nft#cross-chain-portability) | | Pay for streams and market orders in SOL or SPL tokens | **Live** since SDK 0.4.0 / CLI 0.0.6 | [`peaqos stream pay --chain solana`](/peaqos/cli#solana-payments) | | Onboard a machine that holds only a Solana wallet | **Not available yet.** Under development; no public endpoint | | | Economics 2.0 on Solana | **Programs live on mainnet since 2026-09-09, the full set since 2026-09-11, nothing for users yet.** Nine Anchor programs (InfoDesk, MachineBridgeAdapter, MachineRegistry, CoordinationFeeCollector, MachineStateAndSync, EventRegistry, PriceOracle, TreasuryPool, TrustValidatorStaking) are deployed and peaq's `MachineBridgeAdapter` has the Solana adapter registered as its LayerZero peer; the bridging and relocation flags are off on both chains. Activating a machine on Solana is not possible yet: bonding PEAQ for a Solana machine is in development, and the SDKs and CLI have no Economics 2.0 Solana surface. Subscription and staking authority stay on peaq | | The Solana mirrors are pushed by peaq's signer daemon with a secp256k1 push key, not by LayerZero: reading a mirror record means trusting that key holder pushed faithfully. They are a queryable replica of peaq, never a second source of truth. The binding registry is the one trustless piece: anyone can bind, and the record is only ever written by the two keys it links. Mirror records are keyed by `keccak256(machineId)` under each program's ID; check the program's global account for `paused` and `last_finalized_home_block` before trusting a read. Identity and staking mirrors carry Tokenomics 1.0 machine IDs today. There is no `peaqos solana` command group. ## Addresses Per-chain proxy addresses are pasted into operator `.env` files generated from templates in `signer-daemon/deploy/` and surfaced through the SDK satellite registry. There is no single deploy-manifest JSON in the repo. Track the launch announcement for the canonical Agung and Base Sepolia address list, or pull them from `peaqos/concepts/contracts` once published. ## Related * Satellite mirrors — propagation and cursor-independence model (coming soon) * Signer daemon — runtime surface (coming soon) * Satellite SDK overview — JS/Python bindings (coming soon) * Lite views (Solidity) — Solidity consumer surface (coming soon) * Operate → Signer daemon deploy — production runbooks (coming soon) * [Scale function](/peaqos/functions/scale) * [Activate function](/peaqos/functions/activate) * [Qualify function](/peaqos/functions/qualify) * [Machine NFT concept](/peaqos/concepts/machine-nft) * [peaqID concept](/peaqos/concepts/peaqid) * [Roadmap](/roadmap) # peaqID Source: https://docs.peaq.xyz/peaqos/concepts/peaqid W3C DID for machines, portable across every chain peaqOS supports. peaqID is a W3C DID that identifies a machine across every chain it transacts on. ## DID format There are two peaqID formats, one per generation of machine. ### Economics 2.0 machines (activated since 2026-09-01) ``` did:peaq: ``` The machine ID is `uint256(keccak256(abi.encode(machineType, credentialSubject)))`, the same value as the machine's ERC-721 token ID in `MachineRegistry`. The DID document (controller, verification methods, authentication indices, service endpoints) is stored in `MachineRegistry` itself at activation and updated with the [DID setters](/peaqos/sdk-reference/sdk-python#did-updates); `id` is computed on read, never supplied. Documentation and data API URLs live in `serviceEndpoints`. There is no mapping from an address-form DID to a 2.0 machine ID. See [Economics 2.0](/peaqos/concepts/economics-2-0#machine-id-and-did). ### Tokenomics 1.0 machines (onboarded before 2026-09-01) ``` did:peaq:<0x-address> ``` The address is the machine's EOA (externally owned account) on peaq chain. The DID resolves to a flat key-value attribute store on the peaq DID precompile (`0x0000000000000000000000000000000000000800`). Any consumer can start from the DID and traverse to the machine's identity, financial history, and credit rating through the 1.0 MCR API. The rest of this page describes this format. ## Per-machine vs per-proxy peaqOS assigns one DID per machine and one DID per proxy operator. The two serve different roles: | DID type | Registered by | Attributes | Purpose | | :---------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | | Machine DID | `registerMachine` (self-managed) or `registerFor` (proxy delegation) | `machineId`, `nftTokenId`, `operator`, `documentation_url`, `data_api`, `data_visibility` | Identifies a single machine, links to its Machine NFT and event history | | Proxy DID | `registerMachine`. The proxy itself is a registered machine. | `machineId`, `machines` | Identifies an operator, lists the machine IDs of all machines it manages | A machine that self-manages has no `operator` attribute. A proxy operator's `machines` attribute is a JSON array of machine IDs (e.g., `[123, 456, 789]`). **DID writes always go to the caller's DID.** The DID precompile keys attributes by `msg.sender`. A proxy that calls `registerFor` mints the machine's identity NFT and pays the bond, but it cannot write attributes to the machine's DID; the machine must sign its own `writeMachineDIDAttributes` call. Skipping this leaves the machine unreachable through the MCR API. ## DID attribute table After registration, the operator or machine must explicitly call `writeMachineDIDAttributes` (or `writeProxyDIDAttributes`) to write these attributes to the DID. Registration itself only mints the identity and creates the on-chain machine ID; the DID attribute writes are a separate transaction. | Attribute | Type | Description | | :------------------ | :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `machineId` | `uint256` | On-chain machine ID assigned by IdentityRegistry. Primary key for all queries. | | `nftTokenId` | `uint256` | Token ID of the machine's Machine NFT. This is a separate ERC-721 token space and is not equal to `machineId`. | | `operator` | `did:peaq:0x...` | Proxy operator's DID. Absent if the machine self-manages. | | `documentation_url` | URL string | Link to machine documentation maintained by the project. | | `data_api` | URL string | Raw data API endpoint. The MCR API reads this when `data_visibility` is `public`. | | `data_visibility` | `public` / `private` / `onchain` | Controls how the MCR API exposes raw event data. Unset or empty defaults to `private`. | | `machines` | JSON array string | Proxy operator DID only. List of managed machine IDs. The DID precompile stores the full JSON; the MCR API truncates to the first 100 valid IDs when reading the `machines` attribute. | ### Byte limits Both SDKs enforce these constraints before any DID write reaches the chain: | Constant | Value | Applies to | | :-------------------- | :---- | :-------------- | | `DID_MAX_NAME_BYTES` | 64 | Attribute name | | `DID_MAX_VALUE_BYTES` | 2560 | Attribute value | **Migration.** peaq chain has approximately 3.5 million existing peaqID holders. Existing holders retain their peaqIDs. A migration path to the current DID format is on the [roadmap](/roadmap); the new format is what onboards from peaqOS today. ## Resolving a peaqID ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); // Fetch machine profile by DID const response = await fetch( `${client.apiUrl}/machine/did:peaq:0xMachineAddress` ); const machine = await response.json(); console.log(machine.peaqos.did); // "did:peaq:0x..." console.log(machine.peaqos.machine_id); // 123 console.log(machine.peaqos.mcr); // "BBB" ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv import requests from peaq_os_sdk import PeaqosClient load_dotenv() # load envs from .env file client = PeaqosClient.from_env() # Fetch machine profile by DID response = requests.get( f"{client.api_url}/machine/did:peaq:0xMachineAddress" ) machine = response.json() print(machine["peaqos"]["did"]) # "did:peaq:0x..." print(machine["peaqos"]["machine_id"]) # 123 print(machine["peaqos"]["mcr"]) # "BBB" ``` ## Data visibility modes The `data_visibility` attribute controls how the MCR API handles raw event data for this machine: | Mode | MCR API behavior | Raw data location | | :-------- | :-------------------------------------------------------------------------------------------------------------- | :------------------------ | | `public` | Fetches from `data_api`, includes in response | Project's API | | `private` | Returns the `data_api` URL only; consumer fetches directly | Project's API | | `onchain` | Parses JSON metadata from EventRegistry events into `event_data[]`, capped at the first 100 events per response | Onchain (higher gas cost) | `private` is the default when `data_visibility` is unset or empty. ## Cross-links * [Activate function](/peaqos/functions/activate) registers machines and writes DID attributes * [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile, including DID-sourced metadata * [Machine NFT](/peaqos/concepts/machine-nft) is linked to the peaqID via the `nftTokenId` attribute # Trust levels Source: https://docs.peaq.xyz/peaqos/concepts/trust-levels Three tiers classifying how trustworthy a submitted event is. Trust level classifies how trustworthy the data in a submitted event is. Higher trust levels indicate stronger guarantees about the event's authenticity. ## The three levels The machine or operator attests to the event. No external verification: the submitter's word is the source of truth. The event is backed by an on-chain reference (transaction hash, receipt, or cross-chain proof) that anyone can independently verify. The event is signed by attested hardware (a secure element on the device) that binds the event to a specific physical device. Choose the highest level you can honestly attest to. Trust levels are a field on each `EventRegistry` event and are self-declared today. Third-party attestation of the machine itself arrives with [Verify](/peaqos/functions/verify), which in its first release accepts one certified chip family (Infineon OPTIGA Trust M) rather than any TPM or enclave. ## Trust weight in the MCR Higher trust levels contribute more to the [MCR](/peaqos/concepts/machine-credit-rating) score. Hardware-signed events carry the strongest weight, on-chain verifiable events sit in the middle, and self-reported events the lowest. Self-reported machines with a sustained track record (a year or more of event history) earn a small graduation bonus on their trust contribution, though they remain below the on-chain tier. A machine flagged with a [negative event](/peaqos/api-reference/get-mcr) incurs a temporary penalty on its trust contribution for a fixed window from the flag timestamp, after which the penalty expires automatically. ## Admin overrides peaq operates two on-chain admin functions (`AdminFlags` contract) that adjust how the MCR treats a machine. Both are owner-only and emit events. | Override | Effect | | :----------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `setTrustOverride(machineId, value)` | Replaces the trust contribution for this machine within a permitted range. Reverts with `InvalidTrustOverride(value)` if the value is out of range. Use `clearTrustOverride` to remove. | | `flagMachine(machineId)` | Sets the negative-event flag at `block.timestamp`. The penalty applies for a fixed window from this timestamp. Use `clearFlag` to lift early. | Submitting an event with `trustLevel > 2` reverts on `EventRegistry.submitEvent` with `InvalidTrustLevel()`. # Activate Source: https://docs.peaq.xyz/peaqos/functions/activate Put your machine on-chain in one transaction. peaqID, Machine NFT, and a tier bond. Activate is the entry point to peaqOS. One transaction registers a machine on peaq chain, mints its [Machine NFT](/peaqos/concepts/machine-nft), stores its [peaqID](/peaqos/concepts/peaqid) document, and bonds it on a subscription tier under [Economics 2.0](/peaqos/concepts/economics-2-0). **Versions.** The one-transaction flow on this page needs `@peaqos/peaq-os-sdk` 0.7.0+, `peaq-os-sdk` 0.7.1+ (Python 3.10 or newer), or `peaq-os-cli` 0.0.8+ running on `peaq-os-sdk` 0.7.1+. Releases before 2026-09-11 carry the `MachineBridgeAdapter` address from before the 2026-09-08 re-point and fail preflight with `PEER_MISMATCH`. The earlier register-then-mint flow (`registerMachine`, `mintNft`, `writeMachineDIDAttributes`) still works in the SDKs when you do not select a Tokenomics 2.0 deployment, and is documented in the [legacy section](#legacy-flow-tokenomics-1-0) below. The CLI has no legacy path since 0.0.8. ## What ships | Component | Description | | :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | peaqID | W3C DID stored on-chain as a DID document (verification methods, authentication, service endpoints). Its id is `did:peaq:`. | | Machine NFT | One ERC-721 in `MachineRegistry`. The machine ID **is** the token ID. No separate Identity NFT and no separate mint call. | | Subscription bond | PEAQ bonded on the tier you choose (Entry, Basic, or Pro). Quoted per tier at the oracle rate when you activate. Not withdrawable. See [Economics 2.0](/peaqos/concepts/economics-2-0). | | Home chain record | `CrossChainMirror` records peaq as the machine's home chain. | | Machine wallet | Per-machine EVM keypair, generated by the SDK or the CLI. | ## Machine ID The machine ID is derived, not sequential: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} machineId = uint256(keccak256(abi.encode(machineType, credentialSubject))) ``` Only `machineType` and the exact `credentialSubject` bytes contribute. Neither can change after activation, and the same pair can never be activated twice. IDs are full-width `uint256` values (routinely 77 digits): `bigint` in JavaScript, `int` in Python, and a decimal **string** in JSON, URLs, and CLI output. The 2.0 DID form is `did:peaq:`, not an address. ## How activation works Select a deployment (`peaq-mainnet` or `agung-2026-08-28`), preview the quote, then activate. Contract addresses travel with the deployment record inside the SDK; you never set them. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # .env: TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet (peaqos init writes it) peaqos activate \ --machine-type Sensor \ --credential-subject-hex 0xdeadbeef \ --manufacturer 0x3333333333333333333333333333333333333333 \ --tier entry \ --did-document ./did.json \ --dry-run # preview the bond and the net PEAQ, submit nothing # Same command without --dry-run submits the one transaction. ``` Add `--payment usdt --slippage-bps 50` to settle the PEAQ-quoted bond in USDT. `--for 0xMachine --machine-key ./machine.key` switches to machine-owned, operator-controlled mode (see [Two ownership modes](#two-ownership-modes)). Full flag table on [CLI: activate](/peaqos/cli#peaqos-activate). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = new PeaqosClient<"tokenomics20">({ rpcUrl: process.env.PEAQOS_RPC_URL!, privateKey: process.env.PEAQOS_PRIVATE_KEY!, contracts: PeaqosClient.fromEnv().contracts, // the six legacy addresses are still required by the constructor tokenomics20: { deploymentId: "peaq-mainnet" }, // or "agung-2026-08-28" }); const params = { controller: client.address, verificationMethods: [ { id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: client.address, publicKeyMultibase: "z6Mk...", }, ], authentication: [0n], serviceEndpoints: [ { id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" }, { id: "#data", serviceType: "DataApi", serviceEndpoint: "https://example.com/events" }, ], machineType: "Sensor", credentialSubject: "0xdeadbeef", manufacturer: "0x3333333333333333333333333333333333333333", tier: 0, // 0 Entry, 1 Basic, 2 Pro }; const preview = await client.previewMachineActivation(params); console.log(preview.machineId, preview.bondAmount, preview.voucherCredit, preview.netPeaqAmount); const result = await client.activateMachine(params); console.log(result.machineId); // bigint, the permanent machine ID console.log(result.netPeaqAmount); // PEAQ actually transferred console.log(result.isHomedLocally); // true, confirmed on-chain ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_os_sdk import ( ActivateMachineParams, PeaqosClient, ServiceEndpointInput, Tokenomics20Config, VerificationMethodInput, ) load_dotenv() legacy = PeaqosClient.from_env() # since 0.7.1 from_env() also reads TOKENOMICS_DEPLOYMENT_ID; the explicit constructor below is equivalent client = PeaqosClient( rpc_url=legacy.rpc_url, private_key=os.environ["PEAQOS_PRIVATE_KEY"], identity_registry=legacy.contracts.identity_registry, identity_staking=legacy.contracts.identity_staking, event_registry=legacy.contracts.event_registry, machine_nft=legacy.contracts.machine_nft, did_registry=legacy.contracts.did_registry, batch_precompile=legacy.contracts.batch_precompile, tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), # or "agung-2026-08-28" ) params = ActivateMachineParams( controller=client.address, verification_methods=( VerificationMethodInput( id="#key-1", method_type="Ed25519VerificationKey2020", controller=client.address, public_key_multibase="z6Mk...", ), ), authentication=(0,), service_endpoints=( ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"), ServiceEndpointInput(id="#data", service_type="DataApi", service_endpoint="https://example.com/events"), ), machine_type="Sensor", credential_subject=bytes.fromhex("deadbeef"), manufacturer="0x3333333333333333333333333333333333333333", tier=0, # 0 Entry, 1 Basic, 2 Pro ) preview = client.preview_machine_activation(params) print(preview.machine_id, preview.bond_amount, preview.voucher_credit, preview.net_peaq_amount) result = client.activate_machine(params) print(result.machine_id) # int, the permanent machine ID print(result.net_peaq_amount) # PEAQ actually transferred print(result.is_homed_locally) # True, confirmed on-chain ``` ### What the call does 1. Validates every input locally. 2. Verifies the connected chain, that all seven Tokenomics 2.0 contracts have bytecode, that the peer addresses match `InfoDesk.peer(role)`, and that `MachineSubscription.fullMode()` is true. 3. Computes the machine ID and quotes the bond: `bond = requiredPeaqAmount(tier)`, `voucher = min(pendingVoucherCredit, bond)`, `net = bond - voucher`. 4. Resolves the PEAQ token from `InfoDesk.peaqToken()`, checks balance and allowance, and approves exactly `net` to `MachineSubscription` if the allowance is short. 5. Re-reads the quote, simulates, and submits the one `MachineStateAndSync.activateMachine` transaction. 6. Requires three correlated receipt events (`MachineOnboarded`, `MachineMinted`, `Activated`) and reconciles the resulting state (`ownerOf`, `controllerOf`, subscription tier and period, `isHomedLocally`) before returning. The transaction sender becomes the machine's owner **and** bond payer. `manufacturer` is recorded on-chain and never verified by the contract. On peaq, PEAQ is the native-balance precompile at `0x…0809`, so bond and gas come out of the same balance: a wallet holding exactly the net amount still fails on gas. A transaction whose receipt does not arrive in time is reported as pending (`PENDING_TRANSACTION` in the Python SDK, `RECEIPT_UNAVAILABLE` in the JS SDK, `PENDING` at exit 2 in the CLI) with its hash. It may still mine. Never submit a second activation for the same machine; reconcile the recorded hash instead (`reconcile_activation_transaction` in Python, re-running the same command in the CLI). ## Two ownership modes | Mode | Who signs and pays | Controller | Use when | | :--------------------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------- | :----------------------------------------------- | | Self-owned | Your configured signer owns the machine, pays the bond, and is its DID controller. | Same address as the owner | One machine, one wallet. | | Machine-owned, operator-controlled | The **machine** wallet signs, owns the NFT, and pays gas and the bond. | Your operator address, recorded in the DID document | Fleets where the operator manages many machines. | Rights differ by role and both parties should know them: | Who | Can | Cannot | | :--------------------- | :-------------------------------------------------------------- | :----------------------------------------------- | | Machine wallet (owner) | Transfer the NFT; rotate or clear the controller | | | Operator (controller) | Lifecycle (suspend, resume), subscription renewals, DID updates | Transfer the NFT; rotate or clear the controller | **Operator-sponsored onboarding has no 2.0 equivalent.** `MachineStateAndSync.activateMachine` makes `msg.sender` the owner and the payer, so there is no way for an operator to activate a machine it does not own. `registerFor` / `register_for` throw `SPONSORED_ACTIVATION_UNSUPPORTED` in Tokenomics mode. The supported fleet pattern is the machine-owned, operator-controlled mode above. Full guide: [Fleet onboarding](/peaqos/guides/proxy-operator-fleet). ## Confirm activation ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} state = client.get_machine_activation_state(machine_id) activated = state.subscription.period_start != 0 # tier 0 is a valid tier, so never test the tier ``` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos machine status --json ``` Or open the machine on the [Machine Explorer](https://machines.peaq.xyz): `https://machines.peaq.xyz/machine/` for a machine activated under Economics 2.0, `https://machines.peaq.xyz/machine/0x
` for a Tokenomics 1.0 or mirrored legacy machine. The explorer indexes from chain, so allow a few minutes after the transaction. Do not test activation by querying `mcr.peaq.xyz/machine/did:peaq:0x…`: 2.0 machines are addressed by decimal ID, and the MCR queries in the SDK are still disabled in Tokenomics mode. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines). ## After activation | Next step | Function | Status | | :----------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | | Manage the machine: suspend, resume, renew, transfer, DID updates | [CLI: machine](/peaqos/cli#peaqos-machine), [SDK](/peaqos/sdk-reference/sdk-python#tokenomics-2-0) | Live | | Build a credit rating from revenue and activity events | [Qualify](/peaqos/functions/qualify) | Live for 1.0 machines; 2.0 ratings served by `mcr-20.peaq.xyz` over HTTP since 2026-09-05, 2.0 event submission rolling out | | Pair an AI agent with delegated spending and run market search | [Scale](/peaqos/functions/scale) | Live | | Provision the machine as a compute provider and earn to its wallet | [Monetize](/peaqos/functions/monetize) | Live; 2.0 opt-in reaches `mcr-20.peaq.xyz` since 2026-09-05 | | Get attested by the peaq Foundation or an OEM | [Verify](/peaqos/functions/verify) | Coming Soon | | Fractionalize the machine for investor ownership | [Tokenize](/peaqos/functions/tokenize) | Coming Soon | ## Legacy flow (Tokenomics 1.0) Machines onboarded before Economics 2.0 live in `IdentityRegistry` with a 1 PEAQ native bond and a separate `MachineNFT` token. The SDKs keep this path when you construct a client **without** `tokenomics20`: `registerMachine` / `register_machine` (self-managed) or `registerFor` / `register_for` (proxy), then `mintNft`, `tokenIdOf`, and `writeMachineDIDAttributes`. Each of these emits a deprecation warning and throws a typed error in Tokenomics mode. The 1.0 MCR API at `mcr.peaq.xyz` serves these machines by `did:peaq:0x
`. Legacy machines can be mirrored into Economics 2.0 through the `MachineMigrationHub`; that path is operated by peaq for partner fleets, not exposed in the SDKs. See [Economics 2.0: legacy machines](/peaqos/concepts/economics-2-0#legacy-machines). ## Concepts Tiers, bonds, credits, grace and runoff, trust validators. W3C DID, DID document, portable identity. The ERC-721 whose token ID is the machine ID. Addresses on peaq mainnet and agung. 2FA-gated faucet for initial machine gas. Rating from a machine's history. ## SDK reference * [`activateMachine` / `activate_machine`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): the one-transaction activation. * [`previewMachineActivation` / `preview_machine_activation`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): quote without signing. * [`getMachineActivationState` / `get_machine_activation_state`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): confirm the result. * [`fundFromGasStation` / `fund_from_gas_station`](/peaqos/sdk-reference/sdk-js#fundfromgasstation): request gas for a fresh wallet. * [`generateKeypair` / `generate_keypair`](/peaqos/sdk-reference/sdk-js#generatekeypair): create a machine keypair. ## Guides Single machine, owner-operated. Machine-owned, operator-controlled: N machines from one operator. # Monetize Source: https://docs.peaq.xyz/peaqos/functions/monetize Put your machine to work: provision it as a compute provider and earn to its machine wallet. Monetize is how a machine earns from its own spare capacity. A [registered, bonded](/peaqos/functions/activate) machine opts in, provisions itself as a compute provider, and reports that it is online, so compute networks can put it to work and pay it. Earnings land on the machine wallet, not the operator's. Where [Scale](/peaqos/functions/scale) lets a machine *buy* services and [Stream](/peaqos/functions/stream) lets it sell *data*, Monetize lets it sell *capacity*: the machine's own processing power. Compute is the first supported capacity type; v1 ships with a worked flow for onboarding a machine to aggregator networks such as Akash. **Status for Economics 2.0 machines (2026-09-05).** SDK 0.6.0 and CLI 0.0.8 moved the opt-in client to the 2.0 MCR server at `mcr-20.peaq.xyz`, which publishes the compatibility signal since 2026-09-05. Opt-in reads work against the mirrored 2.0 machines; writes were not exercised yet. Provisioning and the heartbeat are unchanged and work. For Tokenomics 1.0 machines, pin `@peaqos/peaq-os-sdk@0.5.0`, `peaq-os-sdk==0.5.0` (Python 3.11 or newer), or `peaq-os-cli<0.0.8` together with `peaq-os-sdk<0.6.0`, which talk to `mcr.peaq.xyz`. Details on the [opt-in reference](/peaqos/sdk-reference/monetization-opt-in). ## What ships | Component | Description | | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monetization opt-in | A separate, toggleable decision on top of activation. The machine's owner or DID controller (under Economics 2.0; the owner, machine wallet, or on-chain operator for Tokenomics 1.0 machines) signs an EIP-191 message and submits it to the MCR API. Opting in requires the machine to be activated, bonded, and not deactivated; opting out is always allowed. Anyone can read a machine's current state. Both SDKs wrap this as an [opt-in client](/peaqos/sdk-reference/monetization-opt-in), and the CLI exposes it as [`peaqos monetize`](/peaqos/cli#peaqos-monetize). | | Provider provisioning | A schema-driven runner in both SDKs that turns a published provisioning manifest into an ordered, auditable install, entirely on the machine. Pre-flight checks gate the run, secrets are redacted end to end, sudo is explicit and scoped, and the node counts as live only when the manifest's verification probes pass. The CLI drives the whole flow with [`peaqos monetize provision`](/peaqos/cli#peaqos-monetize-provision). | | Presence heartbeat | A machine-side client that signs and pushes a presence heartbeat to the peaqOS heartbeat service at a configurable interval. A valid heartbeat keeps the machine online; when heartbeats stop, the server marks it offline. The machine never self-reports offline. | | Machine-wallet payout | The provisioning runner exposes the machine's own wallet as the default commission and payout address. Money earned by the machine routes to the machine, keeping its revenue history clean for its [credit rating](/peaqos/functions/qualify). | ## How it works ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} flowchart LR Reg["Activated + bonded
machine (Activate)"] --> OptIn["Monetization opt-in
(signed, toggleable)"] OptIn --> Prov["Manifest provisioning
(runs on the machine)"] Prov --> HB["Presence heartbeat
(to the peaqOS heartbeat service)"] Prov --> Agg["Aggregator network
(e.g. Akash)"] Agg -->|workloads| Prov Agg -->|payment| Wallet["Machine wallet"] ``` 1. **Opt in.** Monetization is a distinct decision, not part of activation. The owner or controller (owner, machine wallet, or operator for 1.0 machines) opts in through the [SDK](/peaqos/sdk-reference/monetization-opt-in), the [CLI](/peaqos/cli#peaqos-monetize) (`peaqos monetize opt-in`), or the raw [monetization API](/peaqos/api-reference/put-machine-monetization). The machine must be bonded first; opting out works at any time. 2. **Provision.** The machine pulls a versioned provisioning manifest from a public repo and runs it locally through the SDK's [provisioning runner](/peaqos/sdk-reference/provisioning) or the CLI's `peaqos monetize provision run`: pre-flight checks, ordered install steps in manual or auto mode, owner handoffs for things only a human can do (funding, DNS), and verification probes that prove the provider node is actually live. 3. **Report presence.** The machine starts the [heartbeat client](/peaqos/sdk-reference/heartbeat) and pushes a signed heartbeat at a regular interval. Presence is server-derived: online while heartbeats keep arriving, offline when they stop. 4. **Earn.** A provider node that passes verification is live on its aggregator network, which dispatches workloads and pays through its own rails. peaqOS supplies the machine wallet as the payout context, never the operator's, and the manifest maps it into the provider's commission field. ## v1 scope Compute is the only supported capacity type in v1, and Akash is the first supported aggregator network. The machine-side flow (opt-in, provisioning, presence heartbeat) is what ships now. Aggregator-side discovery, connection management, per-job proofs, and automated settlement through peaqOS rails are rolling out next; see the [roadmap](/roadmap). Treat v1 as an early release: it proves the machine-side flow end to end, and is not yet hardened for production scale. ## Build with Monetize Toggle monetization on or off and read the state, from JavaScript and Python. The manifest runner for JavaScript and Python: fetch, pre-flight, provision, verify. The presence client for JavaScript and Python: start, stop, and query presence. Opt in, provision, and verify a provider node from the terminal. The wire contract: canonical message, freshness window, and error codes. Where Monetize sits and what ships next. ## Related * [Activate](/peaqos/functions/activate): the identity and bond Monetize builds on * [Qualify](/peaqos/functions/qualify): the credit rating machine earnings feed into * [Stream](/peaqos/functions/stream): sell the machine's data instead of its capacity * [Wallets (OWS)](/peaqos/wallets) # Qualify Source: https://docs.peaq.xyz/peaqos/functions/qualify Credit rate your machine from its revenue and activity history. Qualify turns a machine's on-chain history into a [Machine Credit Rating (MCR)](/peaqos/concepts/machine-credit-rating): a Moody's-style letter rating that any protocol, agent, or frontend can query from any chain. ## What ships | Component | Description | | :------------------- | :---------------------------------------------------------------------------------------------------- | | EventRegistry | On-chain store for revenue (type `0`) and activity (type `1`) events, with a cross-chain audit trail. | | MCR scoring pipeline | Computes AAA-to-NR ratings from a bonded machine's event history. | | MCR API | Public read API. Any chain, any caller, no auth. | | SDK helpers | `submitEvent`, `validateSubmitEventParams`, `computeDataHash`, `queryMcr` on both JS and Python. | ## How it works Each time a machine earns revenue or performs a trackable activity, the operator submits an event to the EventRegistry. The SDK validates the payload, computes a `keccak256` data hash, and writes the minimal on-chain record. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient, EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAIN_IDS } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); const { txHash, dataHash } = await client.submitEvent({ machineId, eventType: EVENT_TYPE_REVENUE, value: 500, // $5.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000) - 10, // Must be after block time rawData: new TextEncoder().encode(JSON.stringify({ session: "abc" })), trustLevel: TRUST_SELF_REPORTED, sourceChainId: SUPPORTED_CHAIN_IDS.peaq, sourceTxHash: null, metadata: new Uint8Array([]), }); ``` Full walkthrough: [Submit events](/peaqos/guides/submit-events). A freshly registered machine is **Provisioned** until it has enough history to score. Events feed the scoring pipeline, which blends revenue trend, activity cadence, bond status, and trust level. Any consumer (an agent, protocol, frontend, or another chain) can fetch the current rating from the public API. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl ${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc... ``` Response includes the rating (AAA / AA / A / BBB / BB / B / NR / Provisioned), score, event counts, revenue trend, and bond status. Full walkthrough: [Query MCR](/peaqos/guides/query-mcr). ## Cross-chain revenue Revenue earned on another chain (e.g., Base) is recorded on peaq with `sourceChainId` and `sourceTxHash` pointing to the origin transaction. Consumers can verify each event against its source chain. See [Events](/peaqos/concepts/events#cross-chain-revenue-accounting) and [Trust levels](/peaqos/concepts/trust-levels). **Multichain:** the MCR API stays canonical on peaq. Omni-chain V1 shipped `IdentityLite`, `DIDLite`, and `StakingLite` on supported chains (Agung and Base Sepolia at launch), so consumers can read machine identity and stake state locally. A dedicated MCR oracle on supported chains is reserved and follows V1. See the [Omni-chain concept](/peaqos/concepts/omni-chain) and the [roadmap](/roadmap). ## Concepts AAA-to-NR scale, lifecycle from Provisioned to rated. Revenue and activity records that feed MCR. Self-reported, on-chain verifiable, hardware-signed. ## SDK reference * [`submitEvent`](/peaqos/sdk-reference/sdk-js#submitevent): Write a single event to EventRegistry. * [`validateSubmitEventParams`](/peaqos/sdk-reference/sdk-js#validatesubmiteventparams): Validate event params client-side. * [`computeDataHash`](/peaqos/sdk-reference/sdk-js#computedatahash): keccak256 of raw event data. * [`queryMcr`](/peaqos/sdk-reference/sdk-js#querymcr): Fetch a machine's rating from the MCR API. ## API reference Rating, score, trend, bond status for a single machine. Paginated operator fleet with per-machine MCR. ## Guides Event types, validation, data hashing, cross-chain pattern. Fetch ratings from curl, JS, or Python. # Scale Source: https://docs.peaq.xyz/peaqos/functions/scale Delegate bounded authority to an AI agent so it can discover, buy, and consume services on your machine's behalf. Scale pairs an AI agent to an activated, bonded machine and gives it delegated authority over the machine's wallet, bounded by spend limits and an allow/denylist. The agent discovers, buys, and consumes services through the [Machine Markets API](/peaqos/api-reference/machine-markets-overview). The agent is provisioned outside peaq; pairing, policy, discovery, ordering, payment, and execution happen through peaqOS. It builds on the smart account each machine receives at [Activate](/peaqos/functions/activate) and the credit signal from [Qualify](/peaqos/functions/qualify). The machine's MCR and trust level carry through to providers when the agent buys. ## What ships | Component | Description | | :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Machine Agent pairing | Bind a third-party AI agent (Claude, OpenAI, Virtuals, Teneo, etc.) to a machine via a signed EIP-191 challenge. peaqOS verifies the signature, persists the agent's identity and verification metadata, and issues a signed HS256 session JWT (`pairingToken`) the agent presents as `x-agent-pairing-token` on market writes. Tokens rotate via a sessions endpoint. | | Delegation policy | Per-transaction limit, daily spend limit, currency, and allowed/denied skill keys and service IDs. Enforced server-side on every market call. Policy changes invalidate the session token's `delegationPolicyHash`; rotate after a `PATCH`. | | Skill registry and service catalogue | Curated capabilities (`oracle.price-feed`, `compute.marketplace`, `compute.confidential`, `storage.object`, `data.location`, `identity.proof-of-person`, `device.control`, `machine.commerce`, `network.partner-console`) and the concrete provider services that fulfil them. | | Market search | `POST /market/search` takes machine context, requirements, region, budget, and execution preferences and returns ranked `MarketQuote`s with reasons. | | Market orders | `POST /market/orders` locks a service from a quote into an order with state machine `created → payment_pending → ready → executing → delivered → confirmed` (or `disputed` / `cancelled` / `failed` / `handoff`). Server-side policy and spend checks fire on create. | | Payment intent and escrow | `payment-intent` selects a rail; `payment-proof` records or RPC-verifies on-chain payment; `payment/escrow-lock`, `payment/release`, `payment/refund` cover the escrow lifecycle. Solana proofs are recorded; EVM proofs verify via RPC against the ERC-20 Transfer log. | | Execute and runtime endpoints | `POST /orders/:orderId/execute` materialises the order as a task plus route, dispatches to a native provider or a machine-side runtime endpoint registered via `PUT /machines/:machineId/runtime-endpoints/:providerKey` or heartbeated by an on-machine runtime agent, and writes a `Run` and `Outcome`. | | Confirm and dispute | `POST /confirm` closes the order and releases escrow; `POST /dispute` opens a dispute record and freezes payment. Pairings with non-terminal orders cannot be revoked (`OPEN_MARKET_ORDERS`). | | Machine identity proof | DID-controller challenge and EIP-191 signature flow that ties orchestration writes to the on-chain machine identity. | ## How it works ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} flowchart LR Owner["Proxy Operator /
Machine Owner"] -->|provisions| Agent["Machine Agent
(third-party AI)"] Owner -->|pairs + sets policy| Orch["Machine Markets API
peaqOS Orchestration"] Agent -->|x-agent-pairing-token| Orch Orch -->|verifies identity| MCR["peaqOS MCR
(on-chain truth)"] Orch -->|ranked quotes| Agent Agent -->|order, pay, execute, confirm| Orch Orch -->|consume| Providers["Skill providers
(QVAC, agentic.market, pay.sh...)"] ``` 1. **Activate the machine.** Run [Activate](/peaqos/functions/activate) to mint peaqID, Machine NFT, smart account, and post the bond. Pairing requires an active, bonded machine. 2. **Prove identity ownership.** `POST /machine-identity/challenges` returns a message that the DID-controller key signs (EIP-191). The signed proof is attached when the machine is registered with the orchestration service. 3. **Provision a Machine Agent.** Get an agent from any provider you trust: Claude, OpenAI, Virtuals, Teneo, your own runtime. peaq does not provide the agent. 4. **Pair the agent.** Pairing is challenge-based. 1. `POST /machines/:machineId/agent-pairings/challenges` with the agent's `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`. The response is an `AgentPairingChallenge` with `challengeId`, `message`, and `expiresAt`. 2. The Machine Agent signs `message` (EIP-191 `personal_sign`) with the wallet key behind `agentAddress`. 3. `POST /machines/:machineId/agent-pairings` with `agentProof: { challengeId, signature }` and the delegation policy. The orchestrator verifies the signature, persists the pairing with `verification` metadata, and returns the `AgentPairing` with a signed session JWT in `pairingToken`. Store it client-side and send it as `x-agent-pairing-token` on market writes. 4. Tokens expire (default 1 hour). Rotate by issuing a fresh challenge and calling `POST /machines/:machineId/agent-pairings/:pairingId/sessions` with the new proof. Rotate after any `PATCH` to the delegation policy. 5. **Search the market.** The agent calls `POST /market/search` with machine context, service type, capabilities, region, budget, and optional `providerCredentials`. The orchestrator returns ranked `MarketQuote`s with reasons for each ranking. 6. **Place an order.** `POST /market/orders` with `machineId`, `agentPairingId`, `serviceId`, and optional `searchId` + `quoteId` creates an order (status `created`). The orchestrator enforces the delegation policy and per-transaction and daily spend limits at create. The recommended payment rail is copied from the service into `order.payment`. 7. **Settle payment.** `POST /market/orders/:orderId/payment-intent` mints a payment record on the chosen rail. For wallet-based rails, submit `POST /payment-proof` after the on-chain transfer (EVM proofs are RPC-verified against the ERC-20 Transfer log). For the x402 rail there is no buyer transfer — the agent wallet signs the provider's payment challenge and the signed header is the proof; the provider is paid during execute. For escrow rails, call `POST /payment/escrow-lock` with the on-chain lock transaction. Once funds clear, the order moves to `ready`. 8. **Execute.** `POST /market/orders/:orderId/execute` materialises the order as a task plus route, dispatches to a native provider or a machine-side runtime endpoint, and writes a `Run` plus an `Outcome`. Successful runs land in `delivered`. External-handoff services return a structured handoff and land in `handoff`. 9. **Confirm or dispute.** `POST /market/orders/:orderId/confirm` closes the order and triggers `payment/release` for held funds. If the service did not deliver, `POST /market/orders/:orderId/dispute` opens a dispute and freezes payment until peaq resolves it. ### Read-only shortcut Free, read-only operations can skip the order flow entirely. `POST /market/services/:serviceId/execute` runs the operation directly with `machineId`, `agentPairingId`, `operation`, and optional `input` / `providerCredentials`. The orchestrator dispatches through the same task + route + run + outcome machinery as `executeMarketOrder` but does not create an order, payment intent, or escrow record. Paid, external-handoff, and state-changing operations still go through `POST /market/orders`. ## Payment model peaqOS does not collect marketplace payments. The payment-intent and payment-proof endpoints record provider/service payment state only when the selected adapter requires direct provider payment. Operators or machines pay the selected provider directly through that provider's supported rail. Raw payment headers are never stored. ## Payment rails The orchestrator quotes services in the rail the provider supports. Active rails: | Rail | Where it shows up | | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `x402` | [agentic.market](https://agentic.market) services and most [pay.sh](https://pay.sh) services. Agent wallet responds to an HTTP 402 payment-required challenge; orchestrator replays the request with the agent's payment headers via the `paidHttp` adapter. | | `mpp` | Solana micro-payment protocol. Used by some pay.sh services on Solana. | | `vault-stripe` / `external` | Card-mediated or external handoff. | | `wallet` / `wdk-usdt-transfer` | Direct USDT transfer on peaq (default token `USDT`, decimals from `PEAQOS_USDT_DECIMALS`). | | `onchain-escrow` / `escrow` | Funds locked into a service-specified escrow contract via `payment/escrow-lock`. | | `not-required` | Free or pre-authorised services. Order proceeds straight to `ready`. | | `offchain-record` | Off-chain attestation, recorded but not RPC-verified. | The orchestrator handles cross-chain conversion when a service quotes in a chain or token the agent does not natively hold. ## Service providers **At launch:** * **QVAC** private inference (Tether strategic, co-announced). * The **[agentic.market](https://agentic.market)** suite over x402: Claude, ChatGPT, 2Captcha, Firecrawl, Wolfram|Alpha, Exa. * The **[pay.sh](https://pay.sh)** suite: Gemini, BigQuery, Document AI, StableUpload, Cloud Translation. **Added since launch:** * **Akash** — decentralized compute. * **Aethir**: decentralized GPU compute. * **GEODNET** — RTK precision-positioning data. * **Arcium**: confidential compute (`compute.confidential`). * **Acurast** — decentralized compute; its Deploy Agent runs as a native x402 provider runtime in the orchestrator. * **Naver Maps** — mapping and geolocation APIs. * **Korea Public Data** (Data.go.kr): EV charging stations and chargers, next-hour weather, and AirKorea PM2.5. Needs a portal service key, sent with every request and never stored. * **World ID**: proof of personhood (`identity.proof-of-person`). * **Walrus**: object storage. * **Home Assistant**: device control on the machine's local network. * **Aave**: treasury deposits. * **peaq Telemetry Export**: fleet telemetry export and machine data publishing. The live catalogue is on [robotic.sh](https://robotic.sh); query it programmatically via [`GET /market/services` and market search](/peaqos/api-reference/machine-markets-discovery). The adapter registry (`GET /market/adapter-credential-stack`) is the authoritative list of what needs credentials or setup. ## Delegation policy ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type DelegationPolicy = { allowedSkillKeys: string[]; // e.g. ["storage.object", "compute.marketplace"] deniedSkillKeys: string[]; allowedServiceIds: string[]; // exact service IDs from /market/services deniedServiceIds: string[]; perTransactionLimit?: number | null; dailySpendLimit?: number | null; currency?: string | null; // ISO 4217 or token symbol }; ``` Policy is enforced server-side on every call. Update it at any time via `PATCH /machines/:machineId/agent-pairings/:pairingId`. Revoking a pairing (`DELETE`) terminates the agent's delegated access immediately, unless the pairing has open market orders (`OPEN_MARKET_ORDERS`). ## Multichain peaqID, Machine NFT, and the Service Registry stay canonical on peaq. Smart accounts and Machine NFTs deploy to supported chains (Agung and Base Sepolia at launch). Machine Agents pay across chains using the chain and token a service quotes in. A six-daemon Signer Daemon fleet watches finalized peaq and Agung events, packages them into EIP-712 batches, and pushes them to per-chain Lite contracts (`DIDLite`, `IdentityLite`, `StakingLite`). Read full architecture in the [Omni-chain concept](/peaqos/concepts/omni-chain). See the [roadmap](/roadmap) for chain rollout. ## Build with Scale Base path, access model, error codes, and the full HTTP endpoint surface. How pairing, skills, services, orders, payments, and execution hang together. `peaqos scale agent pair`, `search`, and the `order` family from your terminal. `client.orchestration` for [JS](/peaqos/sdk-reference/orchestration-js) and [Python](/peaqos/sdk-reference/orchestration-py). ## Related * [Machine Markets concept](/peaqos/concepts/machine-markets) * [Activate](/peaqos/functions/activate) * [Qualify](/peaqos/functions/qualify) * [Wallets (OWS)](/peaqos/wallets) # Stream Source: https://docs.peaq.xyz/peaqos/functions/stream Sell the data your machine generates: signed, encrypted, and verifiable, with buyers paying for access to exactly what they need. Stream is the *data* side of the machine economy. A machine signs the data it generates, encrypts whatever is sensitive, and sells access to it, so robots and devices earn from the one thing they produce constantly: machine data. Where [Scale](/peaqos/functions/scale) lets a machine *buy* services and [Monetize](/peaqos/functions/monetize) lets it sell its *capacity*, Stream lets it sell **data**. It builds on the identity a machine receives at [Activate](/peaqos/functions/activate) — peaqID, Machine NFT, and a cross-chain wallet — and uses that identity to prove which machine produced a given data package. ## What ships | Component | Description | | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | peaqOS Edge Agent | A persistent on-machine process, installed as a ROS 2 node, that signs, encrypts, and ships the data the machine generates. It provisions identity from [Activate](/peaqos/functions/activate) and runs the signing pipeline automatically once configured. | | Data Event Map | The policy file that controls what streams out: topic subscriptions, per-field rules (`include`, `exclude`, `encrypt`, `anonymize`), signing policy, and output destinations. | | Signed data packages | Every package carries the machine's peaqID, a timestamp, a schema version, and a sequence number, signed with the machine's key. Anyone verifies it with the public key derived from the DID. Field rules run before signing, so protected fields never leave in the clear while the package stays verifiable. | | Chunking + encryption | Data is grouped into bounded chunks, each encrypted under its own key and linked into a tamper-evident chain. Chunks roll up into datasets with a Merkle root. | | Data marketplace | Owners list datasets; buyers verify signatures and hashes, pay over a transfer or x402 rail, and receive an access grant — the purchased chunk keys re-wrapped to their public key. The backend stores manifests and grants, never plaintext. | | Distribution | Signed, encrypted chunks leave over the owner's chosen vector: a direct machine-to-machine [P2P transfer](/peaqos/sdk-reference/stream-distribution#p2p-delivery) (`peaqos-p2p`), S3-compatible cloud storage, Walrus, or a direct API. | ## How it works ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} flowchart LR ROS["ROS 2 topics /
sensors / feeds"] -->|raw data| Agent["peaqOS Edge Agent
(on the machine)"] Owner["Machine Owner"] -->|Data Event Map| Agent Agent -->|sign + encrypt
+ chunk| Out["Signed, encrypted
chunks + manifests"] Out --> Dist["P2P feed / cloud /
Walrus / direct API"] Dist --> Buyer["Context Provider
(buyer)"] Buyer -->|verify + pay| Backend["peaqOS backend
(manifests, grants)"] Backend -->|access grant| Buyer ``` 1. **Install and configure.** The Machine Owner installs the Edge Agent as a [ROS 2 package](/peaqos/sdk-reference/ros2/stream-agent) and writes a Data Event Map: which topics to read, which fields to keep, drop, or encrypt, and where signed data goes. 2. **Sign and encrypt.** As data is captured, field rules run first, then the package is signed with the machine's key and grouped into encrypted chunks. 3. **List and sell.** Chunks roll up into datasets the owner lists. Buyers verify the machine's signatures and chunk hashes before paying. 4. **Grant and deliver.** After payment, an access grant re-wraps the purchased chunk keys to the buyer's key. The buyer decrypts only what they bought. See [Data streams](/peaqos/concepts/data-streams) for the full trust model. ## Sell across chains Buyers pay on the chain a listing quotes in — the transfer rail covers peaq, Base, and Solana, and the x402 rail covers signed pay-per-request authorizations — while the sale record and the machine's identity stay canonical on peaq. Broader omnichain settlement is still rolling out — see the [roadmap](/roadmap). ## Build with Stream The trust model: signing, chunk chains, encryption, datasets, and buyer access grants. The `stream` module for JavaScript and Python. Purchases, payment rails, and delivery — buyer access over S3, or encrypted chunks streamed machine-to-machine over P2P. Run the on-machine node that signs, encrypts, chunks, and streams your data. Publish signed, encrypted chunks and grant buyer access from the terminal. The buyer side: discover listings, order, pay, and download as a Context Provider. Where Stream sits and what ships next. ## Related * [Data streams concept](/peaqos/concepts/data-streams) * [Activate](/peaqos/functions/activate) * [Monetize](/peaqos/functions/monetize) * [Wallets (OWS)](/peaqos/wallets) # Tokenize Source: https://docs.peaq.xyz/peaqos/functions/tokenize Fractionalize your machine into an investable asset via ERC-3643. **Tokenize is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap). Tokenize lets machine owners fractionalize a machine into a tradeable, regulatory-compliant security token, turning machine revenue into an investable asset for third parties. The underlying asset is the [Machine NFT](/peaqos/concepts/machine-nft) minted at [Activate](/peaqos/functions/activate). Tokenize wraps that NFT in an ERC-3643 share class so revenue and activity tracked by [Qualify](/peaqos/functions/qualify) flow back to token holders. # Verify Source: https://docs.peaq.xyz/peaqos/functions/verify Prove your machine is real via hardware attestation and trusted third parties. **Verify is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap). Verify is the attestation layer for peaqOS. It lets manufacturers, labs, and the peaq Foundation co-sign a machine's identity, producing a portable signal that the machine exists and is who it claims to be. Today, [trust levels](/peaqos/concepts/trust-levels) are self-attested when an event is submitted. `Hardware-signed = 2` is a valid value but there's no third-party attestation infrastructure behind it yet. Verify attests the **machine** (not individual events) and is what closes that gap. ## What Verify v1 will be The first release is specified and its contract is written; nothing is deployed and no SDK or API surface exists yet. | Piece | What it does | | :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Attestation registry | An on-chain registry of M-of-N attester quorums. For each subject and topic it answers one of four states: `unverified`, `verified`, `expired`, `revoked`. Revocation dominates expiry. There is no aggregate "verified" boolean across topics. | | Two topics | **KYB** on the machine's current operator address, and **chip** on the machine ID. Each is reported independently. | | Chip proof | Hardware attestation against one certified chip family in v1: an Infineon OPTIGA Trust M v3 secure element whose leaf certificate chains to the Infineon CA. No TPM, software key, or other secure element is accepted in v1. | | Authority split | The SDK validates chip evidence locally and reads attestation state through the peaqOS API; it never touches the chip, submits evidence, or writes the registry. A verifier backend issues challenges, checks evidence, and anchors the result. | Contract addresses, endpoint paths, and SDK methods will appear here when they ship. # Fleet onboarding (operator-controlled) Source: https://docs.peaq.xyz/peaqos/guides/proxy-operator-fleet Activate and manage many machines from one operator under Economics 2.0: each machine owns its NFT and pays its bond, the operator is its DID controller. One operator, many machines. Under [Economics 2.0](/peaqos/concepts/economics-2-0) the account that signs `activateMachine` becomes the machine's owner and pays its bond, so a fleet is built the other way round from Tokenomics 1.0: each **machine** signs its own activation with its own key, and your operator address is recorded as the machine's DID **controller**. The operator then runs lifecycle, renewals, and DID updates for the whole fleet. **Operator-sponsored registration is gone.** `registerFor` / `register_for` (the 1.0 proxy pattern where the operator paid and owned every identity) has no Economics 2.0 equivalent: `MachineStateAndSync.activateMachine` makes `msg.sender` both owner and payer, and the SDKs throw `SPONSORED_ACTIVATION_UNSUPPORTED` in Tokenomics mode. If a machine cannot hold a key and funds, there is no fallback; do not make the operator the owner instead, because the owner is the account that can transfer the NFT. ## Rights in the machine-owned, operator-controlled model | Who | Can | Cannot | | :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | | Machine wallet (owner) | Transfer the NFT; rotate or clear the controller; everything the controller can | | | Operator (controller) | Suspend and resume; renew the subscription (paying from the operator wallet); update verification methods, authentication, and service endpoints | Transfer the NFT; rotate or clear the controller | Credits from renewals (bonding reward and voucher credit) land on the **owner**, even when the controller pays. Both parties should know this before onboarding. ## Prerequisites * Node.js ≥ 22 with `@peaqos/peaq-os-sdk` 0.7.0+, or Python 3.10+ with `peaq-os-sdk` 0.7.1+, or `peaq-os-cli` 0.0.8+ on `peaq-os-sdk` 0.7.1+ * An operator wallet for gas (it signs nothing during activation, but signs later renewals and DID updates) * Each machine wallet funded with its tier bond plus gas. The bond is quoted per tier at the oracle rate; preview one activation first and fund every machine to that amount plus a gas margin. The operator can fund machine wallets from the [Gas Station](/peaqos/concepts/gas-station) (2FA-gated) for gas and top up the bond from any PEAQ-holding address. * Environment variables configured per the [install guide](/peaqos/install) JS examples load `.env` via `import "dotenv/config"`. Python's `from_env()` reads from the shell, so export the file first with `set -a && source .env && set +a` or call `load_dotenv()`. ## From the terminal The CLI does one machine per command. `--for` and `--machine-key` switch to machine-owned mode: the machine key signs and pays, your `PEAQOS_PRIVATE_KEY` address becomes controller. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos activate \ --for 0xMachineAddress --machine-key ./machine.key \ --machine-type Sensor --credential-subject-hex 0x \ --manufacturer 0x3333333333333333333333333333333333333333 \ --tier entry --did-document ./did.json --dry-run # drop --dry-run to submit ``` Loop it over your key files with `--json --yes` for scripts. Afterwards the operator manages every machine with `peaqos machine ...` (status, suspend, resume, subscription renew, DID setters). See [CLI: activate](/peaqos/cli#peaqos-activate) and [CLI: machine](/peaqos/cli#peaqos-machine). ## Single machine, from code Used for funding, previews, and later management. It never signs an activation. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const operator = PeaqosClient.fromEnv(); console.log("Operator:", operator.address); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv from peaq_os_sdk import PeaqosClient load_dotenv() operator = PeaqosClient.from_env() print("Operator:", operator.address) ``` Each machine needs its own address. Store the key on the device or in a secrets vault: it is the owner key of that machine's NFT. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const machineKey = PeaqosClient.generateKeypair(); console.log("Machine address:", machineKey.address); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} machine_address, machine_private_key = PeaqosClient.generate_keypair() print("Machine address:", machine_address) ``` Gas from the Gas Station (2FA on the operator wallet), the bond from any PEAQ-holding address. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const FAUCET_URL = "https://depinstation.peaq.xyz"; await operator.fundFromGasStation( { ownerAddress: operator.address, targetWalletAddress: machineKey.address, chainId: "peaq", twoFactorCode: "654321" }, FAUCET_URL, ); // Then transfer the previewed netPeaqAmount (plus a margin) to machineKey.address from your treasury wallet. ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} FAUCET_URL = "https://depinstation.peaq.xyz" operator.fund_from_gas_station( owner_address=operator.address, target_wallet_address=machine_address, chain_id="peaq", two_factor_code="654321", faucet_base_url=FAUCET_URL, ) # Then transfer the previewed net_peaq_amount (plus a margin) to machine_address from your treasury wallet. ``` ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const machine = new PeaqosClient<"tokenomics20">({ rpcUrl: operator.rpcUrl, privateKey: machineKey.privateKey, contracts: operator.contracts, tokenomics20: { deploymentId: "peaq-mainnet" }, }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import Tokenomics20Config machine = PeaqosClient( rpc_url=operator.rpc_url, private_key=machine_private_key, identity_registry=operator.contracts.identity_registry, identity_staking=operator.contracts.identity_staking, event_registry=operator.contracts.event_registry, machine_nft=operator.contracts.machine_nft, did_registry=operator.contracts.did_registry, batch_precompile=operator.contracts.batch_precompile, tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), ) ``` `controller` is the operator address. The machine signs, owns the NFT, and pays the bond. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const params = { controller: operator.address, verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: operator.address, publicKeyMultibase: "z6Mk..." }], authentication: [0n], serviceEndpoints: [{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" }], machineType: "Sensor", credentialSubject: "0x", manufacturer: "0x3333333333333333333333333333333333333333", tier: 0, }; const preview = await machine.previewMachineActivation(params); const result = await machine.activateMachine(params); console.log("machine", result.machineId.toString(), "owner", result.owner, "controller", result.controller); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ActivateMachineParams, ServiceEndpointInput, VerificationMethodInput params = ActivateMachineParams( controller=operator.address, verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=operator.address, public_key_multibase="z6Mk..."),), authentication=(0,), service_endpoints=(ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"),), machine_type="Sensor", credential_subject=bytes.fromhex(""), manufacturer="0x3333333333333333333333333333333333333333", tier=0, ) preview = machine.preview_machine_activation(params) result = machine.activate_machine(params) print("machine", result.machine_id, "owner", result.owner, "controller", result.controller) ``` From here the operator client (constructed with the same `tokenomics20` deployment) runs the fleet. Renewals paid by the operator credit the owner. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const ops = new PeaqosClient<"tokenomics20">({ ...operatorConfig, tokenomics20: { deploymentId: "peaq-mainnet" } }); const state = await ops.getMachineManagementState(result.machineId); await ops.suspendMachine(result.machineId); await ops.resumeMachine(result.machineId); const renewal = await ops.previewMachineRenewal({ machineId: result.machineId, payment: "peaq" }); await ops.renewMachine({ machineId: result.machineId, payment: "peaq", maxNetPeaqAmount: renewal.maxNetPeaqAmount }); await ops.setMachineServiceEndpoints(result.machineId, [ { id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs/v2" }, ]); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PreviewMachineRenewalParams, RenewMachineParams ops = PeaqosClient(..., tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet")) # operator key state = ops.get_machine_management_state(result.machine_id) ops.suspend_machine(result.machine_id) ops.resume_machine(result.machine_id) renewal = ops.preview_machine_renewal(PreviewMachineRenewalParams(result.machine_id, payment="peaq")) ops.renew_machine(RenewMachineParams(result.machine_id, payment="peaq", max_net_peaq_amount=renewal.max_net_peaq_amount)) ops.set_machine_service_endpoints(result.machine_id, ( ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs/v2"), )) ``` ## Batch activation Activate machines sequentially; each iteration builds a machine client from that machine's key and records the operator as controller. Persist every returned machine ID: there is no on-chain fleet index in 2.0 yet (the 1.0 `machines` DID attribute and `GET /operator/{did}/machines` do not cover 2.0 machines). ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const operator = PeaqosClient.fromEnv(); const keys = loadMachineKeys(); // [{ address, privateKey, credentialSubject }], funded with bond + gas const fleet: { address: string; machineId: string }[] = []; for (const [i, k] of keys.entries()) { const machine = new PeaqosClient<"tokenomics20">({ rpcUrl: operator.rpcUrl, privateKey: k.privateKey, contracts: operator.contracts, tokenomics20: { deploymentId: "peaq-mainnet" }, }); const params = { controller: operator.address, verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: operator.address, publicKeyMultibase: "z6Mk..." }], authentication: [0n], serviceEndpoints: [], machineType: "Sensor", credentialSubject: k.credentialSubject, manufacturer: "0x3333333333333333333333333333333333333333", tier: 0, }; try { const result = await machine.activateMachine(params); fleet.push({ address: k.address, machineId: result.machineId.toString() }); // decimal string at the JSON boundary console.log(`[${i + 1}/${keys.length}] activated ${k.address} as ${result.machineId}`); } catch (err) { console.error(`[${i + 1}/${keys.length}] failed for ${k.address}:`, err); // A RECEIPT_UNAVAILABLE here means the tx may still mine. Never re-run activation for that machine; check getMachineActivationState. } } // Persist `fleet` to a file or database. It is your fleet index. ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ActivateMachineParams, PeaqosClient, Tokenomics20Config, VerificationMethodInput operator = PeaqosClient.from_env() keys = load_machine_keys() # [(address, private_key, credential_subject_bytes)], funded with bond + gas fleet = [] for i, (address, key, anchor) in enumerate(keys): machine = PeaqosClient( rpc_url=operator.rpc_url, private_key=key, identity_registry=operator.contracts.identity_registry, identity_staking=operator.contracts.identity_staking, event_registry=operator.contracts.event_registry, machine_nft=operator.contracts.machine_nft, did_registry=operator.contracts.did_registry, batch_precompile=operator.contracts.batch_precompile, tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), ) params = ActivateMachineParams( controller=operator.address, verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=operator.address, public_key_multibase="z6Mk..."),), authentication=(0,), service_endpoints=(), machine_type="Sensor", credential_subject=anchor, manufacturer="0x3333333333333333333333333333333333333333", tier=0, ) try: result = machine.activate_machine(params) fleet.append({"address": address, "machine_id": str(result.machine_id)}) # decimal string at the JSON boundary print(f"[{i + 1}/{len(keys)}] activated {address} as {result.machine_id}") except Exception as err: print(f"[{i + 1}/{len(keys)}] failed for {address}: {err}") # A PENDING_TRANSACTION here means the tx may still mine. Never re-run activation for that machine; reconcile it. # Persist `fleet` to a file or database. It is your fleet index. ``` ## Key management | Approach | How it works | Trade-offs | | :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | | Per-machine key on the device | The device holds its owner key and signs its own activation and, later, its own transactions. | Strongest isolation. Required if the device should ever transfer its NFT or rotate its controller. | | Per-machine key in the operator's vault | The operator generates and stores one key per machine (HSM, secrets manager) and signs activations from a provisioning host. | The machine still owns its NFT on chain; operationally the operator controls both roles. A vault leak exposes the fleet. | A single shared key for every machine is not possible under 2.0: each machine ID maps to one owner, and a wallet may own many machines, but the machine's DID controller and owner are what the SDK checks. Use `setMachineApprovalForAll` / `peaqos machine approve-all` if a second address must be able to transfer the fleet's NFTs. ## Fleet queries The 1.0 fleet listing (`GET /operator/{did}/machines`, built on the proxy's `machines` DID attribute) does not know 2.0 machines. Read 2.0 machines by ID from your own index: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} for m in fleet: state = ops.get_machine_management_state(int(m["machine_id"])) print(m["machine_id"], state.subscription.status, state.is_available, state.is_relocating) ``` ## Error handling | Error | Cause | Resolution | | :-------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :----------------------------------------------------------------- | | `TokenomicsUnsupportedError` `SPONSORED_ACTIVATION_UNSUPPORTED` | `registerFor` / `register_for` called in Tokenomics mode | Activate with the machine key and set the operator as `controller` | | `TokenomicsActivationError` `INSUFFICIENT_BALANCE` | The machine wallet does not hold the net PEAQ plus gas | Fund the machine wallet, not the operator's | | `TokenomicsActivationError` `NOT_OWNER_OR_CONTROLLER` | Operator tried a management write on a machine that does not list it as controller | Have the owner call `setMachineController` | | `TokenomicsActivationError` `NOT_MACHINE_OWNER` | Operator tried `setMachineController`, `transferMachine`, or a clear-controller | Owner-only operations; sign with the machine key | | `TokenomicsPendingTransactionError` `PENDING_TRANSACTION` (Python) / `TokenomicsActivationError` `RECEIPT_UNAVAILABLE` (JS) | Receipt did not arrive in time | Never resubmit; reconcile the recorded hash | Full code tables on [errors](/peaqos/sdk-reference/errors#tokenomics-2-0-errors). ## Fleets onboarded before Economics 2.0 Machines registered with `registerFor` / `register_for` before 2026-09-01 are Tokenomics 1.0 machines owned by the proxy. They keep working on a client constructed without `tokenomics20` and through the 1.0 MCR API (`GET /operator/{did}/machines`). peaq mirrors partner fleets into Economics 2.0 through the `MachineMigrationHub`; see [Economics 2.0: legacy machines](/peaqos/concepts/economics-2-0#legacy-machines). # Query Machine Credit Rating Source: https://docs.peaq.xyz/peaqos/guides/query-mcr Fetch a machine's MCR from the peaqOS MCR API. Covers curl, JavaScript, and Python. The MCR API returns a machine's credit rating along with event counts, revenue trend, and bond status. It's a public read API. No authentication required. ## Endpoint ``` GET {PEAQOS_MCR_API_URL}/mcr/{did} ``` `{did}` accepts either a full DID (`did:peaq:0xabc...`) or a raw EVM address (`0xabc...`). This is the Tokenomics 1.0 MCR API; machines activated under [Economics 2.0](/peaqos/concepts/economics-2-0) (decimal machine IDs) are served by the 2.0 server at `https://mcr-20.peaq.xyz` (`GET /mcr/did:peaq:`, since 2026-09-05), and the SDK query helpers are disabled in Tokenomics mode. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines). Set `PEAQOS_MCR_API_URL` to the root of the MCR API server. The public peaq-hosted MCR is at `https://mcr.peaq.xyz`. Self-hosted deployments default to `http://127.0.0.1:8000`. ## Fetch the MCR ```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -s "${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc1230000000000000000000000000000000001" ``` ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const did = "did:peaq:0xabc1230000000000000000000000000000000001"; const response = await fetch( `${PEAQOS_MCR_API_URL}/mcr/${encodeURIComponent(did)}` ); if (!response.ok) { throw new Error(`MCR API returned ${response.status}`); } const mcr = await response.json(); console.log("Rating:", mcr.mcr); console.log("Score:", mcr.mcr_score); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os import requests did = "did:peaq:0xabc1230000000000000000000000000000000001" response = requests.get( f"{os.environ['PEAQOS_MCR_API_URL']}/mcr/{did}" ) response.raise_for_status() mcr = response.json() print("Rating:", mcr["mcr"]) print("Score:", mcr["mcr_score"]) ``` ## Response shape ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "did": "did:peaq:0xabc1230000000000000000000000000000000001", "machine_id": 1, "mcr_score": 45, "mcr": "BB", "mcr_degraded": false, "bond_status": "bonded", "negative_flag": false, "event_count": 12, "revenue_event_count": 7, "activity_event_count": 5, "revenue_trend": "stable", "total_revenue": 35000, "average_revenue_per_event": 5000.0, "last_updated": 1711900000 } ``` `total_revenue` and `average_revenue_per_event` are USD cents. Divide by 100 for display ($350 and $50 in this example). A newly registered machine that hasn't accumulated enough history returns `mcr: "Provisioned"` with `mcr_score: 0`. Unbonded machines return `mcr: "NR"` with `mcr_score: 0`. See [GET /mcr/\{did}](/peaqos/api-reference/get-mcr) for the full field reference. ## Query an operator's fleet Use the operator endpoint to list all machines registered under a proxy operator, paginated with MCR scores per machine. ```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}} curl -s "${PEAQOS_MCR_API_URL}/operator/did:peaq:0xProxyAddress/machines?offset=0&limit=20" ``` ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const operatorDid = "did:peaq:0xProxyAddress"; const response = await fetch( `${PEAQOS_MCR_API_URL}/operator/${encodeURIComponent(operatorDid)}/machines?offset=0&limit=20` ); const { machines } = await response.json(); for (const m of machines) { console.log(`Machine ${m.machine_id}: ${m.mcr}`); } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os import requests operator_did = "did:peaq:0xProxyAddress" response = requests.get( f"{os.environ['PEAQOS_MCR_API_URL']}/operator/{operator_did}/machines", params={"offset": 0, "limit": 20}, ) response.raise_for_status() for m in response.json()["machines"]: print(f"Machine {m['machine_id']}: {m['mcr']}") ``` See [GET /operator/\{did}/machines](/peaqos/api-reference/get-operator-machines) for the full response shape and pagination details. ## Caching The server applies a 1-hour TTL on MCR responses by default, configurable via the `MCR_CACHE_TTL` env var (`0` to disable). Repeat requests within the window return cached values. The `last_updated` field tells you when the underlying events were most recently added. ## Error handling The MCR API returns standard HTTP status codes: `404` when the DID is unregistered, `503` when the chain is unavailable, `400` for malformed inputs. Bodies are JSON with a `detail` field carrying the upstream message. If you call through the SDK (`queryMcr(client, did)` in JS / `query_mcr(client, did)` in Python), HTTP failures surface as `RuntimeError` (JS) or `ApiError` (Python). The `code` attribute carries `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `SERVER_ERROR`, `TIMEOUT`, etc. See [SDK errors reference](/peaqos/sdk-reference/errors) for the full code map. ## Next steps * [GET /mcr/\{did}](/peaqos/api-reference/get-mcr): full API reference for this endpoint * [GET /machine/\{did}](/peaqos/api-reference/get-machine): full machine profile * [Machine Credit Rating concept](/peaqos/concepts/machine-credit-rating): rating scale and lifecycle # ROS 2 machine runtime Source: https://docs.peaq.xyz/peaqos/guides/ros2-machine-runtime Run peaqOS machine onboarding, MCR, events, smart accounts, and Machine NFT bridge flows from ROS 2. This guide is for teams that use ROS 2 as the robot control plane and want peaqOS as the machine identity, credit, and asset layer. The ROS 2 node wraps the same peaqOS capabilities documented in the [JavaScript SDK](/peaqos/sdk-reference/sdk-js) and [Python SDK](/peaqos/sdk-reference/sdk-python), but changes the security boundary: callers pass EVM addresses over ROS, while signing keys stay in a local registry file. ## Prerequisites * ROS 2 Jazzy on a native host, or the repository Docker image with ROS 2 Humble * A peaq EVM RPC endpoint * `https://mcr.peaq.xyz` for MCR reads * `https://depinstation.peaq.xyz` for Gas Station flows * A funded peaq EVM signer for registration, minting, events, smart accounts, and bridge transactions * Base ETH on the signer only if you need Base to peaq bridge operations ## Configure Create a local config: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cp peaq_ros2_examples/config/peaq_robot.example.yaml \ peaq_ros2_examples/config/peaq_robot.yaml ``` Set the peaqOS section: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: enabled: true rpc_url: "https://quicknode1.peaq.xyz" api_url: "https://mcr.peaq.xyz" faucet: base_url: "https://depinstation.peaq.xyz" qr_format: "svg" wallet_registry: path: "~/.peaq_robot/peaqos_wallets.json" ``` Do not commit `peaq_robot.yaml` after adding real addresses, local registry paths, or operational credentials. ## Build and start ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} source /opt/ros/jazzy/setup.bash # In the Docker image, use: source /opt/ros/humble/setup.bash python3 -m pip install -r requirements.txt colcon build --packages-select peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml ``` Open another terminal, source the same ROS environment, then call services. ## 1. Create a local machine wallet ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/wallet/create \ peaq_ros2_interfaces/srv/PeaqosCreateWallet \ "{label: 'robot-001'}" ``` Save the returned address. The private key is stored in the local registry and is not returned. You can inspect or remove local wallet metadata without exposing keys: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/wallet/list \ peaq_ros2_interfaces/srv/PeaqosListWallets \ "{}" ros2 service call /peaqos_node/wallet/get \ peaq_ros2_interfaces/srv/PeaqosGetWallet \ "{address: ''}" ``` The returned wallet JSON includes public peaq EVM account metadata: `address`, `account_id`, `chain_id`, `network`, `label`, and `created_at`. ## 2. Fund the machine wallet Registration signs from the machine wallet and requires enough native peaq for gas plus the IdentityRegistry registration bond. Fund the returned `` before calling `register`. You can use the Gas Station flow with owner 2FA: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/faucet/setup_2fa \ peaq_ros2_interfaces/srv/PeaqosSetupFaucet2FA \ "{owner_address: '', qr_format: 'svg'}" ros2 service call /peaqos_node/faucet/confirm_2fa \ peaq_ros2_interfaces/srv/PeaqosConfirmFaucet2FA \ "{owner_address: '', two_factor_code: '123456'}" ros2 service call /peaqos_node/wallet/fund \ peaq_ros2_interfaces/srv/PeaqosFundWallet \ "{owner_address: '', target_address: '', chain_id: '3338', two_factor_code: '123456', request_id: ''}" ``` Or transfer native peaq directly to `` from an already funded wallet. Wait for the balance to be available before registration. ## 3. Register the machine For a self-managed machine, the node looks up `` in the local registry and signs the registration transaction with that wallet. For a self-managed machine: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/machine/register \ peaq_ros2_interfaces/srv/PeaqosRegisterMachine \ "{address: ''}" ``` For a proxy operator: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/machine/register_for \ peaq_ros2_interfaces/srv/PeaqosRegisterFor \ "{proxy_address: '', machine_address: ''}" ``` Registration returns `machine_id`. This is the IdentityRegistry machine ID. It is not necessarily the Machine NFT token ID. ## 4. Mint the Machine NFT and write DID attributes ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/nft/mint \ peaq_ros2_interfaces/srv/PeaqosMintNft \ "{signer_address: '', machine_id: 1, recipient: ''}" ``` Read the Machine NFT token ID: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/nft/token_id_of \ peaq_ros2_interfaces/srv/PeaqosTokenIdOf \ "{signer_address: '', machine_id: 1}" ``` Link the machine's DID to its Machine NFT and metadata endpoints: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/did/write_machine_attributes \ peaq_ros2_interfaces/srv/PeaqosWriteMachineDidAttributes \ "{signer_address: '', machine_id: 1, nft_token_id: 1, operator_did: 'did:peaq:', documentation_url: 'https://docs.example/robot-001', data_api: 'https://api.example/robot-001', data_visibility: 'onchain'}" ``` Verify a single attribute: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/did/read_attribute \ peaq_ros2_interfaces/srv/PeaqosReadDidAttribute \ "{signer_address: '', did_address: '', name: 'machineId'}" ``` ## 5. Query MCR and machine profile ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/mcr/query \ peaq_ros2_interfaces/srv/PeaqosQueryMcr \ "{did: 'did:peaq:'}" ``` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/mcr/machine \ peaq_ros2_interfaces/srv/PeaqosQueryMachine \ "{did: 'did:peaq:'}" ``` Newly registered machines can return `Provisioned` until enough event history exists. ## 6. Validate and submit events Validate before broadcasting: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/events/validate \ peaq_ros2_interfaces/srv/PeaqosValidateEvent \ "{machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x73656e736f723a6f6b', trust_level: 0, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}" ``` Submit one event: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/events/submit \ peaq_ros2_interfaces/srv/PeaqosSubmitEvent \ "{signer_address: '', machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x73656e736f723a6f6b', trust_level: 0, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}" ``` Batch submission uses `events_json`, a JSON array using the same event field names. ## 7. Predict or deploy a machine smart account ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/smart_account/address \ peaq_ros2_interfaces/srv/PeaqosGetSmartAccountAddress \ "{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}" ``` Deploy with the same parameters: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/smart_account/deploy \ peaq_ros2_interfaces/srv/PeaqosDeploySmartAccount \ "{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}" ``` ## 8. Bridge Machine NFT from peaq to Base ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/nft \ peaq_ros2_interfaces/srv/PeaqosBridgeNft \ "{signer_address: '', token_id: 1, source: 'peaq', destination: 'base', recipient: '', base_rpc_url: '', base_nft_address: '', options_hex: ''}" ``` Wait for the NFT to appear on Base: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/wait_arrival \ peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival \ "{dst_rpc_url: 'https://mainnet.base.org', dst_nft_address: '0xee8A521eA434b11F956E2402beC5eBfa753Babfa', token_id: 1, timeout: 900}" ``` ## Production checklist * Keep `peaq_os.wallet_registry.path` local to the robot or machine. * Keep the wallet registry file permissioned as `0600`. * Quote all EVM addresses in ROS YAML service payloads. * Record pre and post balances for production bridge or payment tests. * Confirm contract addresses before release if peaqOS publishes a new deployment. * Fund Base ETH before attempting Base to peaq bridge operations. ## References * [ROS 2 service catalog](/peaqos/sdk-reference/ros2/services) * [ROS 2 configuration](/peaqos/sdk-reference/ros2/configuration) * [Machine NFT concept](/peaqos/concepts/machine-nft) * [Events concept](/peaqos/concepts/events) * [Python SDK reference](/peaqos/sdk-reference/sdk-python) * [JavaScript SDK reference](/peaqos/sdk-reference/sdk-js) # Self-managed onboarding Source: https://docs.peaq.xyz/peaqos/guides/self-managed-onboarding Activate a single machine where the owner is the operator. Generate keys, set up 2FA, fund via Gas Station, preview, and activate in one transaction. Owner equals operator. One keypair, one machine, one identity. This guide covers the full flow from environment setup through on-chain activation under [Economics 2.0](/peaqos/concepts/economics-2-0). ## Prerequisites * Node.js ≥ 22 with `@peaqos/peaq-os-sdk` 0.7.0+, or Python 3.10+ with `peaq-os-sdk` 0.7.1+ * A funded EVM wallet holding the tier bond plus gas. The bond is quoted per tier at the oracle rate (Entry quoted 0.803 PEAQ on mainnet on 2026-09-04 at 20:20 UTC; the figure moves with the daily oracle price); preview it before you fund. On peaq, bond and gas come out of the same PEAQ balance. * Environment variables configured per the [install guide](/peaqos/install) Prefer the terminal? `peaqos init` then `peaqos activate ... --dry-run` does everything on this page in two commands. See [CLI: activate](/peaqos/cli#peaqos-activate). ## Environment setup ```bash .env theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # peaq mainnet RPC. See /peaqos/install#public-rpc-endpoints for alternatives PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public PEAQOS_PRIVATE_KEY=0x<64-hex-chars> # Tokenomics 1.0 contracts. The SDK constructor still requires every variable below; # activation does not read them. For agung addresses see /peaqos/install#agung-testnet-contracts. IDENTITY_REGISTRY_ADDRESS=0xb53Af985765031936311273599389b5B68aC9956 IDENTITY_STAKING_ADDRESS=0x11c05A650704136786253e8685f56879A202b1C7 EVENT_REGISTRY_ADDRESS=0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA MACHINE_NFT_ADDRESS=0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9 DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800 BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805 ``` The Economics 2.0 contract addresses are not environment variables. You select a deployment record (`peaq-mainnet` or `agung-2026-08-28`) in code and the SDK verifies its addresses against `InfoDesk.peer(role)` on chain. JS examples load the file via `import "dotenv/config"`. Python's `from_env()` reads from the shell, so export the file first with `set -a && source .env && set +a` or call `load_dotenv()`. ## Full flow `fromEnv` reads the required variables. Throws `ValidationError` if any is missing. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const owner = PeaqosClient.fromEnv(); console.log("Owner:", owner.address); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv from peaq_os_sdk import PeaqosClient load_dotenv() owner = PeaqosClient.from_env() print("Owner:", owner.address) ``` If the machine does not already have a wallet, generate one. Local operation, no chain interaction. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const keypair = PeaqosClient.generateKeypair(); console.log("Address:", keypair.address); // Store keypair.privateKey securely. It cannot be recovered. ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} address, private_key = PeaqosClient.generate_keypair() print("Address:", address) # Store private_key securely. It cannot be recovered. ``` If the machine already has a funded wallet, skip to step 6 and build the machine client with that key. The Gas Station requires 2FA before it funds any address. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const FAUCET_URL = "https://depinstation.peaq.xyz"; const enrollment = await owner.setupFaucet2FA(keypair.address, FAUCET_URL); console.log("OTPAuth URI:", enrollment.otpauthUri); console.log("QR image:", enrollment.qrImageUrl); // Scan the QR or paste the URI into your authenticator app. The QR URL expires after roughly 2 minutes. ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} FAUCET_URL = "https://depinstation.peaq.xyz" enrollment = owner.setup_faucet_2fa(owner_address=address, faucet_base_url=FAUCET_URL) print("OTPAuth URI:", enrollment["otpauth_uri"]) print("QR image:", enrollment["qr_image_url"]) ``` ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} await owner.confirmFaucet2FA(keypair.address, FAUCET_URL, "123456"); // current TOTP code ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} owner.confirm_faucet_2fa(owner_address=address, faucet_base_url=FAUCET_URL, two_factor_code="123456") ``` Request initial gas from the Gas Station. The faucet funds the wallet or skips if the balance already suffices. The faucet covers gas, not the bond: top up the bond from any PEAQ-holding address after you have previewed the quote in step 7. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await owner.fundFromGasStation( { ownerAddress: keypair.address, targetWalletAddress: keypair.address, chainId: "peaq", twoFactorCode: "654321" }, FAUCET_URL, ); console.log(result.status === "success" ? result.txHash : result.currentBalance); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} result = owner.fund_from_gas_station( owner_address=address, target_wallet_address=address, chain_id="peaq", two_factor_code="654321", faucet_base_url=FAUCET_URL, ) print(result["tx_hash"] if result["status"] == "success" else result["current_balance"]) ``` The machine's own key signs the activation, so the machine becomes owner, payer, and controller. Passing `tokenomics20` selects the Economics 2.0 deployment. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const machine = new PeaqosClient<"tokenomics20">({ rpcUrl: owner.rpcUrl, privateKey: keypair.privateKey, contracts: owner.contracts, tokenomics20: { deploymentId: "peaq-mainnet" }, // "agung-2026-08-28" for the testnet }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import Tokenomics20Config machine = PeaqosClient( rpc_url=owner.rpc_url, private_key=private_key, identity_registry=owner.contracts.identity_registry, identity_staking=owner.contracts.identity_staking, event_registry=owner.contracts.event_registry, machine_nft=owner.contracts.machine_nft, did_registry=owner.contracts.did_registry, batch_precompile=owner.contracts.batch_precompile, tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), # "agung-2026-08-28" for the testnet ) ``` The preview runs every check the activation runs (chain, contracts, peers, price, balance, allowance) without signing or spending. Use it to see the bond, any voucher credit, and the net PEAQ you must hold. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const params = { controller: keypair.address, verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: keypair.address, publicKeyMultibase: "z6Mk..." }], authentication: [0n], serviceEndpoints: [ { id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" }, { id: "#data", serviceType: "DataApi", serviceEndpoint: "https://example.com/events" }, ], machineType: "Sensor", credentialSubject: "0xdeadbeef", // your identity anchor bytes; fixes the machine ID together with machineType manufacturer: "0x3333333333333333333333333333333333333333", tier: 0, // 0 Entry, 1 Basic, 2 Pro }; const preview = await machine.previewMachineActivation(params); console.log("machine ID", preview.machineId.toString()); console.log("bond", preview.bondAmount, "voucher", preview.voucherCredit, "net", preview.netPeaqAmount); console.log("balance", preview.balance, "approval needed", preview.approvalRequired); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ActivateMachineParams, ServiceEndpointInput, VerificationMethodInput params = ActivateMachineParams( controller=address, verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=address, public_key_multibase="z6Mk..."),), authentication=(0,), service_endpoints=( ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"), ServiceEndpointInput(id="#data", service_type="DataApi", service_endpoint="https://example.com/events"), ), machine_type="Sensor", credential_subject=bytes.fromhex("deadbeef"), # your identity anchor bytes; fixes the machine ID together with machine_type manufacturer="0x3333333333333333333333333333333333333333", tier=0, # 0 Entry, 1 Basic, 2 Pro ) preview = machine.preview_machine_activation(params) print("machine ID", preview.machine_id) print("bond", preview.bond_amount, "voucher", preview.voucher_credit, "net", preview.net_peaq_amount) print("balance", preview.balance, "approval needed", preview.approval_required) ``` If `balance` is below `netPeaqAmount` plus gas, top up the machine wallet now. One transaction. The SDK approves exactly the net amount to `MachineSubscription` if needed, re-quotes, simulates, submits once, then requires the three receipt events and a matching on-chain state before returning. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await machine.activateMachine(params); console.log("Activated machine", result.machineId.toString()); console.log("Paid", result.netPeaqAmount, "wei of PEAQ; bonded", result.bondAmount); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} result = machine.activate_machine(params) print("Activated machine", result.machine_id) print("Paid", result.net_peaq_amount, "wei of PEAQ; bonded", result.bond_amount) ``` The machine now has a peaqID (`did:peaq:`), its Machine NFT (token ID equals the machine ID), and a tier bond. Confirm with `getMachineActivationState` / `get_machine_activation_state`: `subscription.periodStart != 0` means activated (tier 0 is a valid tier, so never test the tier). ## Error handling | Error | Cause | Resolution | | :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | | `ValidationError: PEAQOS_PRIVATE_KEY is required` | Missing environment variable | Check your `.env` and shell export | | `TokenomicsConfigError` `DEPLOYMENT_UNKNOWN` | Typo in the deployment ID | Use `peaq-mainnet` or `agung-2026-08-28` | | `TokenomicsActivationError` `PRICE_NOT_AVAILABLE` | The oracle holds no PEAQ price for this chain | Chain-side condition; retry after the trust validator's next daily stamp | | `TokenomicsActivationError` `INSUFFICIENT_BALANCE` | Wallet does not hold the net PEAQ plus gas | Top up the machine wallet and retry | | `TokenomicsActivationError` `CONTRACT_REVERTED` with `MachineAlreadyExists` or similar | The same `machineType` + `credentialSubject` pair was activated before | Pick a new credential subject; the pair is permanent | | `TokenomicsPendingTransactionError` / `PENDING_TRANSACTION` | Receipt did not arrive in time | Do not resubmit. Reconcile with `reconcile_activation_transaction` (Python) or re-check `getMachineActivationState` by machine ID | | `RuntimeError: Invalid 2FA code` / `ApiError: Invalid 2FA code` | TOTP code expired or mistyped | Wait for a fresh code and retry | | `RuntimeError: Faucet rate limit exceeded` / `ApiError` | Too many funding requests | Retry after a short interval | Full code tables on [errors](/peaqos/sdk-reference/errors#tokenomics-2-0-errors). ## What the machine receives * **peaqID** `did:peaq:` with the DID document you supplied (verification methods, authentication, service endpoints) * **Machine NFT** in `MachineRegistry`, token ID equal to the machine ID * **Subscription** on the chosen tier for 365 days, bonded in PEAQ; renew before the 14-day grace and 14-day runoff windows end. See [Economics 2.0](/peaqos/concepts/economics-2-0#subscription-lifecycle) * **Home chain record** on peaq * **Gas Station** funding (if requested) ## Machines onboarded before Economics 2.0 `registerMachine` / `register_machine`, `mintNft`, and `writeMachineDIDAttributes` still work on a client constructed **without** `tokenomics20` and still address the Tokenomics 1.0 contracts (1 PEAQ native bond, separate Machine NFT). They emit deprecation warnings and throw in Tokenomics mode. Use them only to maintain machines onboarded before 2026-09-01. See [Activate: legacy flow](/peaqos/functions/activate#legacy-flow-tokenomics-1-0). # Submit events Source: https://docs.peaq.xyz/peaqos/guides/submit-events Submit revenue and activity events to the EventRegistry. Covers validation, data hashing, event types, cross-chain patterns, and operational limits. Revenue events (type 0) and activity events (type 1) feed the [Machine Credit Rating](/peaqos/concepts/machine-credit-rating). Every event is validated client-side, hashed, and submitted on-chain to the EventRegistry contract. **Economics 2.0 machines.** Since `@peaqos/peaq-os-sdk` 0.7.0 and `peaq-os-sdk` 0.7.1 (2026-09-11) event submission works on a client constructed with `tokenomics20`. The SDK writes to the `EventRegistry` address you configure (`EVENT_REGISTRY_ADDRESS`): use the 2.0 contract `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e` for 2.0 machines and `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` for Tokenomics 1.0 machines. Both share the `submitEvent` selector, so a write to the wrong one lands there without reverting. `machineId` is the machine's `uint256` ID (`bigint` in JS in every mode since 0.6.0). CLI 0.0.8 `peaqos qualify event` cannot address 2.0 machines yet. ## Event types | Type | Value | Purpose | Example | | :------- | :---- | :-------------------------------------------------- | :------------------------------------------------- | | Revenue | `0` | Records economic value generated by the machine | A claw machine collects \$5.00 from a play session | | Activity | `1` | Records operational activity with no direct revenue | A weather sensor reports a telemetry ping | ## Trust levels Each event carries a trust level describing how the data was attested. See [Trust levels](/peaqos/concepts/trust-levels) for the concept overview. | Level | Value | Meaning | Needs source tx hash? | | :------------------ | :---- | :-------------------------------------------------- | :-------------------- | | Self-reported | `0` | Machine self-reports. No external verification. | No | | On-chain verifiable | `1` | Event references a verifiable on-chain transaction. | Yes | | Hardware-signed | `2` | Event signed by tamper-resistant hardware. | No | ## Currency and value units `currency` is a first-class parameter on `submitEvent` / `submit_event`. Revenue events take a 3-10 char uppercase alphanumeric code (`USD`, `HKD`, `JPY`, …); activity events must pass `""`. The SDK applies a smart default when omitted on single-event submits (revenue → `"USD"`, activity → `""`); `batchSubmitEvents` / `batch_submit_events` requires it explicitly. `value` is an **ISO 4217 minor-unit integer**: | Currency | Subunit divisor | Example | | :--------------------------------------------------- | :-------------- | :----------------------- | | `USD`, `HKD`, `EUR` (and other 2-decimal currencies) | `100` | `$1.23 → value: 123` | | `JPY`, `KRW`, `VND` (no subunits) | `1` | `¥100 → value: 100` | | `BHD`, `KWD`, `OMR` (3-decimal) | `1000` | `BD 1.234 → value: 1234` | The MCR pipeline converts `value` to USD cents using the FX rate at `timestamp`. The converted amount surfaces on [`GET /machine/{did}`](/peaqos/api-reference/get-machine) as `usd_value` (USD cents integer) on revenue events when `data_visibility` is `onchain`. `amount_status` distinguishes `"ok"`, `"unsupported_currency"` (currency not in the FX whitelist), and `"fx_unavailable"` (degraded FX feed). Non-`"ok"` rows score conservatively and surface `mcr_degraded: true` on [`GET /mcr/{did}`](/peaqos/api-reference/get-mcr). Activity events ignore the FX path entirely. They don't accumulate revenue. ## Validation Call `validateSubmitEventParams` before submitting. It throws `ValidationError` on the first invalid field. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { validateSubmitEventParams, computeDataHash, EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAIN_IDS, } from "@peaqos/peaq-os-sdk"; const params = { machineId: 1n, eventType: EVENT_TYPE_REVENUE, // 0 value: 500, // $5.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000) - 10, rawData: new TextEncoder().encode(JSON.stringify({ session: "abc123" })), trustLevel: TRUST_SELF_REPORTED, // 0 sourceChainId: SUPPORTED_CHAIN_IDS.peaq, // 3338 sourceTxHash: null, metadata: new Uint8Array([]), }; // Throws ValidationError if any field is invalid validateSubmitEventParams(params); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import json import time from peaq_os_sdk import EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAINS from peaq_os_sdk.types.events import SubmitEventParams from peaq_os_sdk.validation import validate_submit_event_params params = SubmitEventParams( machine_id=1, event_type=EVENT_TYPE_REVENUE, # 0 value=500, # $5.00 in cents currency="USD", timestamp=int(time.time()), raw_data=json.dumps({"session": "abc123"}).encode(), trust_level=TRUST_SELF_REPORTED, # 0 source_chain_id=SUPPORTED_CHAINS["peaq"], # 3338 source_tx_hash=None, metadata=b"", ) # Raises ValidationError if any field is invalid validate_submit_event_params(params) ``` ### Validation rules | Field | Constraint | Error if violated | | :------------------------------------- | :--------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | | `machineId` / `machine_id` | JS: positive `bigint` (0.6.0+; `number` is rejected). Python: positive `int` | `machineId must be a positive bigint` (JS) / `machine_id must be a positive integer` (Python) | | `eventType` / `event_type` | `0` or `1` | `eventType must be 0 or 1` | | `value` | Non-negative integer (ISO 4217 minor units) | `value must be non-negative` | | `currency` | Revenue: `^[A-Z0-9]{3,10}$`. Activity: must be `""`. | `currency must match ^[A-Z0-9]{3,10}$` / `activity events require an empty currency string` | | `trustLevel` / `trust_level` | `0`, `1`, or `2` | `trustLevel must be 0, 1, or 2` | | `sourceChainId` / `source_chain_id` | `0`, `3338`, or `8453` | `sourceChainId must be a supported chain ID (0, 3338, or 8453)` | | `rawData` / `raw_data` | Non-empty when provided | `rawData must not be empty when provided` | | `sourceTxHash` / `source_tx_hash` | 0x-prefixed 32-byte hex (66 chars) when provided | `sourceTxHash must be a 0x-prefixed 32-byte hex string` | | `timestamp` | Positive integer | `timestamp must be a positive integer` | | `sourceTxHash` when `trustLevel === 1` | Required | `sourceTxHash is required when trustLevel is 1` | The contract additionally rejects `metadata` larger than 4096 bytes with a `MetadataTooLarge` revert. The SDK validators don't enforce this client-side, so oversized payloads surface as a transaction failure (`RuntimeError`/`RpcError` with `code: "MetadataTooLarge"`) rather than `ValidationError`. ## Computing the data hash The EventRegistry stores a keccak256 hash of the raw data, not the data itself. Compute it with `computeDataHash` (JS) or `compute_data_hash` (Python). ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { computeDataHash } from "@peaqos/peaq-os-sdk"; const rawData = new TextEncoder().encode( JSON.stringify({ session: "abc123", amount: 500 }) ); const hash = computeDataHash(rawData); // hash: "0x8dd186bf57dece591311f044eaf97f4886878309ff0b546a58386d2df41978e2" ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.utils import compute_data_hash raw_data = b'{"session": "abc123", "amount": 500}' data_hash = compute_data_hash(raw_data) # data_hash is 32 bytes (keccak256) ``` The hash is passed as the `dataHash` field in the on-chain `MachineEvent` struct. Consumers who need to verify the original data compare its keccak256 against the stored hash. ## Submitting a revenue event (type 0) ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient, validateSubmitEventParams, computeDataHash, EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAIN_IDS, } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); const rawData = new TextEncoder().encode( JSON.stringify({ session: "abc123", amount: 500 }) ); const params = { machineId: 1n, eventType: EVENT_TYPE_REVENUE, value: 500, // $5.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000) - 10, rawData, trustLevel: TRUST_SELF_REPORTED, sourceChainId: SUPPORTED_CHAIN_IDS.peaq, sourceTxHash: null, metadata: new Uint8Array([]), }; validateSubmitEventParams(params); const { txHash, dataHash } = await client.submitEvent(params); console.log("Submitted revenue event:", { txHash, dataHash }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv import json import time from peaq_os_sdk import ( PeaqosClient, EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAINS, ) from peaq_os_sdk.types.events import SubmitEventParams from peaq_os_sdk.validation import validate_submit_event_params load_dotenv() # load envs from .env file client = PeaqosClient.from_env() raw_data = json.dumps({"session": "abc123", "amount": 500}).encode() params = SubmitEventParams( machine_id=1, event_type=EVENT_TYPE_REVENUE, value=500, # $5.00 in cents currency="USD", timestamp=int(time.time()) - 10, raw_data=raw_data, trust_level=TRUST_SELF_REPORTED, source_chain_id=SUPPORTED_CHAINS["peaq"], source_tx_hash=None, metadata=b"", ) validate_submit_event_params(params) tx_hash, data_hash = client.submit_event( machine_id=params.machine_id, event_type=params.event_type, value=params.value, currency=params.currency, timestamp=params.timestamp, raw_data=params.raw_data, trust_level=params.trust_level, source_chain_id=params.source_chain_id, source_tx_hash=params.source_tx_hash, metadata=params.metadata, ) print("Submitted revenue event:", tx_hash, data_hash.hex()) ``` ## Submitting an activity event (type 1) Activity events record operational telemetry with no direct revenue value. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient, validateSubmitEventParams, computeDataHash, EVENT_TYPE_ACTIVITY, TRUST_SELF_REPORTED, SUPPORTED_CHAIN_IDS, } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); const rawData = new TextEncoder().encode( JSON.stringify({ type: "heartbeat", uptimeSeconds: 86400 }) ); const params = { machineId: 1n, eventType: EVENT_TYPE_ACTIVITY, // 1 value: 0, // No revenue currency: "", // activity events must be empty timestamp: Math.floor(Date.now() / 1000) - 10, rawData, trustLevel: TRUST_SELF_REPORTED, sourceChainId: SUPPORTED_CHAIN_IDS.peaq, sourceTxHash: null, metadata: new Uint8Array([]), }; validateSubmitEventParams(params); const { txHash, dataHash } = await client.submitEvent(params); console.log("Submitted activity event:", { txHash, dataHash }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv import json import time from peaq_os_sdk import ( PeaqosClient, EVENT_TYPE_ACTIVITY, TRUST_SELF_REPORTED, SUPPORTED_CHAINS, ) from peaq_os_sdk.types.events import SubmitEventParams from peaq_os_sdk.validation import validate_submit_event_params load_dotenv() # load envs from .env file client = PeaqosClient.from_env() raw_data = json.dumps({"type": "heartbeat", "uptime_seconds": 86400}).encode() params = SubmitEventParams( machine_id=1, event_type=EVENT_TYPE_ACTIVITY, # 1 value=0, # No revenue currency="", # activity events must be empty timestamp=int(time.time()) - 10, raw_data=raw_data, trust_level=TRUST_SELF_REPORTED, source_chain_id=SUPPORTED_CHAINS["peaq"], source_tx_hash=None, metadata=b"", ) validate_submit_event_params(params) tx_hash, data_hash = client.submit_event( machine_id=params.machine_id, event_type=params.event_type, value=params.value, currency=params.currency, timestamp=params.timestamp, raw_data=params.raw_data, trust_level=params.trust_level, source_chain_id=params.source_chain_id, source_tx_hash=params.source_tx_hash, metadata=params.metadata, ) print("Submitted activity event:", tx_hash, data_hash.hex()) ``` ## Cross-chain revenue pattern When a machine earns revenue on another chain (e.g., Base), reference the source transaction for on-chain verifiable trust (level 1). ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { validateSubmitEventParams, EVENT_TYPE_REVENUE, TRUST_ON_CHAIN_VERIFIABLE, SUPPORTED_CHAIN_IDS, } from "@peaqos/peaq-os-sdk"; const params = { machineId: 1n, eventType: EVENT_TYPE_REVENUE, value: 1200, // $12.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000) - 10, rawData: new TextEncoder().encode(JSON.stringify({ invoice: "INV-0042" })), trustLevel: TRUST_ON_CHAIN_VERIFIABLE, // 1 sourceChainId: SUPPORTED_CHAIN_IDS.base, // 8453 sourceTxHash: "0xa1b2c3d4e5f6789000000000000000000000000000000000000000000000a1b2", metadata: new Uint8Array([]), }; validateSubmitEventParams(params); // sourceTxHash is required when trustLevel is 1. // The MCR system can verify this transaction on Base. ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import json import time from peaq_os_sdk import ( EVENT_TYPE_REVENUE, TRUST_ON_CHAIN_VERIFIABLE, SUPPORTED_CHAINS, ) from peaq_os_sdk.types.events import SubmitEventParams from peaq_os_sdk.validation import validate_submit_event_params params = SubmitEventParams( machine_id=1, event_type=EVENT_TYPE_REVENUE, value=1200, # $12.00 in cents currency="USD", timestamp=int(time.time()), raw_data=json.dumps({"invoice": "INV-0042"}).encode(), trust_level=TRUST_ON_CHAIN_VERIFIABLE, # 1 source_chain_id=SUPPORTED_CHAINS["base"], # 8453 source_tx_hash="0xa1b2c3d4e5f6789000000000000000000000000000000000000000000000a1b2", metadata=b"", ) validate_submit_event_params(params) # source_tx_hash is required when trust_level is 1. # The MCR system can verify this transaction on Base. ``` ### Supported chain IDs | Chain | ID | | :---------------------------- | :----- | | peaq (same-chain, or use `0`) | `3338` | | Base | `8453` | ## Operational limits The SDK enforces per-transaction value caps and rate limits before submitting. ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { checkOperationalLimits } from "@peaqos/peaq-os-sdk"; checkOperationalLimits( { machineId: 1n, value: 500 }, { maxValuePerTx: 10000, rateLimitMaxEvents: 60, rateLimitWindowSeconds: 3600, }, tracker, // EventTracker from previous submissions, or null ); // Throws ValueCapExceeded if value > maxValuePerTx // Throws RateLimitExceeded if count >= rateLimitMaxEvents within window ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.types.client import OperationalLimits from peaq_os_sdk.validation import check_operational_limits check_operational_limits( params, # SubmitEventParams (or any object with .machine_id and .value) OperationalLimits( max_value_per_tx=10000, rate_limit_max_events=60, rate_limit_window_seconds=3600, ), tracker, # EventTracker from previous submissions, or None ) # Raises ValueCapExceeded if value > max_value_per_tx # Raises RateLimitExceeded if count >= rate_limit_max_events within window ``` | Limit | Error type | Description | | :--------------------------------------------- | :------------------ | :----------------------------------------------------- | | `maxValuePerTx` / `max_value_per_tx` | `ValueCapExceeded` | Single event value exceeds the per-transaction cap | | `rateLimitMaxEvents` / `rate_limit_max_events` | `RateLimitExceeded` | Too many events submitted within the rate-limit window | Set limits to `0` to disable (the default). ## Error handling `submitEvent` / `submit_event` raise four distinct error types. Validation and limit errors are local; `RuntimeError` (JS) / `RpcError` (Python) wraps every chain or RPC failure. JS collapses chain and HTTP errors into a single `RuntimeError`; Python keeps them separate (`RpcError` for chain, `ApiError` for HTTP). ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { ValidationError, ValueCapExceeded, RateLimitExceeded, RuntimeError, } from "@peaqos/peaq-os-sdk"; try { const { txHash, dataHash } = await client.submitEvent(params); } catch (err) { if (err instanceof ValidationError) { // Bad params: check err.field, err.constraint } else if (err instanceof ValueCapExceeded) { // Per-tx value cap tripped } else if (err instanceof RateLimitExceeded) { // Local rate limit tripped } else if (err instanceof RuntimeError) { // Chain/RPC failure: err.code carries the contract revert name // (e.g. "MetadataTooLarge", "MachineNotFound", "NotAuthorizedSubmitter") // or "TX_REVERTED" for unrecognized reverts } else { throw err; } } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( ValidationError, ValueCapExceeded, RateLimitExceeded, RpcError, ) try: tx_hash, data_hash = client.submit_event( machine_id=params.machine_id, event_type=params.event_type, value=params.value, currency=params.currency, timestamp=params.timestamp, raw_data=params.raw_data, trust_level=params.trust_level, source_chain_id=params.source_chain_id, source_tx_hash=params.source_tx_hash, metadata=params.metadata, ) except ValidationError as err: # Bad params: inspect err.field, err.constraint raise except ValueCapExceeded: # Per-tx value cap tripped raise except RateLimitExceeded: # Local rate limit tripped raise except RpcError as err: # Chain/RPC failure: err.code carries the contract revert name # (e.g. "MetadataTooLarge", "MachineNotFound") or "TX_REVERTED" raise ``` See [SDK errors reference](/peaqos/sdk-reference/errors) for the full code map and the cross-language equivalence between `RuntimeError` (JS) and `RpcError`/`ApiError` (Python). ## Next steps * [Events concept](/peaqos/concepts/events) for deeper coverage of event types and trust levels * [SDK reference](/peaqos/sdk-reference/sdk-js) for full method signatures # Install Source: https://docs.peaq.xyz/peaqos/install Install peaqOS via npm or pip. Pick the SDK that matches your stack. Both paths converge on the same onchain state. ## Requirements | Requirement | Value | | :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Node.js | ≥ 22 | | TypeScript | ≥ 5 (for the JS SDK) | | Python | ≥ 3.12 for the Python SDK 0.6.0+ and the CLI 0.0.8+. SDK 0.4.0 to 0.5.0 and CLI 0.0.6 to 0.0.7 need ≥ 3.11. SDK 0.3.0 / CLI 0.0.5 are the last releases that run on 3.10 | | Peer dependency | `viem >= 2.47.10` (JS only; Python pulls `web3 >= 6.0` automatically) | | RPC access | peaq mainnet or agung testnet | | Gas | Handled by Gas Station on fresh wallets; needs 2FA for request | ## Install paths ### Two pieces ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # 1. Install the CLI (the skill drives it under the hood); include the [ows] extra # so `peaqos wallet ...` works inside the skill's onboarding flow. # The skill was last aligned with CLI 0.0.6 and still issues the Tokenomics 1.0 # activate flags, so pin below 0.0.8 until the skill updates. Pin the SDK too: # the CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, which breaks `peaqos monetize`. # Needs Python 3.11 or newer. pip install 'peaq-os-cli[ows]<0.0.8' 'peaq-os-sdk<0.6.0' # 2. Add the peaqos skill to your agent (auto-detects Claude Code, Cursor, or Windsurf) npx skills add peaqnetwork/peaq-os-skills ``` Then invoke `/peaqos` in any Claude Code session. To target a specific runtime explicitly, add `--agent claude-code | cursor | windsurf`. See the [peaqOS AI page](/peaqos/peaqos-ai) for details and the manual upload path for ChatGPT / custom GPTs. ### Works with The `peaqos` skill ships first-class adapters for Claude Code, Cursor, and Windsurf — auto-detected, or selected with `--agent`. Hosted assistants without a local CLI (ChatGPT, Claude Projects, custom GPTs) can load `AGENT-PROMPT.md` as a system prompt manually. The skill calls the JavaScript and Python SDKs through the CLI. Tabs below apply once you start editing code yourself. ### One command ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-os-cli # Optional: include OWS wallet commands (peaqos wallet create / import / list / …) pip install 'peaq-os-cli[ows]' peaqos init peaqos activate --machine-type Sensor --credential-subject-hex 0xdeadbeef \ --manufacturer 0x3333333333333333333333333333333333333333 --tier entry \ --did-document ./did.json --dry-run # drop --dry-run to submit ``` Needs Python 3.10 or newer. `peaqos init` writes `TOKENOMICS_DEPLOYMENT_ID` (`peaq-mainnet` or `agung-2026-08-28`); `peaqos activate`, `peaqos machine`, and `peaqos monetize` all require it. The Economics 2.0 contract addresses are not in `.env`: they ship inside the SDK's deployment record. **After running `peaqos init`, open your `.env` and verify the six Tokenomics 1.0 addresses against the [peaq mainnet contracts](#peaq-mainnet-contracts) table on this page.** A known bug in the init wizard can silently write a contract address to the wrong variable name. The SDK constructor still requires them even though activation does not read them. Drives the same flows as the SDKs from your terminal: `peaqos init`, `peaqos whoami`, `peaqos activate`, `peaqos machine` (status, suspend, resume, subscription, transfer, DID updates), `peaqos monetize`, `peaqos wallet`, `peaqos stream`, `peaqos scale`. `peaqos qualify` and `peaqos show` read the Tokenomics 1.0 MCR API and fail when `TOKENOMICS_DEPLOYMENT_ID` is set. See [peaqOS CLI](/peaqos/cli) for the full command reference. CLI and SDK wallet helpers live on [Wallets (OWS)](/peaqos/wallets). ### Package ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @peaqos/peaq-os-sdk viem dotenv ``` OWS wallet helpers (`createWallet`, `importWallet`, `PeaqosClient.fromWallet`, …) work out of the box; `@open-wallet-standard/core` is bundled as a regular dependency of `@peaqos/peaq-os-sdk`. See [Wallets (OWS)](/peaqos/wallets) for the full surface. `dotenv` is optional but recommended: `PeaqosClient.fromEnv()` reads from `process.env`, so `import "dotenv/config"` at the top of your entry file is the simplest way to load `.env`. ### Language + runtime * Node.js ≥ 22 * TypeScript ≥ 5 * Package exports both ESM and CJS builds; no bundler workarounds required. ### Imports ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; ``` Full method reference on [SDK JS](/peaqos/sdk-reference/sdk-js). Error class hierarchy on [errors](/peaqos/sdk-reference/errors). ### Package ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python3 -m venv .peaq-os source .peaq-os/bin/activate pip install "peaq-os-sdk>=0.6.0" python-dotenv # Optional: include the OWS wallet helpers (createWallet, importWallet, …) pip install "peaq-os-sdk[ows]>=0.6.0" ``` `python-dotenv` is optional but recommended: `PeaqosClient.from_env()` reads from dotenv, so `load_dotenv()` at the top of your entry file is the simplest way to load .env. ### Language + runtime * Python ≥ 3.12 (0.6.0 and newer do not import on 3.10 or 3.11) * `web3.py` pulled in automatically. * Virtualenv strongly recommended. The commands above set one up. ### Imports ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient ``` Full method reference on [SDK Python](/peaqos/sdk-reference/sdk-python). Error class hierarchy on [errors](/peaqos/sdk-reference/errors). ### Workspace ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 ``` ### Build ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} source /opt/ros/jazzy/setup.bash # Docker image: source /opt/ros/humble/setup.bash # Native host only: python3 -m pip install -r requirements.txt colcon build --packages-select peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash ``` ### Run ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cp peaq_ros2_examples/config/peaq_robot.example.yaml peaq_ros2_examples/config/peaq_robot.yaml # Edit peaq_ros2_examples/config/peaq_robot.yaml and set peaq_os.enabled: true. ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml ``` Full method mapping on [ROS 2 SDK reference](/peaqos/sdk-reference/ros2/overview). End-to-end commands on [ROS 2 machine runtime](/peaqos/guides/ros2-machine-runtime). ## Environment variables | Variable | Required | Default | Purpose | | :-------------------------------- | :-------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PEAQOS_RPC_URL` | Yes | n/a | peaq chain RPC endpoint | | `PEAQOS_PRIVATE_KEY` | Yes | n/a | Owner wallet private key (hex, `0x...`) | | `PEAQOS_NETWORK` | CLI only | n/a | `mainnet` or `testnet`. Selects address + URL defaults for the CLI's `peaqos init`. SDK clients ignore it. | | `PEAQOS_GAS_STATION_URL` | CLI only | n/a | Faucet base URL. Set to `https://depinstation.peaq.xyz` for the public peaq-hosted Gas Station (the older `depinstation.peaq.network` host has an expired TLS certificate). Required for the `peaqos activate` funding step unless `--skip-funding` is set or the wallet already meets the gas threshold. | | `TOKENOMICS_DEPLOYMENT_ID` | CLI only | n/a | Economics 2.0 deployment record: `peaq-mainnet` or `agung-2026-08-28`. Written by `peaqos init`. Required by `peaqos activate`, `peaqos machine`, `peaqos monetize`, and `peaqos monetize provision run`. The SDKs do not read it: pass `tokenomics20: { deploymentId }` (JS) or `tokenomics20=Tokenomics20Config(deployment_id=...)` (Python) to the constructor instead. Contract addresses travel with the record; they are never environment variables. | | `OWS_PASSPHRASE` | No | n/a | OWS vault passphrase used by the SDK and CLI wallet helpers (`createWallet`, `importWallet`, `exportWallet`, …). The SDK raises `PeaqosError` if neither this nor the inline `passphrase` arg is set (no interactive prompt). The CLI reads it when `PEAQOS_OWS_WALLET` is set, prompting interactively if unset. See [Wallets (OWS)](/peaqos/wallets). | | `IDENTITY_REGISTRY_ADDRESS` | Yes | n/a | Identity Registry contract. See [peaq mainnet contracts](#peaq-mainnet-contracts) for the canonical address. | | `IDENTITY_STAKING_ADDRESS` | Yes | n/a | Identity Staking contract | | `EVENT_REGISTRY_ADDRESS` | Yes | n/a | Event Registry contract | | `MACHINE_NFT_ADDRESS` | Yes | n/a | Machine NFT (ONFT) contract | | `DID_REGISTRY_ADDRESS` | Yes | n/a | DID Registry precompile address (`0x...0800` on every peaq runtime) | | `BATCH_PRECOMPILE_ADDRESS` | Yes | n/a | Batch precompile for multi-call bonding (`0x...0805` on every peaq runtime) | | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | No | n/a | `MachineAccountFactory`. Required only for smart-account deploy / predict. | | `MACHINE_NFT_ADAPTER_ADDRESS` | No | n/a | `MachineNFTAdapter` (LayerZero ONFT adapter). Required only for `bridge_nft` / `bridgeNft` when `source="peaq"`. | | `PEAQOS_OWS_WALLET` | CLI only | n/a | Active OWS vault wallet name. When set, the CLI loads the wallet via `OWS_PASSPHRASE` and skips `PEAQOS_PRIVATE_KEY`. Set by `peaqos wallet use` or `peaqos init` (`wallet` path). | | `PEAQOS_MCR_API_URL` | Set for mainnet | `http://127.0.0.1:8000` | Tokenomics 1.0 MCR API base URL, read by `peaqos qualify` and `peaqos show`. Set to `https://mcr.peaq.xyz` to read from the public peaq-hosted MCR. Ignored by `peaqos monetize`, which resolves its endpoint from `TOKENOMICS_DEPLOYMENT_ID`. | | `PEAQOS_MANIFEST_REPO_URL` | Monetize only | n/a | Base URL of the provisioning-manifest repo, read by `peaqos monetize provision` (or passed as `--manifest-repo`). There is no default and it is never hardcoded; get the current peaq-published base URL from the peaq team. Missing it exits `3`. | | `PEAQOS_MACHINE_WALLET_ADDRESS` | Monetize | n/a | Machine wallet address used as the provisioning wallet context by `peaqos monetize provision`. Since CLI 0.0.8 `--machine` takes a decimal machine ID or `did:peaq:` (address DIDs are rejected), so this variable is the only way to supply the payout address. Unset it before provisioning a different machine. This value feeds the manifest's commission/payout context. | | `PEAQOS_ORCHESTRATION_URL` | Scale only | n/a | Machine Markets orchestrator base URL. The canonical peaq-managed endpoint is `https://orchestration.peaq.xyz`. Read by the SDK (`client.orchestration`) and CLI (`peaqos scale ...`). | | `PEAQOS_API_KEY` | SDK, optional | n/a | Platform API key sent as `x-api-key` on `client.orchestration` requests. Required only when the orchestrator runs with `PEAQOS_REQUIRE_API_AUTH=true`. | | `PEAQOS_ORCH_API_KEY` | CLI, optional | n/a | Same platform API key as `PEAQOS_API_KEY` but under the CLI-specific name. `peaqos init` writes this; `peaqos scale ...` reads it. Set both names to the same value if you use the CLI and SDK in the same env. | | `PEAQOS_TELEMETRY` | No | on | Anonymous SDK usage telemetry toggle. Set to `0` to disable. SDK only (JS + Python); the CLI sends none. See [Telemetry](#telemetry). | | `DO_NOT_TRACK` | No | n/a | Cross-vendor opt-out convention: set to `1` to disable SDK telemetry (same effect as `PEAQOS_TELEMETRY=0`). | Full reference with defaults on [SDK JS environment](/peaqos/sdk-reference/sdk-js) and [SDK Python environment](/peaqos/sdk-reference/sdk-python). ### Telemetry The JavaScript and Python SDKs emit anonymous usage telemetry — which SDK operations run, tagged with a random install ID and with IP collection off (via PostHog, EU-hosted) — to help prioritize the roadmap. No private keys, wallet secrets, or machine data are sent. Telemetry is on by default; disable it with `PEAQOS_TELEMETRY=0` or the cross-vendor `DO_NOT_TRACK=1`. The CLI sends none. ## Public RPC endpoints Use any of the endpoints below for `PEAQOS_RPC_URL`. QuickNode is the primary set; OnFinality and PublicNode are fallbacks. All of them accept EVM JSON-RPC calls. For private dedicated endpoints, see the [QuickNode](https://www.quicknode.com/guides/quicknode-products/how-to-use-the-quicknode-dashboard#create-a-quicknode-endpoint) and [OnFinality](https://documentation.onfinality.io/support/the-enhanced-api-service) guides. Full list on [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq). ```bash peaq mainnet theme={"theme":{"light":"github-light-default","dark":"github-dark"}} https://quicknode1.peaq.xyz https://quicknode2.peaq.xyz https://quicknode3.peaq.xyz # Secondary / fallback https://peaq.api.onfinality.io/public https://peaq-rpc.publicnode.com ``` ```bash agung testnet theme={"theme":{"light":"github-light-default","dark":"github-dark"}} https://peaq-agung.api.onfinality.io/public https://wss-async-agung.peaq.xyz ``` ## peaq mainnet contracts Core contracts are UUPS upgradeable proxies; treat the addresses as the current proxy pointers. For the architecture diagram and what each contract is responsible for, see [Smart contracts](/peaqos/concepts/contracts). ### Economics 2.0 **Minimum versions: `@peaqos/peaq-os-sdk` 0.7.0 and `peaq-os-sdk` 0.7.1 (both 2026-09-11).** Earlier releases carry the `MachineBridgeAdapter` address from before the 2026-09-08 re-point and fail every `peaq-mainnet` write at preflight with `PEER_MISMATCH`. CLI 0.0.8 resolves 0.7.1 on a fresh install; upgrade an existing environment with `pip install -U peaq-os-sdk`. Selected by `TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet` (CLI) or `deploymentId: "peaq-mainnet"` (SDKs). These are **not** environment variables: the addresses ship inside the SDK's deployment record and are verified against `InfoDesk.peer(role)` before every write. Listed here so you can check what `peaqos whoami` resolves. | Contract | Address | | :----------------------------- | :------------------------------------------- | | InfoDesk | `0x6C7426ada37212D94477Ac4CFcA43A38177D6A82` | | MachineRegistry | `0x64b93Cc29b251fAFa83BD110cDB1C24207f85536` | | MachineStateAndSync | `0xcD1917FB2a56459AcA34FDD118e08776dE0890f2` | | MachineSubscription | `0x9e37AD189c334C92e6B8a812Ca4c02f35Ac43895` | | CrossChainMirror | `0x71DCB313977d6884212395505f081a2991Bfe8E5` | | MachineBridgeAdapter | `0x791087c35484b567c53f392a624d2e4BaDcC53DF` | | SubscriptionTokenProvisionPool | `0x7088Cf400081428a41a4287B75AC633071c0e92A` | The full 13-contract set, including PriceOracle and TrustValidatorStaking, is on [Smart contracts](/peaqos/concepts/contracts#economics-2-0-peaq-mainnet). ### Tokenomics 1.0 (required by the SDK constructor) | Variable | Address | | :-------------------------- | :------------------------------------------------------------------- | | `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` | | `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` | | `EVENT_REGISTRY_ADDRESS` | `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` | | `MACHINE_NFT_ADDRESS` | `0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9` | | `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` (peaq DID precompile) | | `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` (peaq batch precompile) | ### Optional Set only if you use the corresponding SDK method. | Variable | Address | Needed for | | :-------------------------------- | :------------------------------------------- | :------------------------------------------------------------------ | | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x4A808d5A90A2c91739E92C70aF19924e0B3D527f` | `deploySmartAccount` / `getSmartAccountAddress` (ERC-4337) | | `MACHINE_NFT_ADAPTER_ADDRESS` | `0x9AD5408702EC204441A88589B99ADfC2514AFAE6` | `bridgeNft` / `bridge_nft` when `source="peaq"` (LayerZero V2 ONFT) | ## Base mainnet contracts Needed when bridging **into** peaq from Base. Pass `baseNftAddress` / `base_nft_address` to the bridge method. | Contract | Address | | :-------------------------------- | :------------------------------------------- | | `MachineNFTBase` (LayerZero ONFT) | `0xee8A521eA434b11F956E2402beC5eBfa753Babfa` | ## Agung testnet contracts Use these to point the SDK or CLI at agung. Precompile addresses are identical to mainnet (same fixed slots on every peaq runtime). ### Economics 2.0 Selected by `TOKENOMICS_DEPLOYMENT_ID=agung-2026-08-28` or `deploymentId: "agung-2026-08-28"`. Not environment variables. | Contract | Address | | :----------------------------- | :------------------------------------------- | | InfoDesk | `0x72b66AF120c55371cA6e5Ce38D4e10dF1bA30Bb1` | | MachineRegistry | `0x538da35489B1F0035799a8351E67835ae4ABb377` | | MachineStateAndSync | `0xA7D33726232aae30622643a18047f0Ca3e159da9` | | MachineSubscription | `0xaDCc5dD8CD57E3198B881c3388F25D2fD69B5aDc` | | CrossChainMirror | `0xd5F8d5944c5d488B1cD698D642FC8156a8CD0c0c` | | MachineBridgeAdapter | `0x0406e7bE522AEa626aee54f6258D415215e59F76` | | SubscriptionTokenProvisionPool | `0x8A3692Ad0AcF79BB89413514Af9957d95032e19B` | ### Tokenomics 1.0 (required by the SDK constructor) | Variable | Address | | :-------------------------- | :------------------------------------------------------------------- | | `IDENTITY_REGISTRY_ADDRESS` | `0x9E9463a65c7B74623b3b6Cdc39F71be7274e5971` | | `IDENTITY_STAKING_ADDRESS` | `0x55f336714aDb0749DbFE33b057a1702405564E3d` | | `EVENT_REGISTRY_ADDRESS` | `0x2DAD8905380993940e340C5cE6d313d5c2780040` | | `MACHINE_NFT_ADDRESS` | `0xB41C2A4f1c19b6B06beaAce0F5CD8439e77C4b1c` | | `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` (peaq DID precompile) | | `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` (peaq batch precompile) | ### Optional | Variable | Address | Needed for | | :-------------------------------- | :------------------------------------------- | :--------------------------------------------------------- | | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x65a4DfEB799dFf8CF15f13816d648a7805d6b1F9` | `deploySmartAccount` / `getSmartAccountAddress` (ERC-4337) | | `ADMIN_FLAGS_ADDRESS` | `0x4181a2Aa34aFb247450FfcBd65be5aBD4Cbee658` | MCR API server (negative-flag + trust-override reads) | **Bridging cannot be exercised on agung.** LayerZero deprecated the agung endpoint (EID `40299`): DVNs and executors are no longer active, so `bridgeNft` / `bridge_nft` cannot relay. No agung `MachineNFTAdapter` appears in peaq deployment records. Leave `MACHINE_NFT_ADAPTER_ADDRESS` unset on agung and test the bridge on peaq mainnet ↔ Base mainnet only. ## Troubleshooting The OTP from your authenticator app was wrong or expired. Generate a fresh code and retry. See full [error code reference](/peaqos/sdk-reference/errors). The faucet throttles owners after repeated bad OTPs. Wait out the lockout window and retry. See [error code reference](/peaqos/sdk-reference/errors). The faucet enforces a per-IP and per-wallet cap. Retry after the cooldown. See [error code reference](/peaqos/sdk-reference/errors). The same owner address has hit the daily funding cap. Wait 24 hours or contact support. See [error code reference](/peaqos/sdk-reference/errors). Set `PEAQOS_RPC_URL` in your environment to a valid peaq chain RPC endpoint (e.g., `https://peaq.api.onfinality.io/public`). Without it, `PeaqosClient.fromEnv()` raises a `ValidationError`. See [Public RPC endpoints](#public-rpc-endpoints) for the full list. Full error taxonomy on [errors](/peaqos/sdk-reference/errors). # peaqOS overview Source: https://docs.peaq.xyz/peaqos/overview The machine economy runs on peaqOS. peaqOS is the omnichain machine layer. It gives robots and machines an on-chain identity, a credit rating, and the infrastructure to earn, transact, and become investable across chains. ## peaqOS is omnichain peaqOS contracts hold the canonical record of identity and credit on peaq chain. Any chain can query them; any chain can consume them. Your machine gets a peaqID and Machine NFT on peaq chain. Revenue and activity events feed into a Machine Credit Rating. Any chain can query identity and credit from the MCR API, including by Solana address. Machine NFTs bridge between peaq, Base, and Solana via LayerZero V2, and peaq identity and staking state is mirrored to Solana. See [Omni-chain](/peaqos/concepts/omni-chain). ## Functions Put your machine on-chain in one transaction: peaqID, Machine NFT, and a tier bond under [Economics 2.0](/peaqos/concepts/economics-2-0). Self-owned and machine-owned, operator-controlled fleet patterns both supported. Read the [Activate function](/peaqos/functions/activate). Machine Credit Rating built from a machine's revenue and activity history. The MCR API exposes ratings to any chain. Read the [Qualify function](/peaqos/functions/qualify). Pair an AI agent to your machine, set a delegation policy (spend limits, allow/denylist), and let it discover and consume services through the Machine Markets API. Read the [Scale function](/peaqos/functions/scale). Sell the data your machine generates — signed, encrypted, and verifiable. Buyers pay for access to exactly what they need. Read the [Stream function](/peaqos/functions/stream). Put your machine to work as a compute provider and earn to its machine wallet: opt in, provision from a manifest, report presence. Read the [Monetize function](/peaqos/functions/monetize). Prove a machine is real via hardware attestation and trusted third parties. Read the [Verify function](/peaqos/functions/verify). Fractionalize your machine into an investable asset via ERC-3643. Read the [Tokenize function](/peaqos/functions/tokenize). ## Two onboarding patterns Owner equals operator. One machine, one wallet, one `activateMachine` call. Each machine signs its own activation and owns its NFT; the operator is recorded as DID controller and runs lifecycle, renewals, and DID updates for the fleet. ## Keep going TypeScript, Python, and ROS 2 signatures, parameters, returns, errors. Run peaqOS from robot-native ROS 2 services with local key custody. Query MCR and machine profiles from any chain. Onboarding, fleets, event submission. # peaqOS AI Source: https://docs.peaq.xyz/peaqos/peaqos-ai Use the peaqOS agent skill to onboard machines, query MCR, and run fleet workflows from any AI coding agent. peaqOS ships one curated agent skill — `peaqos` — that turns your AI agent into a peaqOS onboarding co-pilot. It's a [skill.md](https://agentskills.io/specification)-spec skill with first-class adapters for Claude Code, Cursor, and Windsurf, plus a manual upload path for any other runtime that reads the standard. Under the hood it drives the [`peaqos` Python CLI](/peaqos/cli) — anything you can do at the terminal, the skill can do for you with the right questions asked first. ## What the skill does Trigger it in your agent (`/peaqos` in Claude Code, or just describe what you want elsewhere) and it picks the right mode based on what you ask: | Mode | When it kicks in | | :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Demo** | First-time tour on agung testnet — full onboarding in \~15 minutes with explanations at every step. | | **Real onboarding** | Asks five questions about your machine and deployment, recommends a self-managed or operator-managed setup, then runs the CLI to activate the machine and submit the first event. | | **Fleet management** | Pulls MCR scores for an operator's fleet, surfaces machines with low or no rating, submits heartbeat events. | | **Machine Markets (Scale)** | Onboards a machine to the orchestrator, pairs an agent with a delegation policy, runs market search, places an order, and confirms or disputes delivery. Drives `peaqos scale ...` end-to-end. | | **Troubleshooting** | Diagnoses common failures — funding, activation, MCR lag, key mismatches — and walks you through the fix. | The skill adapts its tone to your background: concise for developers, plain English with narrated steps for non-technical operators. ## Install Two pieces. The CLI does the actual work; the skill is the orchestration layer your agent loads. ### 1. Install the CLI ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python3 -m venv .peaqos-env source .peaqos-env/bin/activate pip install 'peaq-os-cli[ows]<0.0.8' 'peaq-os-sdk<0.6.0' peaqos --version ``` **Pin the CLI below 0.0.8 for now.** The `peaqos` skill (`@peaqos/skills` 0.1.0) was last aligned with CLI 0.0.6 and drives the Tokenomics 1.0 `peaqos activate --doc-url ... --data-api ...` flow. CLI 0.0.8 (2026-09-04) replaced that command with the one-transaction [Economics 2.0 activation](/peaqos/functions/activate) and rejects those flags, so the skill's onboarding mode breaks against a fresh `pip install peaq-os-cli`. The pin comes off when the skill updates. Pin the SDK as well: `peaq-os-cli<0.0.8` alone resolves to CLI 0.0.7 with SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError` in `MonetizationConfig`. CLI 0.0.7 with SDK 0.5.0 needs Python 3.11 or newer; 0.0.8 needs 3.12. ### 2. Add the skill to your agent ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Auto-detect (Claude Code, Cursor, or Windsurf) npx skills add peaqnetwork/peaq-os-skills # Or target a specific runtime npx skills add peaqnetwork/peaq-os-skills --agent claude-code npx skills add peaqnetwork/peaq-os-skills --agent cursor npx skills add peaqnetwork/peaq-os-skills --agent windsurf ``` The installer ships adapters for Claude Code, Cursor, and Windsurf, auto-detecting the agent it finds on disk (or prompting if multiple are installed). Invoke `/peaqos` (Claude Code) or just describe what you want (e.g. *"onboard my machine to peaqOS"*) in Cursor or Windsurf. For ChatGPT, Claude Projects, custom GPTs, or anywhere you can't run a local CLI, clone the repo and upload `AGENT-PROMPT.md` as the system prompt with `knowledge/` and `GUIDE.md` as knowledge sources: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-os-skills ``` ## LLM context files Separate from the curated skill, Mintlify auto-generates machine-readable bundles of these docs. Point any agent at one of: | File | URL | Use when | | :-------------- | :------------------------------------------------------------------------- | :----------------------------------------------------------------------- | | `llms.txt` | [https://docs.peaq.xyz/llms.txt](https://docs.peaq.xyz/llms.txt) | Model has a modest context window or you want a compact index with links | | `llms-full.txt` | [https://docs.peaq.xyz/llms-full.txt](https://docs.peaq.xyz/llms-full.txt) | Model has a large context window and you want the full docs inline | | `skill.md` | [https://docs.peaq.xyz/skill.md](https://docs.peaq.xyz/skill.md) | Agent needs a doc-derived capabilities spec rather than prose docs | These are doc context, not a substitute for the curated `peaqos` skill — that one knows the onboarding flow, decision tree, and recovery paths; the auto-generated bundle just knows what's on the page. ## Editor setup (docs context) Wire the peaq docs into your editor of choice. Independent of the `peaqos` skill — useful any time you want the agent to ground answers in current docs. Open **Cursor Settings → Features → Docs**, click **Add new doc**, and paste: ``` https://docs.peaq.xyz/llms-full.txt ``` Reference peaq in chat with `@docs` → peaq. Windsurf has no persistent docs store. Paste into Cascade (`Cmd+L`) per chat: ``` @docs:https://docs.peaq.xyz/llms-full.txt ``` Add the Mintlify-hosted MCP server to `.mcp.json`: ```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}} { "mcpServers": { "peaq-docs": { "url": "https://docs.peaq.xyz/mcp" } } } ``` Or one-shot install with the Mintlify CLI: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npx @mintlify/mcp add docs.peaq.xyz ``` Same MCP URL: `https://docs.peaq.xyz/mcp`. For custom GPTs or Claude Projects, upload `llms-full.txt` as a knowledge source. ## See also Chain-level prompting tips, Cursor Projects, peaq SDK prompt patterns. The ground truth the skill calls into via the CLI. # Errors Source: https://docs.peaq.xyz/peaqos/sdk-reference/errors Error class hierarchy, Tokenomics 2.0 error families, 20 faucet codes, and on-chain revert names for the peaqOS SDK. Every error raised by the SDK extends a common base. Faucet-specific codes are surfaced on the error instance (`error.code`) so callers can branch without string matching. ## Class hierarchy ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PeaqosError ├── ValidationError ├── RuntimeError ├── ValueCapExceeded └── RateLimitExceeded ``` These four extend `PeaqosError` directly. Each function area adds its own family alongside them, also extending `PeaqosError`: `MonetizationError` (plus `MonetizationCompatibilityError` since 0.6.0), `ProvisioningError`, `StreamError`, `OrchestrationError`, and since 0.6.0 the Economics 2.0 family `TokenomicsConfigError`, `TokenomicsActivationError`, `TokenomicsUnsupportedError`, `TokenomicsIntegrationUnavailableError` (see [Tokenomics 2.0 errors](#tokenomics-2-0-errors)). So `catch (err instanceof PeaqosError)` covers every SDK error; catching `RuntimeError` alone will miss `ValueCapExceeded` and `RateLimitExceeded`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosError, // base ValidationError, // bad input RuntimeError, // chain or HTTP failure; carries optional `code` ValueCapExceeded, // operational cap hit (extends PeaqosError) RateLimitExceeded, // operational cap hit (extends PeaqosError) } from "@peaqos/peaq-os-sdk"; ``` ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PeaqosError ├── ValidationError ├── RpcError ├── ApiError ├── ValueCapExceeded └── RateLimitExceeded ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( PeaqosError, ValidationError, RpcError, # chain / web3 errors ApiError, # HTTP API errors (faucet, MCR API) ValueCapExceeded, RateLimitExceeded, ) ``` Python separates transport-layer failures into `RpcError` (chain) and `ApiError` (faucet + MCR API). JavaScript collapses both into `RuntimeError` with a `code` field. Since 0.6.0 the root also exports `TokenomicsConfigError`, `TokenomicsActivationError`, `TokenomicsPendingTransactionError` (a subclass of the activation error), `TokenomicsUnsupportedError`, and `TokenomicsIntegrationUnavailableError`; `peaq_os_sdk.monetization` adds `MonetizationCompatibilityError`, `MonetizationNetworkError`, `MonetizationResponseError`, `MonetizationTimeoutError`, and `MonetizationCancelledError`. All extend `PeaqosError`. *** ## Tokenomics 2.0 errors Raised by the Economics 2.0 surface (SDK 0.6.0+): activation, machine management, and the 2.0 monetization client. Codes are string literals on `err.code` in both SDKs; the contract a revert came from travels on `err.contract`, and reverts are decoded by 4-byte selector scoped to that contract's ABI. An unrecognised selector still surfaces as `CONTRACT_REVERTED` with the raw `revertData` / `revert_data`. | Class | Codes | Notes | | :--------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenomicsConfigError` | `DEPLOYMENT_UNKNOWN`, `DEPLOYMENT_UNAVAILABLE`, `DEPLOYMENT_NOT_CONFIGURED`, `ADDRESS_OVERRIDE_MISMATCH` (the CLI reports `DEPLOYMENT_NOT_CONFIGURED` as `TOKENOMICS_NOT_CONFIGURED`) | Deployment selection and the never-merged address overrides. Agung monetization raises `DEPLOYMENT_UNAVAILABLE`. | | `TokenomicsActivationError` | Preflight: `CHAIN_MISMATCH`, `CONTRACT_NOT_DEPLOYED`, `PEER_MISMATCH`, `SIGNER_UNAVAILABLE`, `NOT_FULL_MODE`, `INVALID_TIER`, `PRICE_NOT_AVAILABLE`. Quote and funds: `MACHINE_ID_MISMATCH`, `MAX_NET_PEAQ_EXCEEDED`, `MAX_USDT_EXCEEDED`, `INSUFFICIENT_BALANCE`, `APPROVAL_FAILED`, `ALLOWANCE_INSUFFICIENT`, `SLIPPAGE_EXCEEDED`. Reads: `MACHINE_NOT_FOUND`, `READ_FAILED`. Management: `NOT_OWNER_OR_CONTROLLER`, `NOT_MACHINE_OWNER`, `AUTHENTICATION_REWRITE_REQUIRED`, `MACHINE_RELOCATING`, `ERC721_INCORRECT_OWNER`, `ERC721_INSUFFICIENT_APPROVAL`, `ERC721_INVALID_APPROVER`, `ERC721_INVALID_OPERATOR`. Submission: `CONTRACT_REVERTED`, `EVENT_MISMATCH`, `STATE_MISMATCH`, `ABORTED`, and a pending result when the receipt does not arrive: JS `RECEIPT_UNAVAILABLE` (activation) or `TRANSACTION_PENDING` (management writes), Python `PENDING_TRANSACTION` (as `TokenomicsPendingTransactionError` with `.submitted`) | Attributes `code`, `contract`, `operation`, `solidityError` / `solidity_error`, `revertData` / `revert_data`, `transactionHash` / `transaction_hash`. Never resubmit after a pending or receipt-unavailable result; reconcile the hash. `PEER_MISMATCH` on `machineBridgeAdapter` against `peaq-mainnet` means an SDK older than `@peaqos/peaq-os-sdk` 0.7.0 / `peaq-os-sdk` 0.7.1 (the adapter was re-pointed on 2026-09-08); upgrade. | | `TokenomicsUnsupportedError` | `LEGACY_REGISTRATION_UNSUPPORTED` (`registerMachine`, `mintNft`, `tokenIdOf`), `SPONSORED_ACTIVATION_UNSUPPORTED` (`registerFor`, no replacement), `MACHINE_RELOCATION_UNAVAILABLE` (`bridgeNft`) | Deprecated entry points called in Tokenomics mode. Carries `operation` and `replacement`. | | `TokenomicsIntegrationUnavailableError` | `TOKENOMICS_INTEGRATION_UNAVAILABLE` | Carries `integration` (`legacy DID helpers`, `orchestration identity binding`; `events` and `MCR/query` were removed in JS 0.7.0 / Python 0.7.1, which enable both in Tokenomics mode) and `owningTicket` / `owning_ticket`. Raised before any HTTP, RPC, or signing. | | `MonetizationCompatibilityError` | `MONETIZATION_API_INCOMPATIBLE` | The 2.0 MCR's `/.well-known/peaq-monetization` signal is missing or does not match the deployment. `mcr-20.peaq.xyz` publishes the signal since 2026-09-05; you still see this against a self-hosted MCR that has not been upgraded, or when `TOKENOMICS_DEPLOYMENT_ID` names a different chain than the host serves. | | `MonetizationApiError` (JS) / `MonetizationError` (Python) | Adds `MACHINE_UNAVAILABLE` (503, the only retryable code) and `CHAIN_UNAVAILABLE` to the 1.0 table | Branch on `code`, never on the message. Timeouts and cancellations are separate classes: Python `MonetizationTimeoutError` (`MONETIZATION_TIMEOUT`) and `MonetizationCancelledError` (`MONETIZATION_CANCELLED`); JS `MonetizationTimeoutError` and `MonetizationCancellationError`, which carry no `code`. | Error messages pass through credential redaction (URL userinfo, secret query parameters, 32-byte hex). Revert data and transaction hashes are kept unredacted on the attributes. *** ## Faucet error codes All 20 codes the Gas Station can return from `POST /faucet/fund`, `POST /2fa/setup`, and `POST /2fa/confirm`. Each endpoint returns a subset. For example, `INVALID_OWNER_ADDRESS` and `QR_GENERATION_FAILED` only come from `/2fa/setup`. Codes surface as `RuntimeError.code` (JS) or `ApiError.code` (Python). | Code | Description | Retry | | :------------------- | :--------------------------------------------------- | :--------------------------------------------- | | `INVALID_2FA` | OTP rejected by the faucet | Yes: submit a fresh 6-digit code | | `2FA_NOT_CONFIGURED` | Owner has not completed `setup_faucet_2fa` | No: re-run setup | | `2FA_NOT_ACTIVE` | 2FA enrolled but not confirmed | No: call `confirm_faucet_2fa` with a valid OTP | | `2FA_LOCKED` | Too many invalid attempts; owner temporarily blocked | After lockout window | | Code | Description | Retry | | :-------------------------- | :---------------------------------- | :-------------------------------------------------------- | | `DUPLICATE_REQUEST` | Same `request_id` already in flight | No: use a different `request_id` or wait for prior result | | `REQUEST_ALREADY_PROCESSED` | Same `request_id` already resolved | No: read the prior result | | Code | Description | Retry | | :-------------------- | :---------------------------------- | :------------------------------------- | | `RATE_LIMITED` | Per-IP or per-wallet throttle hit | After cooldown | | `CAP_EXCEEDED_OWNER` | Daily per-owner funding cap reached | Next day | | `CAP_EXCEEDED_WALLET` | Daily per-target-wallet cap reached | Next day, or target a different wallet | | Code | Description | Retry | | :----------------------- | :-------------------------------------- | :------------------ | | `INVALID_PAYLOAD` | Request body malformed | No: fix the payload | | `INVALID_OWNER_ADDRESS` | `owner_address` not a recognized format | No | | `INVALID_TARGET_ADDRESS` | `target_wallet_address` not recognized | No | | `INVALID_CHAIN_ID` | `chain_id` not configured on the faucet | No | | `INVALID_REQUEST_ID` | `request_id` not a UUID | No | | Code | Description | Retry | | :---------------- | :------------------------------------ | :---- | | `TRANSFER_FAILED` | On-chain transfer reverted or stalled | Yes | | `CHAIN_RPC_ERROR` | Faucet's RPC call failed | Yes | | `INTERNAL_ERROR` | Faucet internal failure | Yes | | Code | Description | Retry | | :--------------------- | :--------------------------------------- | :--------------- | | `QR_NOT_FOUND` | QR image expired or never created | No: re-run setup | | `QR_EXPIRED` | QR retrieval attempted after \~2 min TTL | No: re-run setup | | `QR_GENERATION_FAILED` | Faucet could not generate the image | Yes | Full flow reference: [Gas Station concept](/peaqos/concepts/gas-station), [setupFaucet2FA](/peaqos/sdk-reference/sdk-js#setupfaucet2fa), [fund\_from\_gas\_station](/peaqos/sdk-reference/sdk-python#fund_from_gas_station). *** ## On-chain revert names (Tokenomics 1.0) These are the `IdentityRegistry`, `IdentityStaking`, `MachineNFT`, and `EventRegistry` reverts. Economics 2.0 reverts are decoded per contract into the `TokenomicsActivationError` codes above. Revert names the SDK translates to a friendly message and surfaces as `RuntimeError.code` (JS) or `RpcError.code` (Python). The contracts define more custom errors than this table; anything not listed surfaces as `code: "TX_REVERTED"` (Python) or `code: ""` with a generic `Transaction reverted: …` message (JS, when viem decodes the selector). | Revert | Raised by | Cause | | :---------------------------- | :----------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AlreadyRegistered` | `registerMachine` / `registerFor` / `register_machine` | Address already has a machine ID | | `InvalidMachineAddress` | `registerFor` | `machineAddress` is the zero address | | `InvalidAddress` | Staking / NFT flows | Caller passed the zero address where a real address is required | | `AmountZero` | Staking / bond flow | Bond amount is zero | | `AlreadyStaked` | Staking flow | Machine is already bonded/staked | | `MachineNotBonded` | `mintNft` / `mint_nft` | Caller's machine is not bonded on the IdentityRegistry | | `AlreadyMinted` | `mintNft` / `mint_nft` | An NFT is already minted for the machine | | `NotMachineOwner` | `mintNft` / `mint_nft` | Caller is not the owner of the machine | | `RecipientMustBeMachineOwner` | `mint_nft` (Python) | Operator-mint guard: recipient must be the registered machine owner. Python-only — the JS SDK surfaces this revert as `code: ""` via the viem decode path. | | `MachineNotFound` | `submitEvent` / `tokenIdOf` | machineId has no Identity record on the IdentityRegistry | | `MachineDeactivated` | `submitEvent` | The machine has been deactivated by the IdentityRegistry | | `NotAuthorizedSubmitter` | `submitEvent` | Caller is not authorized to submit events for this machine | | `InvalidEventType` | `submitEvent` | `eventType` is not `0` (revenue) or `1` (activity) | | `InvalidTrustLevel` | `submitEvent` | `trustLevel` is not `0`, `1`, or `2` | ## MCR API error codes Returned by `queryMcr` / `query_mcr`, `queryMachine` / `query_machine`, and `queryOperatorMachines` / `query_operator_machines` against the MCR API: `mcr.peaq.xyz` in legacy mode, the deployment's 2.0 server (`mcr-20.peaq.xyz`) in Tokenomics mode since JS 0.7.0 / Python 0.7.1. Surfaced as `RuntimeError.code` (JS) or `ApiError.code` (Python). In Tokenomics mode a malformed or non-canonical `machine_id`, or a response about a different machine or operator than requested, is `BAD_RESPONSE`. | Code | Cause | Retry | | :-------------------- | :--------------------------------------------------------------------- | :---------------------------------------- | | `NOT_FOUND` | The DID, machine, or token ID was not found by the MCR API (HTTP 404) | No: check the input | | `BAD_RESPONSE` | The MCR returned an unexpected payload shape | No: file an issue if it persists | | `HTTP_ERROR` | The MCR returned an unhandled non-2xx status | Maybe: surface the status code and decide | | `SERVER_ERROR` | The MCR returned 5xx | Yes, with backoff | | `SERVICE_UNAVAILABLE` | The MCR returned 503 (`Service not initialised` / `Chain unavailable`) | Yes, with backoff | | `TIMEOUT` | The HTTP request exceeded `timeoutMs` | Yes | | `NETWORK_ERROR` | Transport-level failure (DNS, TCP, TLS) | Yes | | `ABORTED` | The caller aborted the request via `AbortSignal` (JS only) | Caller's choice | ## OWS signing error codes Raised when transaction signing routes through an OWS vault wallet (`PeaqosClient.fromWallet` / `from_wallet` with `owsSigning=true`). The SDK normalises the upstream OWS error code into a typed SDK exception — only `INVALID_INPUT` becomes `ValidationError`; the other four become `PeaqosError` (or `RuntimeError` in JS) with the original OWS error preserved as `.cause`. | Code | Cause | Surfaces as | | :-------------------- | :--------------------------------------------- | :--------------------------------------- | | `WALLET_NOT_FOUND` | Vault wallet name / UUID does not exist | `PeaqosError` (Py) / `RuntimeError` (JS) | | `INVALID_PASSPHRASE` | Wrong vault passphrase | `PeaqosError` (Py) / `RuntimeError` (JS) | | `INVALID_INPUT` | Malformed transaction or sign-hash payload | `ValidationError(field="transaction")` | | `POLICY_DENIED` | Signing blocked by an OWS policy rule | `PeaqosError` (Py) / `RuntimeError` (JS) | | `CHAIN_NOT_SUPPORTED` | Transaction `chainId` is not configured in OWS | `PeaqosError` (Py) / `RuntimeError` (JS) | Constants exported from both SDKs as `OWS_ERROR_WALLET_NOT_FOUND`, `OWS_ERROR_INVALID_PASSPHRASE`, `OWS_ERROR_INVALID_INPUT`, `OWS_ERROR_POLICY_DENIED`, `OWS_ERROR_CHAIN_NOT_SUPPORTED`. JS additionally exports the `OwsSigningErrorCode` union type. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { OWS_ERROR_INVALID_PASSPHRASE, OWS_ERROR_CHAIN_NOT_SUPPORTED } from "@peaqos/peaq-os-sdk"; try { await client.bridgeNft({ tokenId, destination: "base" }); } catch (err) { if (err.cause?.code === OWS_ERROR_INVALID_PASSPHRASE) { /* prompt re-auth */ } if (err.cause?.code === OWS_ERROR_CHAIN_NOT_SUPPORTED) { /* OWS chain config bug */ } throw err; } ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( OWS_ERROR_INVALID_PASSPHRASE, OWS_ERROR_CHAIN_NOT_SUPPORTED, PeaqosError, ) try: client.bridge_nft(token_id=..., destination="base") except PeaqosError as err: code = getattr(err.__cause__, "code", None) if code == OWS_ERROR_INVALID_PASSPHRASE: ... # prompt re-auth if code == OWS_ERROR_CHAIN_NOT_SUPPORTED: ... # OWS chain config bug raise ``` ## SDK transaction sentinels Raised by the SDK's transaction helper around any contract call. Surfaced as `RuntimeError.code` (JS) or `RpcError.code` (Python). | Code | Cause | | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `WALLET_NOT_CONFIGURED` | Method requires a signer but none was configured. (JS only; the Python `PeaqosClient` constructor requires a `private_key` and surfaces this as `ValidationError`.) | | `TX_REVERTED` | The transaction reverted on-chain. The revert reason is in the message; revert names from the table above are decoded into `code` when the SDK recognizes them. | | `RECEIPT_AWAIT_FAILED` | Transaction was submitted but the SDK could not retrieve a receipt. The tx may still have landed; re-query by hash before retrying. | | `REGISTERED_EVENT_MISSING` | Receipt has no `Registered` event log. Indicates a contract/SDK ABI mismatch. | | `REGISTERED_EVENT_MALFORMED` | `Registered` event log was decoded but had unexpected fields. Same root cause as above. | | `INVALID_FEE_RESULT` | `quoteSend` on the LayerZero ONFT adapter returned a malformed `MessagingFee`. Raised inside `bridge_nft` (Python) before submission. | ## Faucet / HTTP envelope sentinels Raised by the SDK when a faucet or MCR response is reachable but unparseable. Surfaced as `ApiError.code` (Python). The JS SDK collapses these into the generic `RuntimeError` envelope path. | Code | Cause | | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `INVALID_RESPONSE` | Faucet returned a non-JSON body. | | `UNEXPECTED_RESPONSE` | Faucet returned JSON but the `status` / `code` / `data` envelope did not match the expected shape for the endpoint. | | `NETWORK_ERROR` | Transport-level failure during a faucet call (DNS, connection refused, TLS, timeout via `requests.RequestException`). Same sentinel as the MCR API table above. | ## Client-side error codes Raised by the SDK itself (not by a chain revert). Surfaced as `RuntimeError.code` (JS) or `ValidationError`/`RpcError` attributes (Python). | Code | Raised by | Cause | | :------------------------ | :------------------------------------------ | :------------------------------------------------------------------------------------- | | `MIN_BOND_INVALID_RESULT` | `registerMachine` / `registerFor` (JS only) | `IdentityRegistry.minBond()` returned a non-`bigint` value before submitting the bond. | *** ## Error handling patterns ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosError, ValidationError, RuntimeError, RateLimitExceeded, } from "@peaqos/peaq-os-sdk"; try { await client.fundFromGasStation(params, faucetUrl); } catch (err) { if (err instanceof ValidationError) { // fix caller input } else if (err instanceof RateLimitExceeded) { // back off } else if (err instanceof RuntimeError) { switch (err.code) { case "INVALID_2FA": // prompt for a fresh TOTP break; case "CAP_EXCEEDED_OWNER": case "CAP_EXCEEDED_WALLET": // surface cap messaging break; default: throw err; } } else { throw err; } } ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosError, ValidationError, ApiError, RpcError try: client.fund_from_gas_station( owner_address=owner, target_wallet_address=target, chain_id="peaq", two_factor_code=totp, faucet_base_url=faucet_url, ) except ValidationError: # fix caller input raise except ApiError as err: if err.code == "INVALID_2FA": # prompt for a fresh TOTP pass elif err.code in ("CAP_EXCEEDED_OWNER", "CAP_EXCEEDED_WALLET"): # surface cap messaging pass else: raise except RpcError: # retry with backoff raise ``` *** ## Related * [SDK JS](/peaqos/sdk-reference/sdk-js) * [SDK Python](/peaqos/sdk-reference/sdk-python) * [Gas Station concept](/peaqos/concepts/gas-station) * [Install page troubleshooting](/peaqos/install) # Monetize: Presence heartbeat Source: https://docs.peaq.xyz/peaqos/sdk-reference/heartbeat The machine-side heartbeat client: sign and push a presence heartbeat at a configurable interval, and query any machine's presence. The heartbeat client ships in `@peaqos/peaq-os-sdk` 0.5.0+ (JS/TS) and `peaq-os-sdk` 0.5.0+ (Python) under the `monetization` namespace. It talks to the dedicated peaqOS heartbeat service; the peaq-hosted instance is `https://heartbeat.peaq.xyz`, and the base URL stays configuration, never a constant. A machine that has [opted into monetization](/peaqos/sdk-reference/monetization-opt-in) reports that it is online by pushing a signed heartbeat to the heartbeat service at a regular interval. A valid heartbeat keeps the machine **online**; when heartbeats stop, the server marks it offline. The client does not enforce the opt-in state itself: start it only on an opted-in machine (check with `getMonetization` / `get_monetization`), and stop it when the machine opts out. ## Push, not poll The machine is a *client* of the heartbeat service: it pushes a signed heartbeat out. It does not host an endpoint that others poll, and it never serves its own availability. ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} sequenceDiagram participant M as Machine (SDK client) participant H as Heartbeat service loop every intervalMs M->>H: POST /api/v1/heartbeats {machineId, publicKey, sentAt, signature} H->>H: verify signature, store presence (online for the TTL) H-->>M: {accepted, online, lastHeartbeatAt} end ``` ## The 180-second TTL and the interval bound A valid heartbeat marks the machine online for a fixed presence TTL of **180 seconds**. Miss that window and the server flips the machine to offline. To stay online, the machine must heartbeat comfortably more often than the TTL, so the client caps the configured interval: | Constant | Value | Meaning | | :--------------- | :---- | :---------------------------------------------------------------------------------------------------------------- | | Presence TTL | 180 s | Server presence window (`PRESENCE_TTL_MS` in JS, `PRESENCE_TTL_SECONDS` in Python). | | Heartbeat margin | 60 s | Safety margin absorbing request latency plus one retry (`HEARTBEAT_MARGIN_MS` / `HEARTBEAT_MARGIN_SECONDS`). | | Max interval | 120 s | The maximum accepted interval, TTL minus margin (`MAX_HEARTBEAT_INTERVAL_MS` / `MAX_HEARTBEAT_INTERVAL_SECONDS`). | `start` rejects an interval that is zero, negative, or above the maximum. The exact interval within that bound is caller-supplied configuration. **Offline is server-derived.** The client never self-reports offline. Stopping the client simply stops heartbeating; the server observes the absence and lets presence lapse. Operator-initiated maintenance is expressed the same way: stop the client. ## Request-signing auth There is no bearer credential. The server authenticates each heartbeat by verifying the `signature` against the `publicKey` in the payload. * The machine identity **private key** is held only by your signer implementation. The client passes it a canonical message and receives a hex signature; the client never handles or logs the raw key. * `publicKey`, `signature`, `machineId`, and `sentAt` are not secret and are safe to log. The signature covers exactly this canonical message, byte for byte (two lines, LF-joined, no trailing newline): ``` machineId: {machineId} sentAt: {sentAt} ``` Build it with `buildCanonicalMessage` / `build_canonical_message`, the single place this format is defined. ## Quick start ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { HeartbeatClient } from "@peaqos/peaq-os-sdk"; // or: const heartbeat = client.monetization.createHeartbeatClient(); const heartbeat = new HeartbeatClient(); heartbeat.start({ baseUrl: "https://heartbeat.peaq.xyz", intervalMs: 30_000, // 0 < intervalMs <= 120_000 machineId: "machine-abc-123", publicKey: "0x04…", signer, // holds the machine identity private key onResult: (res) => { // res: { accepted, online, lastHeartbeatAt } console.log("online:", res.online); }, }); // on shutdown or maintenance: heartbeat.stop(); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.monetization import HeartbeatClient, HeartbeatConfig heartbeat = HeartbeatClient() heartbeat.start(HeartbeatConfig( base_url="https://heartbeat.peaq.xyz", interval_seconds=30, # 0 < interval_seconds <= 120 machine_id="machine-abc-123", public_key="0x04…", signer=signer, # holds the machine identity private key on_result=lambda res: print("online:", res.online), )) # on shutdown or maintenance: heartbeat.stop() ``` Each tick sends exactly `{ machineId, publicKey, sentAt, signature }` and parses `{ accepted, online, lastHeartbeatAt }`, surfaced through the optional result callback. ## Fire-and-forget A failed push, whether a network error or an `accepted: false` response, is logged and retried on the next tick. It never throws out of the timer and never blocks the machine's other operations. Every parsed response reaches the result callback, including rejections. A hung push cannot stall the loop either. In JS/TS the whole tick (signing plus push) is time-bounded to 30 seconds, or the interval if shorter. In Python the bound is per request: the transport's 10-second HTTP timeout, with signing cancelled cooperatively rather than forced. A server that accepts the connection but never responds is timed out, logged, and retried on the next tick like any other failure. `stop()` is the one exception: it cancels the timer, aborts any in-flight request (including one still signing), and is treated as expected cancellation, not a failure. No error log, no result callback, no retry. ## Presence query `checkPresence` / `check_presence` is a standalone one-shot query with no scheduler and no signing. Pass exactly one of `machineId` or `publicKey`: ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { checkPresence } from "@peaqos/peaq-os-sdk"; const presence = await checkPresence( { machineId: "machine-abc-123" }, { baseUrl: "https://heartbeat.peaq.xyz" }, ); // { online, machineId, publicKey, lastHeartbeatAt } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.monetization import PresenceCheckRequest, check_presence presence = check_presence( PresenceCheckRequest(machine_id="machine-abc-123"), base_url="https://heartbeat.peaq.xyz", ) # presence.online, .machine_id, .public_key, .last_heartbeat_at ``` When the machine is offline or unknown, `online` is `false` and `publicKey` / `lastHeartbeatAt` are `null`. ## Presence only The heartbeat payload is a pure presence signal: it carries no status field and no queue depth. Presence is binary under this API: online (a valid heartbeat within 180 seconds) or offline (TTL expired). Utilisation states such as `busy` are not represented, and maintenance is expressed by stopping the client. Do not add extra fields to the payload. ## API summary | Symbol | Role | | :-------------------------------------------------------------------- | :------------------------------------------------------- | | `HeartbeatClient` | `start(config)` / `stop()` presence scheduler. | | `HeartbeatSigner` | `sign(message)` returning hex; holds the private key. | | `buildCanonicalMessage` | Byte-for-byte canonical message builder. | | `HeartbeatTransport` | Wire and HTTP seam; swap it for tests or a custom stack. | | `checkPresence` | One-shot presence query. | | `PRESENCE_TTL_MS`, `HEARTBEAT_MARGIN_MS`, `MAX_HEARTBEAT_INTERVAL_MS` | Interval bounds. | ## Related * [Monetize function](/peaqos/functions/monetize): where the heartbeat fits in the flow * [Provisioning SDK reference](/peaqos/sdk-reference/provisioning): provision the provider node first * [Monetization opt-in API](/peaqos/api-reference/put-machine-monetization) # Monetize: Opt-in Source: https://docs.peaq.xyz/peaqos/sdk-reference/monetization-opt-in Toggle a machine's monetization on or off from the SDK, and read its state: the client for the MCR API's signed opt-in endpoint. **Economics 2.0 monetization is live on `peaq-mainnet` since 2026-09-05.** `@peaqos/peaq-os-sdk` 0.6.0 and `peaq-os-sdk` 0.6.0 talk to the 2.0 MCR at `https://mcr-20.peaq.xyz`, which now publishes the `/.well-known/peaq-monetization` signal (`tokenomics-2.0-monetization-v1`, chain `3338`, the `MachineRegistry` address). Reads were verified against the mirrored 2.0 machines on 2026-09-05 (`getMonetization` returns `PENDING` for a machine that never opted in); writes were not exercised. `agung-2026-08-28` still has no paired MCR (`DEPLOYMENT_UNAVAILABLE`). Tokenomics 1.0 machines stay on `mcr.peaq.xyz` through `@peaqos/peaq-os-sdk@0.5.0` / `peaq-os-sdk==0.5.0` (Python 3.11 or newer) with the [1.0 configuration](#tokenomics-1-0-sdk-0-5-0). An authorized signer turns a machine's monetization on or off with a signed EIP-191 toggle, and anyone can read the current state. The SDK builds the byte-exact canonical message, signs it, and handles the coded error envelope, so you never assemble the message by hand. State is stored off-chain in the MCR server; no on-chain transaction is made. Opting **in** requires the machine to be bonded and not deactivated (server-enforced). Opting **out** is always allowed. The read is public and doubles as the pre-provisioning check. ## Economics 2.0 (SDK 0.6.0+) The configuration is a deployment ID. The SDK resolves the MCR URL, chain ID, `MachineRegistry` address, and protocol version from the same deployment record that activation uses; callers cannot supply or override any of them. ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { optIn, optOut, getMonetization, eip191SignerFromPrivateKey, type MonetizationConfig, } from "@peaqos/peaq-os-sdk"; const config: MonetizationConfig = { deploymentId: "peaq-mainnet", signer: eip191SignerFromPrivateKey(privateKey), // the machine's current owner or DID controller key }; const machineId = 57896044618658097711785492504343953926634992332820282019728792003956564819975n; // Opt in (machine must be bonded and not deactivated). const state = await optIn(config, machineId); console.log(state.status); // "OPTED_IN" // Opt out (always allowed). await optOut(config, machineId); // Public read, no signer needed: the pre-provisioning check. const current = await getMonetization(config, machineId); if (current.status !== "OPTED_IN") { throw new Error("Opt in before provisioning"); } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.monetization import ( Eip191AccountSigner, MonetizationConfig, get_monetization, opt_in, opt_out, ) config = MonetizationConfig( deployment_id="peaq-mainnet", signer=Eip191AccountSigner.from_key(private_key), # the machine's current owner or DID controller key ) machine_id = 57896044618658097711785492504343953926634992332820282019728792003956564819975 # Opt in (machine must be bonded and not deactivated). state = opt_in(config, machine_id) print(state.status) # "OPTED_IN" # Opt out (always allowed). opt_out(config, machine_id) # Public read, no signer needed: the pre-provisioning check. current = get_monetization(config, machine_id) if current.status != "OPTED_IN": raise RuntimeError("Opt in before provisioning") ``` ### Machine key Every call takes the machine as a full-width `uint256` (`bigint` in JS, `int` in Python) or as `did:peaq:`. Address-form DIDs (`did:peaq:0x…`), decimal strings in JS, leading zeros, signs, and exponents are rejected locally before any HTTP. There is no lookup from a 1.0 address DID to a 2.0 machine ID. ### Who may sign The machine's **current `MachineRegistry` owner or DID controller**. A legacy machine-wallet key or a 1.0 EventRegistry operator key is not authorized (`403 UNAUTHORIZED_SIGNER`). The canonical message binds `registry: ` and `chain_id: 3338`. ### Configuration | Field | Notes | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | | `deploymentId` / `deployment_id` | `"peaq-mainnet"` only. `"agung-2026-08-28"` has no paired MCR and raises `TokenomicsConfigError` `DEPLOYMENT_UNAVAILABLE`. | | `signer` | Owner or controller key. Writes only; reads are unsigned. | | `fetch` (JS) / `transport` (Python) | Optional low-level HTTP adapter. It receives only SDK-built, deployment-bound URLs and cannot change the base URL. | The resolved values are readable (`api_base`, `registry`, `chain_id`, `api_version` in Python) but not settable. `apiBase`, `registry`, and `chainId` as constructor fields are gone; passing them is a `TypeError` in Python and a type error in TypeScript. ### Compatibility check Before every read and write the SDK fetches `GET {apiBase}/.well-known/peaq-monetization` and requires the exact protocol version (`tokenomics-2.0-monetization-v1`, exported as `TOKENOMICS_MONETIZATION_API_VERSION`), the canonical decimal chain ID, and the lowercased `MachineRegistry` address from the selected deployment. A write runs this check **before** invoking the signer. Missing, malformed, or mismatched signals raise `MonetizationCompatibilityError` (code `MONETIZATION_API_INCOMPATIBLE`). Not cached, cannot be bypassed. ### Retries, deadline, cancellation | Option | Default | Behaviour | | :------------------------------------------------------------ | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `maxUnavailableRetries` / `max_unavailable_retries` | `2` | Retries a write after `503 MACHINE_UNAVAILABLE`, the only retryable code. Each attempt re-signs with a fresh timestamp. Reads are not retried. | | `unavailableRetryDelayMs` / `unavailable_retry_delay_seconds` | `1000` ms / `1.0` s | Pause before each retry. | | `timeoutMs` / `timeout_seconds` | `30000` ms / `30.0` s | One end-to-end deadline covering validation, the compatibility check, signing, retries, and response parsing. Retries do not reset it. Raises `MonetizationTimeoutError`. | | `signal` / `cancel` | none | Caller abort. Raises `MonetizationCancellationError` (JS) / `MonetizationCancelledError` (Python). | Python passes these through `GetMonetizationOptions` and `SetMonetizationOptions`. ### The response | Field | Notes | | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | `machineId` / `machine_id` | The machine ID, parsed losslessly. On the wire it is a canonical decimal **string**; a JSON number is rejected. | | `status` | `"PENDING"`, `"OPTED_IN"`, or `"OPTED_OUT"`. `PENDING` means never opted in or out. 2.0 machines start at `PENDING`; 1.0 decisions are not imported. | | `signer` | Address that authorized the current state; `null`/`None` only when `PENDING`. | | `updatedAt` / `updated_at` | Unix seconds of the last change; `null`/`None` only when `PENDING`. | ### Errors | Class (JS / Python) | Code | Meaning | | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TokenomicsConfigError` | `DEPLOYMENT_UNKNOWN`, `DEPLOYMENT_UNAVAILABLE`, `DEPLOYMENT_NOT_CONFIGURED` | Bad or missing deployment; agung has no monetization | | `MonetizationCompatibilityError` | `MONETIZATION_API_INCOMPATIBLE` | The `/.well-known/peaq-monetization` signal is missing or does not match the deployment | | `MonetizationApiError` / `MonetizationError` | `UNAUTHORIZED_SIGNER`, `MACHINE_NOT_BONDED`, `MACHINE_DEACTIVATED`, `STALE_SIGNATURE`, `MACHINE_UNAVAILABLE` (503, retryable), `INVALID_REQUEST`, … | Server-side coded envelope; branch on the code, never the message. Full table on the [PUT endpoint page](/peaqos/api-reference/put-machine-monetization#error-responses) | | `MonetizationNetworkError`, `MonetizationResponseError`, `MonetizationTimeoutError`, `MonetizationCancellationError` / `MonetizationCancelledError` | | Transport, malformed response, deadline, caller abort | | `MonetizationValidationError` | | Rejected machine key or option locally | Re-sending the identical signed request surfaces `STALE_SIGNATURE`; re-signing the same state with a newer timestamp succeeds, so the toggle is idempotent to retry. ## Tokenomics 1.0 (SDK 0.5.0) The previous client targets `mcr.peaq.xyz` and serves machines registered through `IdentityRegistry`. Available by pinning `@peaqos/peaq-os-sdk@0.5.0` / `peaq-os-sdk==0.5.0` (Python 3.11 or newer), or from `peaq-os-cli<0.0.8` pinned together with `peaq-os-sdk<0.6.0`. The CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError`. ```ts JavaScript (0.5.0) theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const config: MonetizationConfig = { apiBase: "https://mcr.peaq.xyz", registry: IDENTITY_REGISTRY_ADDRESS, chainId: 3338n, signer: eip191SignerFromPrivateKey(privateKey), // machine wallet, owner, or on-chain operator key }; const state = await optIn(config, 42n); ``` ```python Python (0.5.0) theme={"theme":{"light":"github-light-default","dark":"github-dark"}} config = MonetizationConfig( api_base="https://mcr.peaq.xyz", registry=IDENTITY_REGISTRY_ADDRESS, chain_id=3338, signer=Eip191AccountSigner.from_key(private_key), # machine wallet, owner, or on-chain operator key ) state = opt_in(config, 42) ``` In 1.0 the signed message binds the `IdentityRegistry` address, the authorized signers are the machine wallet, the owner, or the on-chain operator, `getMonetization` also accepts `did:peaq:0x
`, and machine IDs fit a JS `number` range but the SDK still uses `bigint`. `registry` plus `chainId` are the domain binding: a signature made against the wrong pair recovers a non-authorized address and the server rejects it with `403 UNAUTHORIZED_SIGNER`; `verifyLocalRecovery` / `verify_local_recovery` recovers the signer locally so you catch a wrong key before a write. ## From the terminal The CLI wraps this surface as [`peaqos monetize opt-in | opt-out | status`](/peaqos/cli#peaqos-monetize); all signing and HTTP stay in the SDK. ## Related * [Monetize function](/peaqos/functions/monetize): where opt-in fits in the flow * [Provisioning SDK reference](/peaqos/sdk-reference/provisioning): the step after opting in * [PUT /machine/\{key}/monetization](/peaqos/api-reference/put-machine-monetization): the 1.0 wire contract, canonical message, and error codes * [Economics 2.0](/peaqos/concepts/economics-2-0): what changed and what is live # Orchestration (JavaScript) Source: https://docs.peaq.xyz/peaqos/sdk-reference/orchestration-js client.orchestration.* namespace on the JS/TS SDK — machines, agent pairings, machine agents, runtime endpoints, skills, market services, market search. The JS/TS SDK exposes the [Machine Markets API](/peaqos/api-reference/machine-markets-overview) as a typed namespace on the existing `PeaqosClient`. Same client, additive surface. The flat method layout matches the HTTP API one-to-one. ## Setup ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import 'dotenv/config'; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); const { items } = await client.orchestration.listMachines({ limit: 20 }); ``` Touching `client.orchestration` without `orchestrationUrl` configured throws `OrchestrationConfigError`. The namespace lazy-initialises and caches per client. ## Configuration | Field | Env var (via `fromEnv()`) | Notes | | :----------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------- | | `orchestrationUrl` | `PEAQOS_ORCHESTRATION_URL` | Required to use the namespace. Must parse as `http:` or `https:`. Non-loopback `http://` triggers a warning. | | `apiKey` | `PEAQOS_API_KEY` | Required for platform-auth calls. Sent as `x-api-key`. | Constructor form when not using env: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} new PeaqosClient({ rpcUrl, privateKey, contracts, orchestrationUrl: "https://orchestration.peaq.xyz", apiKey: "...", }); ``` Transport constants: * `API_BASE_PATH = "/api/v1"`, prepended to every path. * Timeouts: 30 s GET, 60 s POST/PATCH/PUT/DELETE. * A `peaq-os-sdk-js/…` `User-Agent` header. No retry, no exponential backoff. Six auto-paginated iterators are available; see [Pagination](#pagination) below. ## Common envelopes ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type ListResponse = { readonly items: readonly T[]; readonly nextCursor?: string; // omitted when there is no next page }; type ItemResponse = { readonly item: Readonly; }; type ListQuery = { readonly limit?: number; // 1..500, default 100 readonly cursor?: string; // opaque, omit on first page }; ``` `204 No Content` resolves to `void`. `validateListQuery` rejects `limit < 1` or `limit > 500` with `OrchestrationValidationError` before the HTTP call. ## Machines ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listMachines(options?: { cursor?: string; limit?: number; }): Promise> client.orchestration.createMachine(params: CreateMachineRequest) : Promise> client.orchestration.getMachine(machineId: string) : Promise> client.orchestration.updateMachine(machineId: string, params: UpdateMachineRequest) : Promise> client.orchestration.archiveMachine(machineId: string): Promise ``` `Machine` shape: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type Machine = { readonly id: string; readonly displayName: string; readonly status: "draft" | "active" | "degraded" | "blocked" | "archived"; readonly ownerId: string; readonly identityRef?: string | null; readonly identityProof?: { readonly method: "eip191"; readonly identityRef: string; readonly signerAddress: string; readonly challengeId: string; readonly verifiedAt: string; readonly challengeExpiresAt: string; readonly controllerAddresses: readonly string[]; readonly resolutionSource: "peaqos-mcr"; } | null; readonly machineType: string; readonly runtimeProfile: string; readonly capabilities: readonly string[]; readonly labels: Readonly>; readonly policyIds: readonly string[]; readonly skillKeys: readonly string[]; readonly createdAt: string; readonly updatedAt: string; }; ``` ## Machine identity challenges ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.createMachineIdentityChallenge( params: CreateMachineIdentityChallengeRequest, ): Promise> ``` Requests a server-issued challenge tied to a `did:peaq:0x...` or `peaqos:machine:` identity reference. The DID controller signs `item.message` with EIP-191 `personal_sign` and submits the resulting `{ challengeId, signature }` as `identityProof` when creating or updating the machine record. ## Agent pairings ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listAgentPairings(machineId: string, options?: ListAgentPairingsOptions) : Promise> client.orchestration.createAgentPairingChallenge( machineId: string, params: CreateAgentPairingChallengeRequest, ): Promise> client.orchestration.createAgentPairing( machineId: string, params: CreateAgentPairingRequest, ): Promise> client.orchestration.createAgentPairingSession( machineId: string, pairingId: string, params: CreateAgentPairingSessionRequest, ): Promise> client.orchestration.updateAgentPairing( machineId: string, pairingId: string, params: UpdateAgentPairingRequest, ): Promise> client.orchestration.revokeAgentPairing(machineId: string, pairingId: string) : Promise ``` Pairing is challenge-based end to end: 1. `createAgentPairingChallenge` returns a server-issued challenge keyed to `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`. 2. The Machine Agent signs `item.message` (EIP-191) with the wallet key behind `agentAddress`. 3. `createAgentPairing` accepts the proof in `params.agentProof` and returns the pairing with a signed HS256 session JWT in `pairingToken`. The token is returned once at create. 4. `createAgentPairingSession` rotates the session token before expiry with a fresh proof. Required after any `updateAgentPairing` to the delegation policy, since policy changes invalidate the current token's `delegationPolicyHash`. `createAgentPairing` and `createAgentPairingSession` return the `pairingToken` exactly once each. `toJSON` redacts the token to `"[REDACTED]"` so it does not leak through `JSON.stringify`. `CreateAgentPairingRequest` now requires `agentProof: { challengeId, signature }` and accepts optional `agentDid`. `delegationPolicy.allowedServiceIds` and `delegationPolicy.deniedServiceIds` join the existing skill-level allow/deny lists. ## Machine agents ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listMachineAgents(machineId: string) : Promise> client.orchestration.enrollMachineAgent( machineId: string, params?: EnrollMachineAgentRequest, ): Promise> client.orchestration.revokeMachineAgent(machineId: string, agentId: string) : Promise client.orchestration.machineAgentHeartbeat( params: MachineAgentHeartbeatRequest, ): Promise ``` `enrollMachineAgent` POSTs `/machines/:machineId/agents/enrollment` and returns a one-time `provisioningToken`. `machineAgentHeartbeat` uses no auth header — the `agentToken` rides in the request body. ## Runtime endpoints ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listRuntimeEndpoints(machineId: string, options?: ListRuntimeEndpointsOptions) : Promise> client.orchestration.getRuntimeEndpoint(machineId: string, providerKey: string) : Promise> client.orchestration.upsertRuntimeEndpoint( machineId: string, providerKey: string, params: UpsertRuntimeEndpointRequest, ): Promise> client.orchestration.deleteRuntimeEndpoint(machineId: string, providerKey: string) : Promise ``` Upsert uses HTTP `PUT`. `endpointBaseUrl` is parsed with `new URL(...)` client-side; invalid URLs throw `OrchestrationValidationError(field: "endpointBaseUrl", constraint: "valid URL")`. ## Skills ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listSkills(options?: { scope?: SkillScope; direction?: SkillDirection; source?: SkillSource; }): Promise> client.orchestration.getSkill(skillKey: string) : Promise> client.orchestration.getSkillManifest(skillKey: string) : Promise> client.orchestration.updateSkillConfig( skillKey: string, params: UpdateSkillConfigRequest, ): Promise> ``` ## Market services ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listMarketServices(options?: { machineId?: string; serviceType?: ServiceType; executionMode?: ExecutionMode; providerKey?: string; }): Promise> client.orchestration.getMarketService( serviceId: string, options?: { machineId?: string }, ): Promise> ``` ## Market search ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.searchMarket( params: MarketSearchRequest, pairingToken: string, ): Promise> client.orchestration.getMarketSearch(searchId: string) : Promise> ``` `searchMarket` uses `x-agent-pairing-token` auth — the token rides in the second argument and is sent per call. `getMarketSearch` falls back to platform auth. ## Market orders ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listMarketOrders( options: ListMarketOrdersOptions, // { machineId, cursor?, limit? } ): Promise> client.orchestration.createMarketOrder( params: CreateMarketOrderRequest, // { machineId, agentPairingId, serviceId, searchId?, quoteId?, operation?, input?, budget?, providerCredentials? } pairingToken: string, ): Promise> client.orchestration.getMarketOrder( orderId: string, ): Promise> client.orchestration.executeMarketOrder( orderId: string, params: ExecuteMarketOrderRequest, // { input? } pairingToken: string, ): Promise // discriminated union client.orchestration.confirmMarketOrder( orderId: string, pairingToken: string, ): Promise // { order, payment: MarketPayment | null } client.orchestration.disputeMarketOrder( orderId: string, params: DisputeMarketOrderRequest, // { reason, evidence? } pairingToken: string, ): Promise // { order, payment, dispute }; payment → "frozen" ``` `listMarketOrders` and `getMarketOrder` use platform auth; the four mutating calls require the agent's `pairingToken`. `executeMarketOrder` returns a discriminated union on `execution.statusCode`: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await client.orchestration.executeMarketOrder(orderId, {}, pairingToken); if (result.execution.statusCode === 200) { // Native execution const { run, outcome } = result.execution; } else { // 202 — external handoff const { handoff } = result.execution; // { label, url, notes } } ``` ## Payment settlement ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.createPaymentIntent( orderId: string, params: CreatePaymentIntentRequest, // { rail?, amount?, currency? } pairingToken: string, ): Promise> client.orchestration.getMarketPayment( orderId: string, ): Promise> // platform auth; item may be null client.orchestration.submitPaymentProof( orderId: string, params: SubmitPaymentProofRequest, // EVM: transactionHash | Solana: transactionSignature + verificationMode + chain, token, payerAddress, payeeAddress, amount pairingToken: string, ): Promise> client.orchestration.lockEscrowPayment( orderId: string, params: LockEscrowPaymentRequest, // { transactionHash, chain, escrowAddress } all required pairingToken: string, ): Promise> client.orchestration.releasePayment( orderId: string, params: ReleasePaymentRequest | undefined, // { transactionHash?, notes? } pairingToken: string, ): Promise> client.orchestration.refundPayment( orderId: string, params: RefundPaymentRequest | undefined, // { transactionHash?, reason? } pairingToken: string, ): Promise> ``` `submitPaymentProof` accepts EVM (`transactionHash`) and Solana (`transactionSignature`) shapes with `verificationMode: "recorded" | "rpc"`. RPC verification cross-checks the ERC-20 Transfer log against expected `token`, `payerAddress`, `payeeAddress`, and `amount`. ## Pagination Six auto-paginated iterators ship in this release: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} for await (const m of client.orchestration.listMachinesAll()) { /* … */ } for await (const s of client.orchestration.listSkillsAll()) { /* … */ } for await (const svc of client.orchestration.listMarketServicesAll()) { /* … */ } for await (const o of client.orchestration.listMarketOrdersAll({ machineId: "m-1" })) { /* … */ } for await (const p of client.orchestration.listAgentPairingsAll("m-1")) { /* … */ } for await (const e of client.orchestration.listRuntimeEndpointsAll("m-1")) { /* … */ } ``` Each `*All` accepts `Omit` — cursor is managed internally; filters and `limit` pass through on every page. For manual pagination, the underlying `list*` methods now accept `cursor` and `limit`: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} let cursor: string | undefined; do { const page = await client.orchestration.listMarketOrders({ machineId: "m-1", limit: 50, cursor }); for (const order of page.items) handle(order); cursor = page.nextCursor; // undefined when terminal } while (cursor); ``` ## Policies Cross-cutting policy records above and beyond a pairing's delegation policy. Platform auth. ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.listPolicies() : Promise> client.orchestration.createPolicy(params: CreatePolicyRequest) : Promise> client.orchestration.updatePolicy(policyId: string, params: UpdatePolicyRequest) : Promise> ``` `getPolicy` is not wired yet — fetch via `listPolicies` until it ships. ## Observability ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.checkHealth(): Promise // GET /health (host root) client.orchestration.getReadiness(): Promise // GET /readiness client.orchestration.listAuditEvents(options?: { machineId?: string; resourceType?: string; limit?: number; }): Promise> ``` ## Coming next These endpoints ship on the HTTP API (see [Machine Markets API: Orchestration](/peaqos/api-reference/machine-markets-orchestration)). SDK method bindings follow shortly: * Tasks: `createTask`, `discoverTask`, `resolveTask`, `executeTask`, `getTask`, `listTasks` * Graph: `getMachineGraph`, `createGraphNode`, `updateGraphNode`, `deleteGraphNode`, `createGraphEdge`, `updateGraphEdge`, `deleteGraphEdge` * Policies: `getPolicy` * Runs: `listRuns`, `getRun` * Order family extras: `cancelMarketOrder`, `retryMarketOrder` Types for the remaining order-family operations (plus tasks, graph, and runs) already ship in `@peaqos/peaq-os-sdk` so application code can prepare for the methods landing. Forward-looking type declarations carry an `@experimental` JSDoc tag — treat them as wire shapes likely to change. Method-level stubs for not-yet-implemented endpoints (skill submissions, etc.) are intentionally not exported until the endpoints land. ## Errors Five exported classes, all extend `PeaqosError`: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class OrchestrationError extends PeaqosError {} class OrchestrationApiError extends OrchestrationError { readonly code: string; // server-provided, e.g. "NOT_FOUND" readonly statusCode: number; // HTTP 4xx/5xx readonly details: unknown; // server-provided detail blob } class OrchestrationNetworkError extends OrchestrationError { // .cause carries the original transport error } class OrchestrationConfigError extends OrchestrationError {} class OrchestrationValidationError extends OrchestrationError { readonly field: string; readonly constraint: string; } ``` Switch on `instanceof OrchestrationApiError` then on `err.code`. Never parse `err.message`. Server `code` values exported as constants from `@peaqos/peaq-os-sdk`: `ERROR_CODE_AUTH_REQUIRED`, `ERROR_CODE_AUTH_INVALID`, `ERROR_CODE_AGENT_AUTH_REQUIRED`, `ERROR_CODE_AGENT_AUTH_INVALID`, `ERROR_CODE_AGENT_AUTH_EXPIRED`, `ERROR_CODE_VALIDATION_ERROR`, `ERROR_CODE_NOT_FOUND`, `ERROR_CODE_MACHINE_NOT_ACTIVE`, `ERROR_CODE_MACHINE_NOT_ACTIVATED`, `ERROR_CODE_MACHINE_IDENTITY_EXISTS`, `ERROR_CODE_MACHINE_IDENTITY_IMMUTABLE`, `ERROR_CODE_MACHINE_IDENTITY_PROOF_REQUIRED`, `ERROR_CODE_MACHINE_IDENTITY_PROOF_INVALID`, `ERROR_CODE_MACHINE_IDENTITY_PROOF_EXPIRED`, `ERROR_CODE_PEAQOS_IDENTITY_UNAVAILABLE`, `ERROR_CODE_AGENT_PAIRING_PROOF_REQUIRED`, `ERROR_CODE_AGENT_PAIRING_PROOF_INVALID`, `ERROR_CODE_AGENT_PAIRING_PROOF_EXPIRED`, `ERROR_CODE_AGENT_PAIRING_UNAVAILABLE`, `ERROR_CODE_AGENT_PAIRING_REQUIRED`, `ERROR_CODE_AGENT_PAIRING_INACTIVE`, `ERROR_CODE_AGENT_POLICY_DENIED`, `ERROR_CODE_AGENT_SPEND_LIMIT_EXCEEDED`, `ERROR_CODE_AGENT_DAILY_LIMIT_EXCEEDED`, `ERROR_CODE_QUOTE_EXPIRED`, `ERROR_CODE_ORDER_CLOSED`, `ERROR_CODE_ORDER_NOT_DELIVERED`, `ERROR_CODE_PAYMENT_REQUIRED`, `ERROR_CODE_PAYMENT_RPC_REQUIRED`, `ERROR_CODE_PAYMENT_RPC_ERROR`, `ERROR_CODE_PAYMENT_TX_FAILED`, `ERROR_CODE_PAYMENT_NOT_MINED`, `ERROR_CODE_PAYMENT_TRANSFER_NOT_FOUND`, `ERROR_CODE_EXECUTION_UNSUPPORTED`, `ERROR_CODE_ENDPOINT_UNREACHABLE`. **New types exported in `0.3.0`:** ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type MachineIdentityProofInput = { challengeId: string; signature: string }; type AgentPairingChallengeResponse = { challengeId: string; agentAddress: string; message: string; expiresAt: string; verificationMethod: "eip191"; }; type AgentPairingProof = { challengeId: string; signature: string }; type ProviderCredentials = { // Per-adapter credentials keyed by providerKey. Shape is // adapter-specific and documented per-adapter on robotic.sh. // Always redacted before request bodies are persisted. [providerKey: string]: Record; }; ``` `MarketPaymentRailType` adds `"wdk-usdt-transfer"` alongside the existing `"x402" | "mpp" | "wallet" | "vault-stripe" | "escrow" | "onchain-escrow" | "offchain-record" | "external" | "not-required"` set. The transport synthesises `BAD_RESPONSE` (non-JSON error body) and `INVALID_RESPONSE_SHAPE` (envelope mismatch) inside `OrchestrationApiError` when the server response is malformed. ## Credential handling Three auth modes per call: * **Platform** (`x-api-key`) — set once on the client via `apiKey`, sent on every call by default. * **Agent pairing** (`x-agent-pairing-token`) — passed per call to `searchMarket` as the second argument. The SDK does not store or rotate it. * **None** — used only by `machineAgentHeartbeat` (token rides in the body). `sanitizeBody()` redacts any body field whose name contains `token`, `key`, `secret`, `password`, `credential`, or `auth` (case-insensitive) before attaching the body to an error. Redaction is recursive into nested objects. ## Related * [Machine Markets API](/peaqos/api-reference/machine-markets-overview) * [Orchestration (Python)](/peaqos/sdk-reference/orchestration-py) * [Scale function](/peaqos/functions/scale) * [Machine Markets concept](/peaqos/concepts/machine-markets) # Orchestration (Python) Source: https://docs.peaq.xyz/peaqos/sdk-reference/orchestration-py client.orchestration.* namespace on the Python SDK — machines, agent pairings, machine agents, runtime endpoints, skills, market services, market search. The Python SDK exposes the [Machine Markets API](/peaqos/api-reference/machine-markets-overview) on the existing `PeaqosClient` via `client.orchestration`. Flat methods, sync, snake\_case, frozen-slotted dataclasses. ## Setup ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv from peaq_os_sdk import PeaqosClient load_dotenv() client = PeaqosClient.from_env() machines = client.orchestration.list_machines() for m in machines.items: print(m.id, m.display_name) ``` Functional form for tree-shaking or testing: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.orchestration import list_machines resp = list_machines(client, limit=20) ``` Accessing `client.orchestration` without `orchestration_url` raises `OrchestrationConfigError`. ## Configuration ### Constructor kwargs ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} PeaqosClient( *, rpc_url: str, private_key: str, # ... on-chain contract addresses ... api_url: str = "http://127.0.0.1:8000", # MCR API orchestration_url: str | None = None, api_key: str | None = None, # platform x-api-key verbose: bool = False, ) ``` ### Env vars (via `PeaqosClient.from_env()`) | Var | Required | Purpose | | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------ | | `PEAQOS_RPC_URL` | yes | peaq RPC for on-chain | | `PEAQOS_PRIVATE_KEY` | yes | signer for on-chain | | `IDENTITY_REGISTRY_ADDRESS`, `IDENTITY_STAKING_ADDRESS`, `EVENT_REGISTRY_ADDRESS`, `MACHINE_NFT_ADDRESS`, `DID_REGISTRY_ADDRESS`, `BATCH_PRECOMPILE_ADDRESS` | yes | on-chain contract addresses | | `PEAQOS_ORCHESTRATION_URL` | optional | enables `client.orchestration` | | `PEAQOS_API_KEY` | optional | platform API key (`x-api-key` header) | | `PEAQOS_MCR_API_URL` | optional | MCR base URL | Transport constants: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} DEFAULT_READ_TIMEOUT_MS = 30_000 # GET DEFAULT_WRITE_TIMEOUT_MS = 60_000 # everything else API_BASE_PATH = "/api/v1" HEADER_API_KEY = "x-api-key" HEADER_AGENT_PAIRING_TOKEN = "x-agent-pairing-token" ``` `User-Agent: peaqos-sdk-py/`. Per-method timeout overrides are not exposed on public methods yet. ## Envelopes ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @dataclass(frozen=True, slots=True) class ListResponse(Generic[T]): items: tuple[T, ...] next_cursor: str | None # None when there is no next page @dataclass(frozen=True, slots=True) class ItemResponse(Generic[T]): item: T LIST_QUERY_DEFAULT_LIMIT = 100 LIST_QUERY_MAX_LIMIT = 500 @dataclass(frozen=True, slots=True) class ListQuery: limit: int | None = None # 1..500 cursor: str | None = None # omit on first page ``` `204 No Content` returns `None`. `validate_list_limit` rejects `limit < 1` or `limit > 500` with `OrchestrationValidationError`. ## Machines ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_machines( *, limit: int | None = None, cursor: str | None = None, ) -> ListResponse[Machine] client.orchestration.create_machine(params: CreateMachineRequest) -> ItemResponse[Machine] client.orchestration.get_machine(machine_id: str) -> ItemResponse[Machine] client.orchestration.update_machine(machine_id: str, params: UpdateMachineRequest) -> ItemResponse[Machine] client.orchestration.archive_machine(machine_id: str) -> None ``` `Machine` shape: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @dataclass(frozen=True, slots=True) class Machine: id: str display_name: str status: Literal["draft", "active", "degraded", "blocked", "archived"] owner_id: str machine_type: str runtime_profile: str capabilities: tuple[str, ...] labels: Mapping[str, str] policy_ids: tuple[str, ...] skill_keys: tuple[str, ...] created_at: str updated_at: str identity_ref: str | None = None ``` ## Machine agents ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_machine_agents(machine_id: str) -> ListResponse[MachineAgent] client.orchestration.enroll_machine_agent(machine_id: str, params: EnrollMachineAgentRequest | None = None) -> ItemResponse[MachineAgent] client.orchestration.revoke_machine_agent(machine_id: str, agent_id: str) -> None client.orchestration.machine_agent_heartbeat(params: MachineAgentHeartbeatRequest) -> MachineAgentHeartbeatResponse ``` `enroll_machine_agent` returns `ItemResponse[MachineAgent]` whose `item.provisioning_token` is populated once at enrollment. `machine_agent_heartbeat` uses no auth header — `agent_token` rides in the JSON body. ## Machine identity challenges ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.create_machine_identity_challenge( params: CreateMachineIdentityChallengeRequest, ) -> ItemResponse[MachineIdentityChallengeResponse] ``` Requests a server-issued challenge tied to a `did:peaq:0x...` or `peaqos:machine:` identity reference. The DID controller signs `item.message` with EIP-191 `personal_sign` and submits the resulting `{challengeId, signature}` as `identity_proof` when creating or updating the machine record. ## Agent pairings ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_agent_pairings(machine_id: str, *, limit: int | None = None, cursor: str | None = None) -> ListResponse[AgentPairing] client.orchestration.create_agent_pairing_challenge( machine_id: str, params: CreateAgentPairingChallengeRequest, ) -> ItemResponse[AgentPairingChallengeResponse] client.orchestration.create_agent_pairing( machine_id: str, params: CreateAgentPairingRequest, ) -> ItemResponse[AgentPairing] client.orchestration.create_agent_pairing_session( machine_id: str, pairing_id: str, params: CreateAgentPairingSessionRequest, ) -> ItemResponse[AgentPairing] client.orchestration.update_agent_pairing( machine_id: str, pairing_id: str, params: UpdateAgentPairingRequest, ) -> ItemResponse[AgentPairing] client.orchestration.revoke_agent_pairing(machine_id: str, pairing_id: str) -> None ``` Pairing is challenge-based end to end: 1. `create_agent_pairing_challenge` returns a server-issued challenge keyed to `agent_address`, `agent_provider`, `agent_role`, and optional `agent_did`. 2. The Machine Agent signs `item.message` (EIP-191) with the wallet key behind `agent_address`. 3. `create_agent_pairing` accepts the proof in `params.agent_proof` and returns the pairing with a signed HS256 session JWT in `pairing_token`. The token is returned once at create. 4. `create_agent_pairing_session` rotates the session token before expiry with a fresh proof. Required after any `update_agent_pairing` to the delegation policy, since policy changes invalidate the current token's `delegation_policy_hash`. Both create methods set `pairing_token` exactly once each. `AgentPairing.__repr__` redacts the token to keep it out of logs. `CreateAgentPairingRequest` now requires `agent_proof: AgentPairingProof` and accepts optional `agent_did`. `delegation_policy.allowed_service_ids` and `delegation_policy.denied_service_ids` join the existing skill-level allow/deny lists. ## Runtime endpoints ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_runtime_endpoints(machine_id: str, *, limit: int | None = None, cursor: str | None = None) -> ListResponse[RuntimeEndpoint] client.orchestration.get_runtime_endpoint(machine_id: str, provider_key: str) -> ItemResponse[RuntimeEndpoint] client.orchestration.upsert_runtime_endpoint(machine_id: str, provider_key: str, params: UpsertRuntimeEndpointRequest) -> ItemResponse[RuntimeEndpoint] client.orchestration.delete_runtime_endpoint(machine_id: str, provider_key: str) -> None ``` `upsert_runtime_endpoint` validates that `endpoint_base_url` parses as `http`/`https` with a non-empty host. `auth_token` is treated as a secret in error redaction. ## Skills ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class ListSkillsOptions: scope: SkillScope | None = None # core-product | bootstrap-reference | ecosystem direction: SkillDirection | None = None # machine-offers | machine-consumes source: SkillSource | None = None # peaq | partner | clawhub | external-marketplace client.orchestration.list_skills(options: ListSkillsOptions | None = None, *, limit: int | None = None, cursor: str | None = None) -> ListResponse[SkillSummary] client.orchestration.get_skill(skill_key: str) -> ItemResponse[SkillSummary] client.orchestration.get_skill_manifest(skill_key: str) -> ItemResponse[SkillManifest] client.orchestration.update_skill_config(skill_key: str, params: UpdateSkillConfigRequest) -> ItemResponse[SkillConfigResponse] ``` ## Market services ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class ListMarketServicesOptions: machine_id: str | None = None service_type: ServiceType | None = None execution_mode: ExecutionMode | None = None provider_key: str | None = None class GetMarketServiceOptions: machine_id: str | None = None client.orchestration.list_market_services(options: ListMarketServicesOptions | None = None, *, limit: int | None = None, cursor: str | None = None) -> ListResponse[MarketService] client.orchestration.get_market_service(service_id: str, options: GetMarketServiceOptions | None = None) -> ItemResponse[MarketService] ``` ## Market search ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.search_market(params: MarketSearchRequest, pairing_token: str) -> ItemResponse[MarketSearch] client.orchestration.get_market_search(search_id: str) -> ItemResponse[MarketSearch] ``` `search_market` uses `x-agent-pairing-token` auth — the token rides as the second argument and is sent per call. `get_market_search` uses platform auth. ## Market orders ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_market_orders( *, machine_id: str, limit: int | None = None, cursor: str | None = None, ) -> ListResponse[MarketOrder] client.orchestration.create_market_order( params: CreateMarketOrderRequest, # machine_id, agent_pairing_id, service_id required pairing_token: str, ) -> ItemResponse[MarketOrder] client.orchestration.get_market_order(order_id: str) -> ItemResponse[MarketOrder] client.orchestration.execute_market_order( order_id: str, params: ExecuteMarketOrderRequest, # input? pairing_token: str, ) -> ExecuteMarketOrderResponse # union narrowed by execution.status_code client.orchestration.confirm_market_order( order_id: str, pairing_token: str, ) -> ConfirmMarketOrderResponse # { order, payment: MarketPayment | None } client.orchestration.dispute_market_order( order_id: str, params: DisputeMarketOrderRequest, # reason required; evidence optional pairing_token: str, ) -> DisputeMarketOrderResponse # { order, payment, dispute }; payment → "frozen" ``` `list_market_orders` and `get_market_order` use platform auth; the four mutating calls require the agent's `pairing_token`. `execute_market_order` returns a union narrowed by `execution.status_code`: `200` for a native run (with `run` + `outcome`), `202` for an external handoff (with `handoff: { label, url, notes }`). ## Payment settlement ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.create_payment_intent( order_id: str, params: CreatePaymentIntentRequest, # rail?, amount?, currency? pairing_token: str, ) -> ItemResponse[MarketPayment] client.orchestration.get_market_payment(order_id: str) -> ItemResponse[MarketPayment | None] client.orchestration.submit_payment_proof( order_id: str, params: SubmitPaymentProofRequest, # EVM: transaction_hash | Solana: transaction_signature; verification_mode required; chain, token, payer_address, payee_address, amount required pairing_token: str, ) -> ItemResponse[MarketPayment] client.orchestration.lock_escrow_payment( order_id: str, params: LockEscrowPaymentRequest, # transaction_hash, chain, escrow_address all required pairing_token: str, ) -> ItemResponse[MarketPayment] client.orchestration.release_payment( order_id: str, params: ReleasePaymentRequest | None, # transaction_hash?, notes? pairing_token: str, ) -> ItemResponse[MarketPayment] client.orchestration.refund_payment( order_id: str, params: RefundPaymentRequest | None, # transaction_hash?, reason? pairing_token: str, ) -> ItemResponse[MarketPayment] ``` `submit_payment_proof` accepts both EVM and Solana proof shapes; `verification_mode` is `"recorded" | "rpc"`. RPC verification cross-checks the ERC-20 Transfer log against expected `token`, `payer_address`, `payee_address`, and `amount`. ## Pagination Six sync auto-paginated iterators ship in this release: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} for m in client.orchestration.list_machines_all(): ... for s in client.orchestration.list_skills_all(): ... for svc in client.orchestration.list_market_services_all(): ... for o in client.orchestration.list_market_orders_all(machine_id="m-1"): ... for p in client.orchestration.list_agent_pairings_all(): ... for e in client.orchestration.list_runtime_endpoints_all(): ... ``` Each `*_all` accepts the same filter kwargs as its non-iterator counterpart, minus `cursor`. `limit` controls page size; cursor is managed internally. For manual pagination, the underlying `list_*` methods now accept `cursor` and `limit` as keyword-only args: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cursor = None while True: page = client.orchestration.list_market_orders( machine_id="m-1", limit=50, cursor=cursor, ) for order in page.items: handle(order) if page.next_cursor is None: break cursor = page.next_cursor ``` `MarketSearch`: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @dataclass(frozen=True, slots=True) class MarketSearch: id: str machine_id: str request_payload: Mapping[str, Any] normalized_task: NormalizedTask status: Literal["completed", "no_match"] quotes: tuple[MarketQuote, ...] created_at: str agent_pairing_id: str | None = None ``` ## Policies Cross-cutting policy records above and beyond a pairing's delegation policy. Platform auth. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.list_policies( *, limit: int | None = None, cursor: str | None = None, ) -> ListResponse[Policy] client.orchestration.create_policy(params: CreatePolicyRequest) -> ItemResponse[Policy] client.orchestration.update_policy(policy_id: str, params: UpdatePolicyRequest) -> ItemResponse[Policy] ``` `get_policy` is not wired yet — fetch via `list_policies` until it ships. ## Observability ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.orchestration.check_health() -> HealthResponse # GET /health (host root) client.orchestration.get_readiness() -> ReadinessResponse # GET /readiness client.orchestration.list_audit_events( *, machine_id: str | None = None, resource_type: str | None = None, limit: int | None = None, ) -> ListResponse[AuditEvent] ``` ## Coming next These endpoints ship on the HTTP API (see [Machine Markets API: Orchestration](/peaqos/api-reference/machine-markets-orchestration)). SDK method bindings follow shortly: * Tasks: `create_task`, `discover_task`, `resolve_task`, `execute_task`, `get_task`, `list_tasks` * Graph: `get_machine_graph`, `create_graph_node`, `update_graph_node`, `delete_graph_node`, `create_graph_edge`, `update_graph_edge`, `delete_graph_edge` * Policies: `get_policy` * Runs: `list_runs`, `get_run` * Order family extras: `cancel_market_order`, `retry_market_order` Types for the remaining order-family operations already ship in `peaq_os_sdk` so application code can prepare for the methods landing. ## Errors All extend `peaq_os_sdk.exceptions.base.PeaqosError`: ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class OrchestrationError(PeaqosError): """Base. Catch this for any orchestration failure.""" class OrchestrationApiError(OrchestrationError): code: str # CommonErrorCode literal or any service string status_code: int # HTTP status; 0 when raised by client-side validation details: Any | None # snake-cased payload; sensitive fields redacted class OrchestrationNetworkError(OrchestrationError): """Transport-level failure. __cause__ carries the original requests.RequestException.""" class OrchestrationConfigError(OrchestrationError): """Misconfiguration — orchestration_url missing or invalid.""" class OrchestrationValidationError(OrchestrationError): field: str constraint: str ``` `CommonErrorCode` literal: `AUTH_REQUIRED`, `AUTH_INVALID`, `AGENT_AUTH_REQUIRED`, `AGENT_AUTH_INVALID`, `AGENT_AUTH_EXPIRED`, `VALIDATION_ERROR`, `NOT_FOUND`, `MACHINE_NOT_ACTIVE`, `MACHINE_NOT_ACTIVATED`, `MACHINE_IDENTITY_EXISTS`, `MACHINE_IDENTITY_IMMUTABLE`, `MACHINE_IDENTITY_PROOF_REQUIRED`, `MACHINE_IDENTITY_PROOF_INVALID`, `MACHINE_IDENTITY_PROOF_EXPIRED`, `PEAQOS_IDENTITY_UNAVAILABLE`, `AGENT_PAIRING_PROOF_REQUIRED`, `AGENT_PAIRING_PROOF_INVALID`, `AGENT_PAIRING_PROOF_EXPIRED`, `AGENT_PAIRING_UNAVAILABLE`, `AGENT_PAIRING_REQUIRED`, `AGENT_PAIRING_INACTIVE`, `AGENT_POLICY_DENIED`, `AGENT_SPEND_LIMIT_EXCEEDED`, `AGENT_DAILY_LIMIT_EXCEEDED`, `QUOTE_EXPIRED`, `ORDER_CLOSED`, `ORDER_NOT_DELIVERED`, `PAYMENT_REQUIRED`, `PAYMENT_RPC_REQUIRED`, `PAYMENT_RPC_ERROR`, `PAYMENT_TX_FAILED`, `EXECUTION_UNSUPPORTED`, `ENDPOINT_UNREACHABLE`. **New types exported in `0.3.0`:** ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @dataclass(frozen=True, slots=True) class MachineIdentityProofInput: challenge_id: str signature: str @dataclass(frozen=True, slots=True) class AgentPairingChallengeResponse: challenge_id: str agent_address: str message: str expires_at: str verification_method: Literal["eip191"] machine_id: str | None = None machine_identity_ref: str | None = None agent_did: str | None = None @dataclass(frozen=True, slots=True) class AgentProofInput: challenge_id: str signature: str @dataclass(frozen=True, slots=True) class ProviderCredentials: # Per-adapter credentials. Shape is adapter-specific and # documented per-adapter on robotic.sh. Always redacted # before request bodies are persisted. providers: Mapping[str, Mapping[str, str]] | None = None ``` `MarketPaymentRailType` adds `"wdk-usdt-transfer"` alongside the existing `"x402" | "mpp" | "wallet" | "vault-stripe" | "escrow" | "onchain-escrow" | "offchain-record" | "external" | "not-required"` set. The transport synthesises `BAD_RESPONSE` (non-JSON body) and `INVALID_RESPONSE_SHAPE` (envelope mismatch) when the server response is malformed. ## Sync vs async Sync only on this branch. Every public function is plain `def` over `requests.Session`. The transport reuses `client.session`, so users can wrap retries externally. First-class async support is on the roadmap. ## Credential handling * **Platform** (`x-api-key`) — set once on the client via `api_key`, sent on every call by default. * **Agent pairing** (`x-agent-pairing-token`) — passed per call to `search_market` as the second argument. The SDK does not store or rotate it. * **None** — used only by `machine_agent_heartbeat` (token rides in the body). `_sanitize_body` redacts any field whose name contains `token`, `key`, `secret`, `password`, `credential`, or `auth` (case-insensitive) before attaching the body to an error. ## Related * [Machine Markets API](/peaqos/api-reference/machine-markets-overview) * [Orchestration (JavaScript)](/peaqos/sdk-reference/orchestration-js) * [Scale function](/peaqos/functions/scale) * [Machine Markets concept](/peaqos/concepts/machine-markets) # Monetize: Provider provisioning Source: https://docs.peaq.xyz/peaqos/sdk-reference/provisioning The schema-driven provisioning runner: turn a published manifest into an ordered, auditable install that makes a machine a compute provider node. Provider provisioning ships in `@peaqos/peaq-os-sdk` 0.5.0+ (JS/TS) and `peaq-os-sdk` 0.5.0+ (Python). It is machine-local: the runner executes entirely on the machine, against no external API. The provisioning runner turns a `ProviderProvisioningManifest` (`apiVersion: node-provider.peaq.network/v1alpha1`) into an ordered, auditable install that provisions a machine as a compute provider node. All provider-specific complexity lives in the **manifest**; the runner only understands the schema's generic vocabulary: phases, steps, prompts, captures, secrets, and verification probes. It never hardcodes any provider's setup. ## Design invariants * **Verification, not exit codes, defines success.** A machine is a live provider only when the manifest's success probes pass. * **Secrets never leak.** Secret inputs, secret captures, and per-step redaction are applied before anything is streamed, logged, or persisted. * **Sudo is explicit and scoped.** Auto mode elevates only the commands the manifest marks, and only within the operator's granted allowlist. * **Handoffs are real stops.** Owner-action steps block until the owner confirms; a confirmation is never synthesised. * **Payout context defaults to the machine wallet**, never the operator's. The runner exposes the machine wallet address as generic context; the manifest decides how it maps into commission fields. ## The lifecycle ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} flowchart LR Fetch["1. Fetch manifest
(versioned, integrity-checked)"] --> Inputs["2. Resolve inputs
(types, validation, secrets)"] Inputs --> Pre["3. Pre-flight gate
(block / warn)"] Pre --> Run["4. Provision
(phases + steps)"] Run --> Verify["5. Verify
(success probes)"] Verify --> Post["6. Post-provision utilities
(optional)"] ``` The namespace is `client.provisioning` in JS/TS and the `peaq_os_sdk.provisioning` module in Python; every method is also importable standalone. ## Fetch a manifest Manifests are hosted on a public repo and pulled on demand. The repo base URL is configuration, never hardcoded. The JS SDK is YAML-agnostic (pass a parse function from a YAML library you already depend on); the Python SDK parses YAML itself. ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { parse as parseYaml } from "yaml"; const { manifest, resolvedVersion } = await client.provisioning.fetchManifest( { repoBaseUrl: MANIFEST_REPO_URL, parse: parseYaml }, { provider: "akash" }, // omit version to resolve "latest" ); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import provisioning manifest = provisioning.fetch_manifest( "akash", # version defaults to "latest" repo_base_url=MANIFEST_REPO_URL, ) print(manifest.resolved_version) ``` * **Version:** pass an explicit `version` or resolve `"latest"`; the concrete version is read back from the document and recorded for reproducibility. * **Integrity:** pass an expected digest (`expectedDigest` in JS, `checksum` in Python) to verify the fetched document; a mismatch fails closed and the manifest is never returned or used. One Python caveat: the fetched bytes are written to `cache_dir` before the digest check, so clear the cache after a mismatch (or keep `checksum` on every read, including `offline=True` ones); a later cache read without it would return the rejected document. * **Pinning:** request the raw source bytes (`includeSource: true` in JS) and compute a deterministic `sha256:` pin with `manifestPinDigest`, so a later verify can prove it ran against the exact document that provisioned the node. The CLI's state file uses this pin. * **Cache and offline:** caching is opt-in. Configure it (`cache` in JS, `cache_dir` in Python) and a network fetch stores the raw document for offline mode to reuse; configure nothing and nothing is stored, so offline mode has nothing to read. The document is re-validated against the schema on every read. Manifest YAML keys are camelCase (`apiVersion`, `onFailure`, `allowedCommands`). The JS SDK keeps them camelCase; the Python SDK maps them onto snake\_case dataclass fields (`validation.min`/`max` become `min_value`/`max_value`). ## Resolve inputs `resolveInputs` / `resolve_inputs` merges caller values with the manifest's declared inputs: it enforces `required`, applies defaults, type-checks every declared type (`string`, `int`, `bool`, `enum`, `url`, `hostname`, `bytesize`, `array`, `object`), and applies `validation` rules (`values`, `pattern`, `min`, `max`). Inputs marked `secret: true` are flagged and never echoed in errors. ## Pre-flight is a gate `runPreflight` / `run_preflight` runs each check and reports passed, blocked, and per-check results. A check with `onFailure: block` that fails blocks provisioning; `onFailure: warn` surfaces without blocking. A privileged check that cannot be auto-run is surfaced for the owner, and a surfaced *blocking* check still blocks the gate (fail closed). `provision` refuses to start while the gate is blocked. ## Provision: manual vs auto mode `provision` runs in `manual` or `auto` mode, and every step also declares its own mode (`manual`, `auto`, or `both`). The manifest decides; the runner honours it. | Mode | Behaviour | | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `manual` | Step-by-step, but the confirmation gate is yours to wire. Python **requires** `on_confirm_step` in manual mode: it is called before each command and returning `False` stops the run resumably, and without the callback the first command step raises `ProvisioningError` (with no resume state; pass `lambda e: True` for an unattended manual run). The JS runner has no confirmation callback at all: `onStep` is informational and cannot decline a step. Use the CLI if you want the interactive gate without building it. Default mode. | | `auto` | End-to-end execution. Requires a sudo grant for privileged steps; the exact command is emitted before each run so the owner can audit it. | **Sudo** is explicit per command (`never` / `optional` / `required`). The runner never elevates a command the manifest does not mark. In auto mode, a `sudo: required` command runs only under a sudo grant matched by prefix against the manifest's `allowedCommands`; the effective grant is the intersection of the two (the narrower side wins), so an operator can scope down but the grant can never broaden beyond the manifest. A privileged command outside the grant fails closed: it is not run unprivileged. ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await client.provisioning.provision(manifest, { mode: "auto", executor: new LocalShellExecutor(), values: { providerDomain: "provider.example.com", walletPassword: "…" }, sudoGrant: { allowedCommands: ["apt-get", "kubectl", "provider-services"] }, machineWalletAddress: "0x…", // exposed to the manifest as context.machineWalletAddress writeFile: async (r) => fs.writeFile(r.path, r.content), onOwnerAction: async (r) => promptOwner(r.instructions, r.expectedConfirmation), }); ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import provisioning inputs = provisioning.resolve_inputs(manifest, { "providerDomain": "provider.example.com", "walletPassword": "…", }) executor = provisioning.LocalShellExecutor() # Python does not run pre-flight for you: run it and pass the result, or the gate never applies. preflight = provisioning.run_preflight(manifest, executor, mode="auto", inputs=inputs) preflight.raise_if_blocked() result = provisioning.provision( manifest, inputs, mode="auto", preflight=preflight, executor=executor, sudo_grant=provisioning.SudoGrant.from_manifest(manifest), context={"machine_wallet_address": "0x…"}, on_owner_action=prompt_owner, ) ``` **The sudo grant is a scoping and audit aid, not a security boundary.** It reduces the blast radius of an honest manifest, but it cannot contain a hostile one: a granted program can still run arbitrary code through its own arguments. Auto mode elevates commands unattended, so only run a manifest you trust. Verify its integrity (fetch with an expected digest) before an auto run, and prefer OS-level isolation for defense in depth. Treat auto mode on an unverified, remotely served manifest as remote root execution. ### Step kinds | Kind | Behaviour | | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `command` | Run the command with the step's sudo, cwd, user, and timeout settings; capture outputs per `captures`. | | `render-file` | Render a file (path, owner, mode, interpolated content) to disk. Content is never echoed. A privileged write is grant-scoped like a command (matched as `render-file `). | | `download` | Fetch a resource; integrity is verified via the step's `checksum` (sha256, bare hex or `sha256:`) in both SDKs. | | `owner-action` | Pause, emit instructions, and wait for the declared confirmation. Never auto-confirmed. | | `verify` | Run an inline command. JS evaluates the step's `expect` (default: exit code 0). Python has no step-level `expect` and passes on exit code 0 alone, so put output assertions in `verification` probes, not in a `verify` step. | ### Prompts, captures, and the machine wallet Interactive prompts fill from `input` (a resolved input), `capture` (an earlier step's value), or `owner` (answered interactively, never auto-filled). Captured values thread by `key` into later steps, verification, and post-provision. The runner exposes the machine wallet address as run context (`context.machineWalletAddress` in JS, where `context.machineWallet` is only a legacy alias slated for removal; `{{ context.machine_wallet_address }}` in Python, supplied via `context=`); the manifest declares how that maps into any commission or payout field. The runner itself holds no commission semantics. ## Owner-action handoffs Wallet funding, DNS changes, hardware-wallet signing, and SSH access confirmations are `owner-action` steps: real pause points. The runner emits the interpolated (redacted) instructions and blocks until the owner returns the declared confirmation. A missing handler or a mismatched answer fails the step. ## Idempotency and resumability Each step may declare `idempotency`: a `check` command runs first and skips the step when the effect is already present, and `rerun` policy is `safe`, `unsafe`, or `requires-confirmation`. The confirmation handler differs per SDK: JS prompts via `onRerunConfirm` and raises `StepExecutionError` when the handler is missing, while Python calls `on_confirm_rerun` and, when it is omitted, silently skips a `requires-confirmation` step whose check passes. Pass the handler in Python if the re-run decision matters. An interrupted run always leaves a resumable state: `provision` returns a resume state, attaches one to the failure error, and (optionally) emits it at every step boundary via `onStateChange` / `on_state_change`, so a caller can checkpoint durably as the run progresses. The state is JSON-round-trippable and carries a schema version, the completed step ids (keyed by `phase/step`), a cursor, and the resolved **non-secret** inputs and captures. A resumed run skips completed steps and restores the non-secret captures. The boundary callback also fires at the pre-confirmation owner-action pause, with the owner-action excluded: a run killed at a funding or DNS handoff resumes by re-entering that handoff, never by skipping it. ```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} try { await client.provisioning.provision(manifest, { ...options, onStateChange: (state) => checkpoint.write(state), // durable per-step checkpoint }); } catch (err) { if (err instanceof StepExecutionError && err.resumeState) { await client.provisioning.provision(manifest, { ...options, resumeState: err.resumeState, resumeSecrets: { walletKey: "…" }, // re-supply secret captures if needed }); } } ``` ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} try: provisioning.provision( manifest, inputs, mode="auto", executor=executor, on_state_change=lambda s: checkpoint.write(s.to_dict()), ) except provisioning.StepExecutionError as err: state = err.resume_state # persist this (never contains a secret) # later: provisioning.provision( manifest, inputs, mode="auto", executor=executor, resume_state=state, resume_secrets={"wallet_key": "…"}, # re-supply secret captures if needed ) ``` **Secret captures are never persisted**, in plaintext or otherwise; only their key names are recorded. On resume, if a remaining step needs a secret from an already-completed producer, the runner re-runs that producer when its `rerun` policy is `safe`, takes a caller-re-supplied value via `resumeSecrets` / `resume_secrets` otherwise, and fails with `ResumeNotPossibleError` (naming the step and the missing keys) when neither applies. A step never interpolates a missing or empty secret. ## Verification closes the loop `verifyProvisioning` / `verify_provisioning` runs the manifest's probes. Success requires **every** probe in `verification.success.allOf` to pass; exit-zero installs alone never count. A privileged probe that cannot be elevated is not run unprivileged: it is reported as failed. `ensureVerified` in JS/TS, and the result's own `raise_if_failed()` in Python, raises `VerificationFailedError` (listing the failed probes) unless verification succeeded; report the node live only when it does not raise. ## Post-provision operations `runPostProvision` / `run_post_provision` runs one opt-in operator utility by id (tail logs, check balance, and similar), honouring its mode, sudo policy, and actor. These are helpers only, never part of the success gate. ## Secrets and redaction A run-scoped redaction registry holds every secret input value plus the declared extraction pattern of every secret capture, registered before its step runs. Every streamed chunk, event, captured output, rendered-file echo, and error message passes through redaction before it leaves the SDK. A secret stdout/stderr capture with no pattern suppresses raw stream emission entirely. Typed errors carry only structural metadata (ids, paths, exit codes), never secret material. That is automatic inside `provision`, but **not** everywhere. `verifyProvisioning` / `verify_provisioning` (both SDKs) and JS `runPreflight` take their own optional `redaction` registry and do not seed one from your resolved inputs: call them without it and a probe command that interpolates a secret is emitted, executed, and returned unredacted. Python's `run_preflight` does seed redaction from the `inputs` you pass, so hand it your resolved inputs. Pass the registry yourself in the other cases, or run these through the CLI, which seeds it for you. ## Errors | Error | Raised when | | :---------------------------- | :-------------------------------------------------------------------- | | `ManifestFetchError` | Network failure, 404, timeout, or checksum mismatch while fetching | | `ManifestSchemaError` | The manifest fails structural validation | | `ProvisioningValidationError` | An input fails type or validation rules | | `PreflightBlockedError` | A blocking pre-flight check failed | | `StepExecutionError` | A step failed, timed out, or was disallowed; carries the resume state | | `VerificationFailedError` | Required probes did not all pass | ## Related * [Monetize function](/peaqos/functions/monetize): where provisioning fits in the flow * [Heartbeat SDK reference](/peaqos/sdk-reference/heartbeat): report presence once provisioned * [Monetization opt-in API](/peaqos/api-reference/put-machine-monetization) # Bridge Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/bridge Bridge Machine NFTs between peaq and Base from ROS 2 via LayerZero V2. `peaq_ros2_peaqos` exposes the LayerZero V2 ONFT bridge as two services: | Service | Type | Purpose | | :--------------------------------- | :---------------------------------------------------- | :------------------------------------------------------- | | `/peaqos_node/bridge/nft` | `peaq_ros2_interfaces/srv/PeaqosBridgeNft` | Send a Machine NFT between peaq and Base | | `/peaqos_node/bridge/wait_arrival` | `peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival` | Poll the destination chain until the bridged NFT arrives | Bridging is **mainnet only**. LayerZero V2 has no DVN routes between agung and Base, so bridge calls on testnet won't relay end-to-end. Test the round trip on peaq mainnet ↔ Base mainnet. ## Required configuration Both directions need the LayerZero ONFT contracts wired into `peaq_robot.yaml`: ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: contracts: machine_nft_adapter: "0x9AD5408702EC204441A88589B99ADfC2514AFAE6" # peaq mainnet # Pass the Base counterpart per call (see base_nft_address below) or # leave it blank to fall back to the SDK default. ``` Equivalent env override: `MACHINE_NFT_ADAPTER_ADDRESS`. Base side reference contract: `0xee8A521eA434b11F956E2402beC5eBfa753Babfa` (`MachineNFTBase`). ## peaq → Base The signer must own the Machine NFT on peaq and hold enough peaq for gas plus the LayerZero fee. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/nft \ peaq_ros2_interfaces/srv/PeaqosBridgeNft \ "{signer_address: '', token_id: 1, source: 'peaq', destination: 'base', recipient: '', base_rpc_url: '', base_nft_address: '', options_hex: ''}" ``` Then poll the destination: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/wait_arrival \ peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival \ "{dst_rpc_url: 'https://mainnet.base.org', dst_nft_address: '0xee8A521eA434b11F956E2402beC5eBfa753Babfa', token_id: 1, timeout: 900}" ``` `timeout` is in seconds. The service returns once `ownerOf(token_id)` matches the recipient on the destination chain or the deadline is hit. ## Base → peaq Same service shape, swapped `source` / `destination`. The signer must hold the Machine NFT on Base **and** enough Base ETH to cover gas plus the LayerZero fee — the bridge can't bootstrap funding for you. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/nft \ peaq_ros2_interfaces/srv/PeaqosBridgeNft \ "{signer_address: '', token_id: 1, source: 'base', destination: 'peaq', recipient: '', base_rpc_url: 'https://mainnet.base.org', base_nft_address: '0xee8A521eA434b11F956E2402beC5eBfa753Babfa', options_hex: ''}" ``` Wait for arrival on peaq: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/wait_arrival \ peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival \ "{dst_rpc_url: 'https://quicknode1.peaq.xyz', dst_nft_address: '0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9', token_id: 1, timeout: 900}" ``` ## Field reference `PeaqosBridgeNft`: | Field | Description | | :----------------- | :------------------------------------------------------------------------------------------ | | `signer_address` | Local wallet address that owns the NFT on the source chain | | `token_id` | Machine NFT token id | | `source` | `peaq` or `base` | | `destination` | `peaq` or `base` | | `recipient` | Address that receives the NFT on the destination chain | | `base_rpc_url` | Base JSON-RPC URL. Required when `source: 'base'`; unused when `source: 'peaq'` | | `base_nft_address` | Base `MachineNFTBase` address. Required when `source: 'base'`; unused when `source: 'peaq'` | | `options_hex` | Raw LayerZero `extraOptions` payload hex. Leave blank for contract defaults | `PeaqosWaitForBridgeArrival`: | Field | Description | | :---------------- | :------------------------------------------------ | | `dst_rpc_url` | RPC for the destination chain | | `dst_nft_address` | NFT contract on the destination chain | | `token_id` | Machine NFT token id | | `timeout` | Seconds to poll before returning `arrived: false` | ## Operational notes * Record pre/post balances on both chains for any production bridge test. * The reverse direction (Base → peaq) is the failure mode most people hit: a fresh Base wallet has no ETH, so the bridge tx reverts on `insufficient funds`. Fund Base first. * For a deeper protocol view (LayerZero peers, ONFT contracts, supported chains) see the [Activate function](/peaqos/functions/activate) and the [SDK JS](/peaqos/sdk-reference/sdk-js) / [SDK Python](/peaqos/sdk-reference/sdk-python) bridge methods, which this node mirrors 1:1. # ROS 2 configuration Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/configuration Configure peaqOS ROS 2 with a unified YAML file, local wallet registry, MCR API, faucet, and contract addresses. `peaqos_node` uses the same unified `peaq_robot.yaml` pattern as the rest of the robotics workspace. Keep populated config files out of version control. ## Minimal YAML ```yaml peaq_robot.yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: enabled: true rpc_url: "https://quicknode1.peaq.xyz" api_url: "https://mcr.peaq.xyz" faucet: base_url: "https://depinstation.peaq.xyz" qr_format: "svg" wallet_registry: path: "~/.peaq_robot/peaqos_wallets.json" ``` The ROS 2 node signs internally after looking up the local private key by EVM address. Do not pass private keys, mnemonics, or faucet TOTP secrets through ROS topics. ## Mainnet contracts The example config ships current peaq mainnet proxy defaults. Env override: `IDENTITY_REGISTRY_ADDRESS` Mainnet address: `0xb53Af985765031936311273599389b5B68aC9956` Env override: `IDENTITY_STAKING_ADDRESS` Mainnet address: `0x11c05A650704136786253e8685f56879A202b1C7` Env override: `EVENT_REGISTRY_ADDRESS` Mainnet address: `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` Env override: `MACHINE_NFT_ADDRESS` Mainnet address: `0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9` Env override: `DID_REGISTRY_ADDRESS` Mainnet address: `0x0000000000000000000000000000000000000800` Env override: `BATCH_PRECOMPILE_ADDRESS` Mainnet address: `0x0000000000000000000000000000000000000805` Env override: `MACHINE_ACCOUNT_FACTORY_ADDRESS` Mainnet address: `0x4A808d5A90A2c91739E92C70aF19924e0B3D527f` Env override: `MACHINE_NFT_ADAPTER_ADDRESS` Mainnet address: `0x9AD5408702EC204441A88589B99ADfC2514AFAE6` ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: contracts: identity_registry: "0xb53Af985765031936311273599389b5B68aC9956" identity_staking: "0x11c05A650704136786253e8685f56879A202b1C7" event_registry: "0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA" machine_nft: "0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9" did_registry: "0x0000000000000000000000000000000000000800" batch_precompile: "0x0000000000000000000000000000000000000805" machine_account_factory: "0x4A808d5A90A2c91739E92C70aF19924e0B3D527f" machine_nft_adapter: "0x9AD5408702EC204441A88589B99ADfC2514AFAE6" ``` ## Optional defaults Use defaults when multiple service calls use the same owner, proxy, or machine wallet. ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: defaults: owner_address: "0xOwner..." machine_address: "0xMachine..." proxy_address: "0xProxy..." ``` When a matching request field is empty, the node can use these defaults. Production automation should still pass explicit addresses where possible for auditability. ## Operational limits All zeros disable SDK-side event caps. ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: operational_limits: max_value_per_tx: 0 rate_limit_max_events: 0 rate_limit_window_seconds: 0 ``` ## Base bridge settings peaq to Base bridge calls use the peaq-side `machine_nft_adapter`. Base to peaq bridge calls additionally require a Base RPC URL and Base Machine NFT address in the service request. Known Base Machine NFT address: ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} 0xee8A521eA434b11F956E2402beC5eBfa753Babfa ``` Base to peaq bridge calls require Base ETH on the signer wallet for gas. ## Launch ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml ``` Next, use the [service catalog](/peaqos/sdk-reference/ros2/services) or run the [ROS 2 machine runtime guide](/peaqos/guides/ros2-machine-runtime). # Installation Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/installation Build the peaqOS ROS 2 packages and configure the unified peaq_robot.yaml. The peaqOS ROS 2 packages live in [`peaqnetwork/peaq-robotics-ros2`](https://github.com/peaqnetwork/peaq-robotics-ros2). ## Prerequisites | Requirement | Value | | :------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ROS 2 | Humble (Docker) or Jazzy (native Ubuntu) | | Python | ≥ 3.10 (Humble) or ≥ 3.12 (Jazzy). `peaq-os-sdk` 0.6.0 and 0.7.0 need 3.12; 0.7.1+ runs on 3.10 again | | `peaq-os-sdk` | ≥ 0.0.2 (PyPI, pulled in by the workspace). On Humble (Python 3.10) pin `peaq-os-sdk<0.4.0`: 0.4.0 and 0.5.0 need 3.11, 0.6.0 and 0.7.0 need 3.12, 0.7.1+ runs on 3.10. The services drive the Tokenomics 1.0 flow either way | | RPC | peaq mainnet or agung testnet EVM JSON-RPC | | Optional | Base mainnet RPC for the peaq ↔ Base bridge | ## Workspace build The Docker image ships with ROS 2 Humble, IPFS (Kubo), and Python deps preinstalled. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 docker build -t peaq-ros2:latest . docker run -it --rm \ --name peaq-ros2-dev \ -v "$(pwd)":/work \ -w /work \ -p 5001:5001 -p 8080:8080 \ peaq-ros2:latest # Inside the container: source /opt/ros/humble/setup.bash colcon build --packages-select \ peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash ``` ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 pip install -r requirements.txt source /opt/ros/jazzy/setup.bash colcon build --packages-select \ peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash ``` Add `peaq_ros2_tether`, `peaq_ros2_core`, or `peaq_ros2_openclaw` to the `--packages-select` list when you need the matching node alongside peaqOS. ## Unified config The node reads from a single `peaq_robot.yaml`. Start from the example and fill in placeholders: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cp peaq_ros2_examples/config/peaq_robot.example.yaml \ peaq_ros2_examples/config/peaq_robot.yaml ``` ### `peaq_os` block ```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaq_os: enabled: true # peaq EVM JSON-RPC used by peaq-os-sdk rpc_url: "https://quicknode1.peaq.xyz" # Hosted MCR API. Use http://127.0.0.1:8000 to read from a local MCR server. api_url: "https://mcr.peaq.xyz" faucet: base_url: "https://depinstation.peaq.xyz" qr_format: "svg" wallet_registry: path: "~/.peaq_robot/peaqos_wallets.json" defaults: owner_address: "" machine_address: "" proxy_address: "" contracts: # Current peaq mainnet proxy addresses. identity_registry: "0xb53Af985765031936311273599389b5B68aC9956" identity_staking: "0x11c05A650704136786253e8685f56879A202b1C7" event_registry: "0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA" machine_nft: "0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9" did_registry: "0x0000000000000000000000000000000000000800" batch_precompile: "0x0000000000000000000000000000000000000805" # Required only for smart-account and peaq → Base bridge calls. machine_account_factory: "0x4A808d5A90A2c91739E92C70aF19924e0B3D527f" machine_nft_adapter: "0x9AD5408702EC204441A88589B99ADfC2514AFAE6" operational_limits: # All zeros disable SDK-side event limits. max_value_per_tx: 0 rate_limit_max_events: 0 rate_limit_window_seconds: 0 ``` For agung testnet addresses see [Install → Agung testnet contracts](/peaqos/install#agung-testnet-contracts). ### Environment overrides The same overrides recognized by the [Python SDK](/peaqos/sdk-reference/sdk-python) and [JS SDK](/peaqos/sdk-reference/sdk-js) work here. Useful when you pin contract addresses externally: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export EVENT_REGISTRY_ADDRESS=0x... export MACHINE_ACCOUNT_FACTORY_ADDRESS=0x... export MACHINE_NFT_ADAPTER_ADDRESS=0x... export BATCH_PRECOMPILE_ADDRESS=0x... ``` ## Run the node ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml ``` Confirm services are registered: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 node list | grep peaqos_node ros2 service list | grep /peaqos_node/ ``` Run the node in the background and tail logs: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} nohup ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=/work/peaq_ros2_examples/config/peaq_robot.yaml \ > /tmp/peaqos_node.log 2>&1 & tail -f /tmp/peaqos_node.log ``` If multiple ROS 2 environments share a host, isolate them with `ROS_DOMAIN_ID` to prevent service collisions. ## Production checklist * Use a local `peaq_robot.yaml`; do not commit machine private keys, faucet codes, or RPC tokens. * Keep `peaq_os.wallet_registry.path` on encrypted robot storage when possible. Permissions `0600`. * Use a reliable peaq EVM RPC endpoint and monitor rate limits. * Record pre/post balances for any production bridge or event-spend test. * Fund Base ETH on the signer before attempting Base → peaq bridge. * Pin peaqOS contract addresses to the values in [Install](/peaqos/install#peaq-mainnet-contracts) unless deployment docs change. ## Troubleshooting The workspace expects `peaq-os-sdk>=0.0.2`. Install it inside the same Python env that runs `ros2`: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install 'peaq-os-sdk>=0.0.2' ``` Verify with `python3 -c "import peaq_os_sdk; print(peaq_os_sdk.__version__)"`. Quote EVM addresses and large integers in `ros2 service call` payloads: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/wallet/get \ peaq_ros2_interfaces/srv/PeaqosGetWallet \ "{address: '0xAbC...'}" ``` `peaq_os.contracts.machine_nft_adapter` is required only when bridging from peaq. Set it in `peaq_robot.yaml` or export `MACHINE_NFT_ADAPTER_ADDRESS`. Agung has no LayerZero DVN routes — test bridging on peaq mainnet ↔ Base mainnet only. Continue to the full [service reference](/peaqos/sdk-reference/ros2/services). # SDK: ROS 2 Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/overview ROS 2 services that turn a robot into an on-chain machine actor. `peaq_ros2_peaqos` maps the peaqOS Python SDK capability surface into typed ROS 2 services. It is for robot teams that already run ROS 2 and want peaqOS primitives without passing private keys through ROS messages. ROS 2 stays the robot control plane; peaqOS becomes the machine economy and identity plane. **Tokenomics 1.0 surface.** The ROS 2 services wrap the Python SDK's `register_machine`, `register_for`, `mint_nft`, and DID-attribute calls, which onboard machines on the Tokenomics 1.0 contracts. Economics 2.0 activation (`activate_machine`, released in `peaq-os-sdk` 0.6.0 on 2026-09-04) has no ROS 2 service yet. The workspace pins `peaq-os-sdk>=0.0.2`, which now resolves to 0.6.0+; that version needs Python 3.12, so on ROS 2 Humble (Python 3.10) pin `peaq-os-sdk<0.4.0` (0.4.0 and 0.5.0 already need 3.11). See [Activate](/peaqos/functions/activate). ## What it unlocks Create local EVM machine wallets, register machines, and fund them through the Gas Station. Mint Machine NFTs and write the standard DID attributes the MCR API understands. Validate single events locally, submit one event, or batch-submit atomically through the peaq batch precompile. Query MCR, machine profiles, and operator fleets from the hosted MCR API. Preview and deploy deterministic ERC-4337 smart accounts through `MachineAccountFactory`. Bridge Machine NFTs between peaq and Base via LayerZero V2. ## Security model ```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}} Robot process | | ROS 2 service call (address only, no keys) v peaqos_node | | local wallet lookup by EVM address v peaq-os-sdk (PyPI) ---> peaq EVM RPC, MCR API, Gas Station API, Base RPC | v peaqOS contracts and services ``` * Wallet private keys live in one local registry file on the robot or machine (default `~/.peaq_robot/peaqos_wallets.json`, perms `0600`). * ROS callers pass EVM **addresses**, not private keys. * The node never accepts or returns private keys over ROS. * Wallet `list` / `get` / `delete` expose only public metadata: `address`, `account_id`, `chain_id`, `network`, `label`, `created_at`. * Faucet 2FA codes are request-only and should not be logged. ## Service map | Area | Service | Purpose | | :------------ | :------------------------------------------ | :----------------------------------------------- | | Wallet | `/peaqos_node/wallet/create` | Create a locally stored EVM wallet | | Wallet | `/peaqos_node/wallet/list` | List local wallet public metadata | | Wallet | `/peaqos_node/wallet/get` | Get one wallet's public metadata | | Wallet | `/peaqos_node/wallet/delete` | Delete one local wallet | | Faucet | `/peaqos_node/faucet/setup_2fa` | Start faucet 2FA enrollment | | Faucet | `/peaqos_node/faucet/confirm_2fa` | Confirm faucet 2FA | | Faucet | `/peaqos_node/wallet/fund` | Request Gas Station funding | | Onboarding | `/peaqos_node/machine/register` | Register the local machine wallet | | Onboarding | `/peaqos_node/agent/register` | Same handler, agent-flavored alias | | Onboarding | `/peaqos_node/machine/register_for` | Register a machine through a proxy operator | | Onboarding | `/peaqos_node/agent/register_for` | Same handler, agent-flavored alias | | NFT | `/peaqos_node/nft/mint` | Mint a Machine NFT | | NFT | `/peaqos_node/nft/token_id_of` | Read a machine's NFT token ID | | DID | `/peaqos_node/did/read_attribute` | Read one DID precompile attribute | | DID | `/peaqos_node/did/write_machine_attributes` | Write the standard machine DID attributes | | DID | `/peaqos_node/did/write_proxy_attributes` | Write proxy/operator DID attributes | | Events | `/peaqos_node/events/validate` | Validate event payload and compute data hash | | Events | `/peaqos_node/events/submit` | Submit one event through `EventRegistry` | | Events | `/peaqos_node/events/batch_submit` | Atomic batch submit through the batch precompile | | MCR | `/peaqos_node/mcr/query` | Query Machine Credit Rating | | MCR | `/peaqos_node/mcr/machine` | Query machine profile | | MCR | `/peaqos_node/mcr/operator_machines` | Query an operator's fleet | | Smart account | `/peaqos_node/smart_account/address` | Predict deterministic smart-account address | | Smart account | `/peaqos_node/smart_account/deploy` | Deploy machine smart account | | Bridge | `/peaqos_node/bridge/nft` | Bridge Machine NFT between peaq and Base | | Bridge | `/peaqos_node/bridge/wait_arrival` | Poll destination chain until bridged NFT arrives | Full service-by-service reference with example payloads is on [Services](/peaqos/sdk-reference/ros2/services). Bridge has its own page on [Bridge](/peaqos/sdk-reference/ros2/bridge). ## Quickstart Build and source the workspace, copy the example config, then start the node against your unified config: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 source /opt/ros/jazzy/setup.bash # Use /opt/ros/humble/setup.bash inside the Docker image. # Native host only: python3 -m pip install -r requirements.txt colcon build --packages-select peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash cp peaq_ros2_examples/config/peaq_robot.example.yaml peaq_ros2_examples/config/peaq_robot.yaml # Edit peaq_ros2_examples/config/peaq_robot.yaml and set peaq_os.enabled: true. ros2 run peaq_ros2_peaqos peaqos_node --ros-args \ -p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml ``` The node reads one unified YAML file. See [ROS 2 configuration](/peaqos/sdk-reference/ros2/configuration) for production settings and contract addresses. Create a wallet, register, mint, and submit an event: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/wallet/create \ peaq_ros2_interfaces/srv/PeaqosCreateWallet \ "{label: 'robot-001'}" ros2 service call /peaqos_node/machine/register \ peaq_ros2_interfaces/srv/PeaqosRegisterMachine \ "{address: ''}" ros2 service call /peaqos_node/nft/mint \ peaq_ros2_interfaces/srv/PeaqosMintNft \ "{signer_address: '', machine_id: 1, recipient: ''}" ros2 service call /peaqos_node/events/submit \ peaq_ros2_interfaces/srv/PeaqosSubmitEvent \ "{signer_address: '', machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x', trust_level: 1, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}" ``` End-to-end commands are in [ROS 2 machine runtime](/peaqos/guides/ros2-machine-runtime). ## ROS distro support | Distro | Status | | :----------- | :------------------------------- | | ROS 2 Humble | Supported (Docker image default) | | ROS 2 Jazzy | Supported (native Ubuntu hosts) | Native install on non-Linux hosts is best done via Docker or WSL. ## What's tested on mainnet This release path has been exercised on a ROS 2 Jazzy Ubuntu server against peaq mainnet: * Wallet create / list / get / delete public-metadata lifecycle * Machine NFT token lookup * DID `readAttribute` * MCR query, machine query, and operator machines query * Event validation, single submission, and batch submission via the batch precompile * Deterministic smart-account address calculation * Smart-account deployment through `MachineAccountFactory` * peaq to Base Machine NFT bridge through `MachineNFTAdapter` * Base arrival polling for the bridged Machine NFT Reverse Base to peaq bridge requires Base ETH on the signer wallet for gas. ## Keep going YAML fields, contract addresses, wallet registry, API URLs, and environment overrides. Canonical machine endpoints plus documented agent aliases, with request fields and examples. Sign, encrypt, and sell machine data from a ROS 2 node — the on-machine half of Stream. Run the machine onboarding, event, MCR, smart-account, and bridge flow from ROS 2. The PyPI SDK surface wrapped by the ROS 2 node. The JavaScript SDK surface used for peaqOS application integrations. # ROS 2 service catalog Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/services Canonical peaqOS ROS 2 machine endpoints plus documented agent aliases, grouped by SDK capability. This page lists the canonical machine endpoints and documented agent aliases exposed under `/peaqos_node`. Requests use ROS 2 service types from `peaq_ros2_interfaces`. All examples assume the node is running with a populated `peaq_robot.yaml`; see [ROS 2 configuration](/peaqos/sdk-reference/ros2/configuration). Quote EVM addresses and large integers in `ros2 service call` YAML payloads. Unquoted addresses can parse as numbers and break the request. ## Wallets Local EVM wallet registry. Service calls return public metadata only: `address`, `account_id`, `chain_id`, `network`, `label`, and `created_at`. Private keys never cross ROS. | Service | Type | | :--------------------------- | :-------------------------------------------- | | `/peaqos_node/wallet/create` | `peaq_ros2_interfaces/srv/PeaqosCreateWallet` | | `/peaqos_node/wallet/list` | `peaq_ros2_interfaces/srv/PeaqosListWallets` | | `/peaqos_node/wallet/get` | `peaq_ros2_interfaces/srv/PeaqosGetWallet` | | `/peaqos_node/wallet/delete` | `peaq_ros2_interfaces/srv/PeaqosDeleteWallet` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Create ros2 service call /peaqos_node/wallet/create \ peaq_ros2_interfaces/srv/PeaqosCreateWallet \ "{label: 'robot-001'}" # List ros2 service call /peaqos_node/wallet/list \ peaq_ros2_interfaces/srv/PeaqosListWallets "{}" # Get ros2 service call /peaqos_node/wallet/get \ peaq_ros2_interfaces/srv/PeaqosGetWallet \ "{address: ''}" # Delete ros2 service call /peaqos_node/wallet/delete \ peaq_ros2_interfaces/srv/PeaqosDeleteWallet \ "{address: ''}" ``` SDK import, export, and `from_wallet` flows are intentionally **not** exposed over ROS because they require private keys, mnemonics, or passphrases. Use the [Python SDK](/peaqos/sdk-reference/sdk-python) or [CLI](/peaqos/cli) for those. ## Gas Station Gas Station onboarding for fresh machine wallets. 2FA codes are request-only and should not be logged. | Service | Type | | :-------------------------------- | :------------------------------------------------ | | `/peaqos_node/faucet/setup_2fa` | `peaq_ros2_interfaces/srv/PeaqosSetupFaucet2FA` | | `/peaqos_node/faucet/confirm_2fa` | `peaq_ros2_interfaces/srv/PeaqosConfirmFaucet2FA` | | `/peaqos_node/wallet/fund` | `peaq_ros2_interfaces/srv/PeaqosFundWallet` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # 1. Start 2FA enrollment for the owner ros2 service call /peaqos_node/faucet/setup_2fa \ peaq_ros2_interfaces/srv/PeaqosSetupFaucet2FA \ "{owner_address: '', qr_format: 'svg'}" # 2. Confirm with the OTP from the authenticator ros2 service call /peaqos_node/faucet/confirm_2fa \ peaq_ros2_interfaces/srv/PeaqosConfirmFaucet2FA \ "{owner_address: '', two_factor_code: '123456'}" # 3. Fund the target wallet through the Gas Station ros2 service call /peaqos_node/wallet/fund \ peaq_ros2_interfaces/srv/PeaqosFundWallet \ "{owner_address: '', target_address: '', chain_id: '3338', two_factor_code: '123456', request_id: ''}" ``` See the [error reference](/peaqos/sdk-reference/errors) for `INVALID_2FA`, `2FA_LOCKED`, `RATE_LIMITED`, and `CAP_EXCEEDED_OWNER`. ## Registration | Service | Type | | :---------------------------------- | :----------------------------------------------- | | `/peaqos_node/machine/register` | `peaq_ros2_interfaces/srv/PeaqosRegisterMachine` | | `/peaqos_node/agent/register` | `peaq_ros2_interfaces/srv/PeaqosRegisterAgent` | | `/peaqos_node/machine/register_for` | `peaq_ros2_interfaces/srv/PeaqosRegisterFor` | | `/peaqos_node/agent/register_for` | `peaq_ros2_interfaces/srv/PeaqosRegisterFor` | `/machine/register` and `/agent/register` are interchangeable surfaces over the same registration call. Pick whichever language matches your fleet model. `/machine/register_for` and `/agent/register_for` are likewise the same handler under two paths. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Self-managed: machine signs for itself ros2 service call /peaqos_node/machine/register \ peaq_ros2_interfaces/srv/PeaqosRegisterMachine \ "{address: ''}" # Equivalent agent-flavored alias ros2 service call /peaqos_node/agent/register \ peaq_ros2_interfaces/srv/PeaqosRegisterAgent \ "{address: ''}" # Proxy operator: operator signs on behalf of a fleet machine ros2 service call /peaqos_node/machine/register_for \ peaq_ros2_interfaces/srv/PeaqosRegisterFor \ "{proxy_address: '', machine_address: ''}" ``` For background on the two patterns, see [Self-managed onboarding](/peaqos/guides/self-managed-onboarding) and [Proxy operator fleet](/peaqos/guides/proxy-operator-fleet). ## Machine NFT | Service | Type | | :----------------------------- | :----------------------------------------- | | `/peaqos_node/nft/mint` | `peaq_ros2_interfaces/srv/PeaqosMintNft` | | `/peaqos_node/nft/token_id_of` | `peaq_ros2_interfaces/srv/PeaqosTokenIdOf` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/nft/mint \ peaq_ros2_interfaces/srv/PeaqosMintNft \ "{signer_address: '', machine_id: 1, recipient: ''}" ros2 service call /peaqos_node/nft/token_id_of \ peaq_ros2_interfaces/srv/PeaqosTokenIdOf \ "{signer_address: '', machine_id: 1}" ``` ## DID attributes | Service | Type | | :------------------------------------------ | :--------------------------------------------------------- | | `/peaqos_node/did/read_attribute` | `peaq_ros2_interfaces/srv/PeaqosReadDidAttribute` | | `/peaqos_node/did/write_machine_attributes` | `peaq_ros2_interfaces/srv/PeaqosWriteMachineDidAttributes` | | `/peaqos_node/did/write_proxy_attributes` | `peaq_ros2_interfaces/srv/PeaqosWriteProxyDidAttributes` | `/did/write_machine_attributes` is the standard machine DID write path the MCR API consumes. It writes `machineId`, `nftTokenId`, `operatorDid`, `documentationUrl`, `dataApi`, and `dataVisibility` in one call. `/did/write_proxy_attributes` is the proxy/operator equivalent; it atomically writes the two standard proxy DID attributes (`proxy_agent_id` and the list of `machine_ids` the operator manages). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Read one attribute by name from the DID precompile ros2 service call /peaqos_node/did/read_attribute \ peaq_ros2_interfaces/srv/PeaqosReadDidAttribute \ "{signer_address: '', did_address: '', name: 'machineId'}" # Write the standard machine attributes in one shot ros2 service call /peaqos_node/did/write_machine_attributes \ peaq_ros2_interfaces/srv/PeaqosWriteMachineDidAttributes \ "{signer_address: '', machine_id: 1, nft_token_id: 1, operator_did: 'did:peaq:', documentation_url: 'https://docs.example/robot-001', data_api: 'https://api.example/robot-001', data_visibility: 'onchain'}" # Write proxy/operator DID attributes ros2 service call /peaqos_node/did/write_proxy_attributes \ peaq_ros2_interfaces/srv/PeaqosWriteProxyDidAttributes \ "{signer_address: '', proxy_agent_id: 1, machine_ids: [1, 2, 3]}" ``` ## Events | Service | Type | | :--------------------------------- | :------------------------------------------------- | | `/peaqos_node/events/validate` | `peaq_ros2_interfaces/srv/PeaqosValidateEvent` | | `/peaqos_node/events/submit` | `peaq_ros2_interfaces/srv/PeaqosSubmitEvent` | | `/peaqos_node/events/batch_submit` | `peaq_ros2_interfaces/srv/PeaqosBatchSubmitEvents` | `/events/validate` runs locally: it computes the data hash and checks the payload before you spend gas. `/events/batch_submit` goes through the peaq batch precompile (`0x...0805`) so a multi-event submission is atomic on chain. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Validate locally ros2 service call /peaqos_node/events/validate \ peaq_ros2_interfaces/srv/PeaqosValidateEvent \ "{machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x', trust_level: 1, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}" # Submit one event ros2 service call /peaqos_node/events/submit \ peaq_ros2_interfaces/srv/PeaqosSubmitEvent \ "{signer_address: '', machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x', trust_level: 1, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}" # Atomic batch ros2 service call /peaqos_node/events/batch_submit \ peaq_ros2_interfaces/srv/PeaqosBatchSubmitEvents \ "{signer_address: '', events_json: '[{\"machine_id\":1,\"event_type\":1,\"value\":1,\"timestamp\":1770000000,\"raw_data_hex\":\"0x\",\"trust_level\":1,\"source_chain_id\":3338,\"source_tx_hash\":\"\",\"metadata_hex\":\"0x7b7d\"}]'}" ``` For event field rules, see [Events](/peaqos/concepts/events) and [SDK JS event validation](/peaqos/sdk-reference/sdk-js#validatesubmiteventparams). ## MCR queries Hosted MCR API queries. Returned bodies are SDK JSON passthrough; operator-fleet results may include per-machine `negative_flag` and top-level `pagination`. | Service | Type | | :----------------------------------- | :----------------------------------------------------- | | `/peaqos_node/mcr/query` | `peaq_ros2_interfaces/srv/PeaqosQueryMcr` | | `/peaqos_node/mcr/machine` | `peaq_ros2_interfaces/srv/PeaqosQueryMachine` | | `/peaqos_node/mcr/operator_machines` | `peaq_ros2_interfaces/srv/PeaqosQueryOperatorMachines` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/mcr/query \ peaq_ros2_interfaces/srv/PeaqosQueryMcr \ "{did: 'did:peaq:'}" ros2 service call /peaqos_node/mcr/machine \ peaq_ros2_interfaces/srv/PeaqosQueryMachine \ "{did: 'did:peaq:'}" ros2 service call /peaqos_node/mcr/operator_machines \ peaq_ros2_interfaces/srv/PeaqosQueryOperatorMachines \ "{did: 'did:peaq:'}" ``` The full HTTP API is documented under [API Reference](/peaqos/api-reference/overview). ## Smart accounts ERC-4337 machine accounts are deployed through `MachineAccountFactory`. Address prediction is deterministic (CREATE2 salt) and cheap; call `address` first, deploy second. | Service | Type | | :----------------------------------- | :------------------------------------------------------ | | `/peaqos_node/smart_account/address` | `peaq_ros2_interfaces/srv/PeaqosGetSmartAccountAddress` | | `/peaqos_node/smart_account/deploy` | `peaq_ros2_interfaces/srv/PeaqosDeploySmartAccount` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # Predict address (no tx) ros2 service call /peaqos_node/smart_account/address \ peaq_ros2_interfaces/srv/PeaqosGetSmartAccountAddress \ "{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}" # Deploy ros2 service call /peaqos_node/smart_account/deploy \ peaq_ros2_interfaces/srv/PeaqosDeploySmartAccount \ "{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}" ``` `daily_limit` is kept on the service for backward compatibility. The current `MachineAccountFactory` ignores it, so an empty string is fine. Both calls require `peaq_os.contracts.machine_account_factory` (or `MACHINE_ACCOUNT_FACTORY_ADDRESS`) to be set. ## Bridge | Service | Type | | :--------------------------------- | :---------------------------------------------------- | | `/peaqos_node/bridge/nft` | `peaq_ros2_interfaces/srv/PeaqosBridgeNft` | | `/peaqos_node/bridge/wait_arrival` | `peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival` | ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 service call /peaqos_node/bridge/nft \ peaq_ros2_interfaces/srv/PeaqosBridgeNft \ "{signer_address: '', token_id: 1, source: 'peaq', destination: 'base', recipient: '', base_rpc_url: '', base_nft_address: '', options_hex: ''}" ros2 service call /peaqos_node/bridge/wait_arrival \ peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival \ "{dst_rpc_url: 'https://mainnet.base.org', dst_nft_address: '0xee8A521eA434b11F956E2402beC5eBfa753Babfa', token_id: 1, timeout: 900}" ``` Bridging has its own page: see [Bridge](/peaqos/sdk-reference/ros2/bridge) for the full peaq to Base flow, gas requirements, and arrival polling. Reverse Base to peaq bridging requires Base ETH on the signer wallet. ## Tips * Source ROS and the workspace before every session: `source /opt/ros//setup.bash && source install/setup.bash`. * Stream logs in another terminal: `tail -f /tmp/peaqos_node.log`. * For batched onboarding flows, prefer `/events/batch_submit` over multiple `/events/submit` calls: single tx, lower fees. # Stream Edge Agent Source: https://docs.peaq.xyz/peaqos/sdk-reference/ros2/stream-agent The peaq_ros2_stream node: subscribe to ROS 2 topics, sign and encrypt machine data on the robot, chunk it, store it, anchor it on peaq, and serve it to buyers. The **Stream Edge Agent** is the on-machine half of [Stream](/peaqos/functions/stream). It is a ROS 2 node (`stream_agent_node`, package `peaq_ros2_stream`) that subscribes to the topics you allow, applies Data Event Map field rules, then signs, encrypts, and chunks each message, stores the ciphertext, posts a signed manifest to the Stream backend, anchors the payload hash on peaq, and serves purchased chunks to buyers. Keys never leave the machine in the clear, and the agent publishes no ROS topics. It runs alongside the [`peaqos_node` runtime](/peaqos/sdk-reference/ros2/overview): the runtime provides the machine's identity and the `events/submit` service the agent calls to anchor data on-chain. For the trust model behind the chunks, see [Data streams](/peaqos/concepts/data-streams). ## How it works ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} flowchart LR Topics["ROS 2 topics"] -->|subscribe| Agent["stream_agent_node"] Policy["Data Event Map
(YAML)"] -->|configures| Agent Agent -->|field rules + sign
+ encrypt + chunk| Store["Storage adapter
local / Walrus / S3 / Drive"] Agent -->|signed manifest| Backend["Stream backend"] Agent -->|payload hash| Node["peaqos_node
events/submit → peaq"] Backend -->|paid order| Agent Agent -->|re-wrap keys + serve| Buyer["Buyer"] ``` Per inbound message: **capture → field transform → sign → encrypt → chunk → store → post manifest → anchor on-chain**. When an order is paid, the agent re-wraps that buyer's chunk keys and serves the encrypted chunks from a local delivery server. ## Install and run The agent lives in `peaq_ros2_stream` and depends on `peaq_ros2_interfaces` (for the `PeaqosSubmitEvent` service). ROS 2 Humble (Docker image) or Jazzy (native host) are both supported. `peaq_ros2_stream` lives on the `feature/peaqos-ros2-runtime` branch of `peaq-robotics-ros2` and has not been merged to the default branch, so the clone below checks that branch out. The `git checkout` step stays necessary until it lands. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 git checkout feature/peaqos-ros2-runtime # Stream package lives here until it merges to the default branch source /opt/ros/jazzy/setup.bash # Use /opt/ros/humble/setup.bash inside the Docker image. colcon build --packages-select peaq_ros2_interfaces peaq_ros2_stream source install/setup.bash ``` Start it with the launch file (which declares the `config_yaml` argument): ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 launch peaq_ros2_stream stream_agent.launch.py \ config_yaml:=/path/to/stream_config.yaml ``` Or run the node directly: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ros2 run peaq_ros2_stream stream_agent_node --ros-args \ -p config.yaml_path:=/path/to/stream_config.yaml ``` Python dependencies (installed by `rosdep` or `pip`): `PyNaCl`, `PyYAML`, `boto3`, `google-api-python-client`, `google-auth`, `requests`. The agent is inert until `stream_agent.enabled` is `true`. Configuration resolves in this order, last wins: built-in defaults → the `config_yaml` file → `PEAQOS_STREAM_*` environment variables → ROS parameters → an optional standalone `policy_path` file. ## Configure: the Data Event Map The agent reads one YAML block, `stream_agent` (the alias `stream` also works; keys accept snake\_case or camelCase). This is the [Data Event Map](/peaqos/concepts/data-streams): what to read, how to protect each field, where ciphertext goes, and who can decrypt. ```yaml stream_config.yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}} stream_agent: enabled: true # inert unless true # Identity + backend (all four required when enabled) machine_id: "mach_01HXYZ" agent_id: "agent_ros2_stream" agent_token: "stk_live_xxx" # per-agent secret — prefer env/policy file identity_ref: "did:peaq:0xMACHINE" # written into every manifest signature api_base_url: "https://stream.example.com" # your Stream API host; default http://127.0.0.1:8000 api_key: "" # optional; sent as Bearer # Topic subscriptions + per-field rules topics: - topic: "/battery_state" message_type: "sensor_msgs/msg/BatteryState" qos_preset: "sensor_data" # default | sensor_data | reliable field_rules: # ordered - { path: "voltage", action: "include" } - { path: "serial_number", action: "hash" } - { path: "location", action: "anonymize" } - { path: "raw_cells", action: "exclude" } - { path: "operator_note", action: "encrypt", public_key_hex: "aa11…(32 bytes)" } - topic: "/cmd_vel" message_type: "geometry_msgs/msg/Twist" qos_preset: "reliable" # Who can unwrap each chunk's key (≥1 required; buyers are added later, on purchase) key_recipients: - { recipient_id: "owner-1", recipient_type: "owner", public_key_hex: "11…(32 bytes)" } - { recipient_id: "operator-1", recipient_type: "operator", public_key_hex: "22…(32 bytes)" } - { recipient_id: "mach_01", recipient_type: "machine", public_key_hex: "33…(32 bytes)" } # Where encrypted chunks go (a local copy is always written first) storage: backend: "walrus" # local | walrus | s3 | google-drive walrus: publisher_url: "https://walrus-publisher.example" aggregator_url: "https://walrus-aggregator.example" epochs: 5 permanent: true # Anchor each payload hash on peaq via the peaqos_node runtime peaqos_event: enabled: true node_name: "peaqos_node" # → /peaqos_node/events/submit machine_id: 42 # on-chain machine id (uint64) trust_level: 0 # Local HTTP server that serves purchased chunks to buyers delivery: enabled: true host: "127.0.0.1" port: 8765 token: "deliver_xxx" # required when delivery.enabled ``` Validation is strict: when `enabled`, `machine_id` / `agent_id` / `agent_token` / `identity_ref` and at least one topic and one key recipient are required; `recipient_type` is `machine` / `owner` / `operator`; each `public_key_hex` is a 32-byte (64-hex) x25519 key; `storage.backend` outside the known set falls back to `local`; `delivery.token` is required if delivery is on. Local state defaults under `~/.peaq_robot/` (signing key, sequence counters, chunk `.bin` files, manifests, the SQLite catalog and key store) and is configurable per path. An offline `buffer` (SQLite) holds envelopes when the backend is unreachable and drains on a retry timer. ## Field rules Rules run **before** signing, so protected fields never leave the machine in the clear while the package stays verifiable. Rules are ordered; if any `include` rule is present the agent starts from an allow-list, otherwise from the full message. | Action | Effect | | :---------- | :--------------------------------------------------------------------------------------------------- | | `include` | Keep the field as-is (and, if used, allow-list it). | | `exclude` | Drop the field entirely. | | `hash` | Replace the value with `sha256:`. | | `anonymize` | Replace the value with `{ "redacted": true }`. | | `encrypt` | Seal the value to the rule's `public_key_hex` (x25519 sealed box); only that key holder can read it. | ## How a message becomes a sellable chunk 1. **Capture.** The subscription fires; the ROS message is converted to a canonical ordered dict. 2. **Transform.** Field rules apply (above). 3. **Sequence.** A monotonic per-(machine, topic, policy version) number is assigned. 4. **Sign.** The agent builds a `peaqos-stream-envelope@v1` carrying the `payloadHash` and signs it with the machine's **Ed25519** key (`signature.keyId`, `algorithm: "ed25519"`). 5. **Encrypt.** The transformed payload is encrypted with a fresh per-chunk **XChaCha20-Poly1305** key (32-byte key, 24-byte nonce), yielding `plaintextHash`, `encryptedDataHash`, and a `keyCommitment`. 6. **Chunk ID.** A deterministic `sha256:` id is computed over `{ schemaVersion, previousChunkId, index, plaintextHash, encryptedDataHash }`, forming a hash chain. 7. **Store.** The chosen adapter writes the ciphertext and returns a `storageRef`. 8. **Manifest.** A `peaq.stream.chunks.v1` manifest wraps the chunk key to every `key_recipient` (x25519 sealed box) and is Ed25519-signed over `encryptedDataHash`, then `POST`ed to `/api/v1/stream/chunks`. The envelope is `POST`ed to `/api/v1/stream/events`, returning a receipt. 9. **Anchor.** If `peaqos_event.enabled`, the agent calls `/peaqos_node/events/submit` (`PeaqosSubmitEvent`) with the payload hash as `raw_data_hex` and `{ streamReceiptId, policyId, policyVersion }` as `metadata_hex`, then patches the receipt with the returned `txHash`. On startup the agent syncs its policy to the backend and registers its Ed25519 **public** key (`keyId = {agent_id}-stream-ed25519`). ## Storage adapters Every adapter writes the local `.bin` first (so the delivery server always has a copy), then pushes to the remote. | Backend | Required config | `storageRef` | | :------------- | :---------------------------------------------------- | :-------------------------------- | | `local` | `chunk_storage_path` | `file://` | | `walrus` | `walrus.publisher_url` (+ `aggregator_url` for reads) | `walrus://` | | `s3` | `s3.bucket` (+ region / endpoint / creds) | `s3:////.bin` | | `google-drive` | `google_drive.folder_id` + `credentials_path` | `gdrive://` | ## On-chain anchoring Anchoring is optional and runs through the `peaqos_node` runtime, so that node must be up with `events/submit` available. The agent submits only the **payload hash** plus non-secret metadata — never raw data or keys — producing a tamper-evident on-chain record that links each Stream receipt to a peaq transaction. ## Selling and delivery The agent polls the backend for orders. When an order is `paid`, it looks up each purchased chunk's symmetric key from the local key store, re-wraps it to the buyer's public key (x25519 sealed box, `peaq.stream.buyer-access.v1`), and submits the access grant. The chunk data is never re-encrypted. If `delivery.enabled`, a token-gated local HTTP server serves the encrypted chunks: | Route | Returns | | :-------------------------- | :------------------------------------------- | | `GET /health` | `{ ok: true }` | | `GET /chunks` | Catalog list (filter by topic, time, status) | | `GET /chunks/{id}/manifest` | The `peaq.stream.chunks.v1` manifest | | `GET /chunks/{id}/data` | Raw encrypted bytes (supports HTTP Range) | ## Environment variables Every config field has a `PEAQOS_STREAM_*` override (applied above the YAML file, below ROS params) — use them to keep secrets out of committed YAML. The essentials: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} export PEAQOS_STREAM_ENABLED=true export PEAQOS_STREAM_API_BASE_URL=https://stream.example.com export PEAQOS_STREAM_MACHINE_ID=mach_01HXYZ export PEAQOS_STREAM_AGENT_ID=agent_ros2_stream export PEAQOS_STREAM_AGENT_TOKEN=stk_live_xxx export PEAQOS_STREAM_IDENTITY_REF=did:peaq:0xMACHINE export PEAQOS_STREAM_STORAGE_BACKEND=walrus ``` Storage credentials also accept the standard vendor variables: Walrus (`WALRUS_PUBLISHER_URL`, …), S3 (`AWS_ACCESS_KEY_ID`, `AWS_REGION`, `AWS_ENDPOINT_URL_S3`, …), and Google Drive (`GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_DRIVE_FOLDER_ID`). ## Security * The **Ed25519 signing key** is generated on first run, written to `signing_key_path` (`0600`), and never sent or logged — only the public key is registered and embedded in manifests. * **Per-chunk symmetric keys** live only in the local SQLite key store and travel only as sealed-box-wrapped blobs (to recipients, then to the buyer). Plaintext keys never appear in ROS messages, manifests, the backend payload, or logs. * **No secrets cross ROS.** The agent publishes no topics; its only ROS egress is the `events/submit` service call, which carries a hash and non-secret metadata. * The delivery server silences request logging and gates every route except `/health` behind `delivery.token`. Keep `agent_token`, `api_key`, `delivery.token`, and storage credentials in environment variables, not committed YAML. ## Related * [Stream function](/peaqos/functions/stream) * [Data streams concept](/peaqos/concepts/data-streams) * [ROS 2 overview](/peaqos/sdk-reference/ros2/overview) * [Stream SDK reference](/peaqos/sdk-reference/stream) * [peaqOS CLI: `peaqos stream`](/peaqos/cli#peaqos-stream) # SDK: JavaScript Source: https://docs.peaq.xyz/peaqos/sdk-reference/sdk-js TypeScript client for peaqOS. Class, static factories, methods, types, constants. `@peaqos/peaq-os-sdk` is the opinionated TypeScript entry point for the Machine Financial Passport flow. Each capability is exposed both as a `PeaqosClient` instance method and as a standalone function. **0.6.0 (2026-09-04) adds Economics 2.0.** Pass `tokenomics20: { deploymentId }` to the constructor to enter Tokenomics mode: one-transaction [activation](#tokenomics-2-0), machine management, and the 2.0 monetization client. In that mode the legacy registration, mint, bridge, event, and MCR-query methods throw typed errors instead of running. Without `tokenomics20` the client behaves as before, with one exception that applies to every 0.6.0 client: `SubmitEventParams.machineId` and `MachineEvent.machineId` are now `bigint`, not `number`. ## Install ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @peaqos/peaq-os-sdk viem dotenv ``` * **Node.js:** ≥ 22 * **TypeScript:** ≥ 5 * **Peer dependency:** `viem` (>= 2.47.10) * **Economics 2.0:** 0.6.0 or newer * **Exports:** ESM and CJS. No bundler workarounds. `dotenv` is optional but recommended: `PeaqosClient.fromEnv()` reads from `process.env`, so `import "dotenv/config"` at the top of your entry file is the simplest way to load `.env`. ## Environment variables | Variable | Required | Default | Purpose | | :-------------------------------- | :--------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------------- | | `PEAQOS_RPC_URL` | Yes | n/a | peaq chain RPC endpoint | | `PEAQOS_PRIVATE_KEY` | Yes | n/a | Owner private key (`0x` + 64 hex) | | `IDENTITY_REGISTRY_ADDRESS` | Yes | n/a | Identity Registry contract | | `IDENTITY_STAKING_ADDRESS` | Yes | n/a | Identity Staking contract | | `EVENT_REGISTRY_ADDRESS` | Yes | n/a | Event Registry contract | | `MACHINE_NFT_ADDRESS` | Yes | n/a | Machine NFT contract (LayerZero ONFT) | | `DID_REGISTRY_ADDRESS` | Yes | n/a | DID Registry precompile | | `BATCH_PRECOMPILE_ADDRESS` | Yes | n/a | Batch precompile for multi-call bonding | | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | No | n/a | `MachineAccountFactory`. Required only for `deploySmartAccount` / `getSmartAccountAddress`. | | `MACHINE_NFT_ADAPTER_ADDRESS` | No | n/a | `MachineNFTAdapter` (LayerZero ONFT adapter). Required only for `bridgeNft` when `source === "peaq"`. | | `PEAQOS_MCR_API_URL` | No | `http://127.0.0.1:8000` | MCR API base URL | | `PEAQOS_ORCHESTRATION_URL` | For Scale | n/a | Machine Markets orchestrator base URL (e.g. `https://orchestration.peaq.xyz`). Required to use `client.orchestration`. | | `PEAQOS_API_KEY` | Optional | n/a | Platform API key for `client.orchestration` (sent as `x-api-key`). | | `OWS_PASSPHRASE` | Wallet ops | — | OWS vault passphrase for create/import/export wallet helpers when no explicit passphrase is passed | ### peaq mainnet contracts Use these addresses for the contract-address variables (`IDENTITY_REGISTRY_ADDRESS` and friends — note they are unprefixed) when pointing at peaq mainnet. All contracts are UUPS upgradeable proxies; treat the addresses as the current proxy pointers. | Variable | Address | | :-------------------------------- | :------------------------------------------------------------------- | | `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` | | `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` | | `EVENT_REGISTRY_ADDRESS` | `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` | | `MACHINE_NFT_ADDRESS` | `0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9` | | `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` (peaq DID precompile) | | `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` (peaq batch precompile) | | `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x4A808d5A90A2c91739E92C70aF19924e0B3D527f` | | `MACHINE_NFT_ADAPTER_ADDRESS` | `0x9AD5408702EC204441A88589B99ADfC2514AFAE6` | For agung testnet addresses see [Install → Agung testnet contracts](/peaqos/install#agung-testnet-contracts). Bridging is mainnet-only: LayerZero has no DVN routes to agung. ## Machine Markets orchestration Scale lives under `client.orchestration` — the Machine Markets surface (machine identity proofs, agent pairings, skill registry, market search, orders, payments). Reach it by setting `PEAQOS_ORCHESTRATION_URL` (and optionally `PEAQOS_API_KEY`) before `PeaqosClient.fromEnv()`. Full method reference: [Orchestration (JS)](/peaqos/sdk-reference/orchestration-js). ## Client ### `PeaqosClient` **0.7.0 makes the client generic over its mode:** `PeaqosClient` with `readonly mode: M`. Construct a 2.0 client as `new PeaqosClient<"tokenomics20">({ ...config, tokenomics20: { deploymentId: "peaq-mainnet" } })`; without the type argument TypeScript infers `"legacy"` and the constructor call is a compile error. Query results carry `machineId: bigint` on a `tokenomics20` client and `number` on a legacy one. `fromEnv()` returns the union of both and `fromWallet()` preserves the config's mode; narrow on `client.mode`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class PeaqosClient { constructor(config: Readonly); readonly rpcUrl: string; readonly contracts: Readonly; readonly apiUrl: string; readonly operationalLimits: Readonly; readonly publicClient: PublicClient; readonly walletClient: WalletClient; readonly tokenomics20: ResolvedTokenomics20Deployment | undefined; // set when the config carries tokenomics20 get address(): Address; static fromEnv(): PeaqosClient; static fromWallet( nameOrId: string, passphrase: string | undefined, owsSigning: boolean | undefined, config: Readonly>, options?: WalletOptions, ): Promise; static generateKeypair(): Readonly<{ address: Address; privateKey: `0x${string}` }>; // Static wallet wrappers (thin pass-throughs to the module-level helpers) static createWallet(name: string, passphrase?: string, words?: 12 | 24, options?: WalletOptions): Promise; static importWallet(name: string, privateKey: string, passphrase?: string, chain?: ImportChain, options?: WalletOptions): Promise; static importWalletMnemonic(name: string, mnemonic: string, passphrase?: string, index?: number, options?: WalletOptions): Promise; static listWallets(options?: WalletOptions): Promise; static getWallet(nameOrId: string, options?: WalletOptions): Promise; static exportWallet(nameOrId: string, passphrase?: string, options?: WalletOptions): Promise; static deleteWallet(nameOrId: string, options?: WalletOptions): Promise; toJSON(): Record; } ``` RPC endpoint (non-empty). `0x` + 64 hex characters. All six contract addresses. MCR API URL. Defaults to `DEFAULT_API_URL`. Per-tx and rate-limit caps. All-zero disables limits. Returns a `PeaqosClient` instance. `toJSON()` and `util.inspect` output redact the private key (`"[REDACTED]"`). Other RPC endpoints are available. See [Public RPC endpoints](/peaqos/install#public-rpc-endpoints). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = new PeaqosClient({ rpcUrl: "https://peaq.api.onfinality.io/public", privateKey: "0xabc...def", contracts: { identityRegistry: "0x...", identityStaking: "0x...", eventRegistry: "0x...", machineNft: "0x...", didRegistry: "0x...", batchPrecompile: "0x...", }, }); console.log(client.address); // checksummed 0x address ``` **Errors:** `ValidationError`: missing/invalid `rpcUrl`, `privateKey`, or any contract address. ### `fromEnv` Since 0.7.0 `fromEnv()` reads `TOKENOMICS_DEPLOYMENT_ID`: a non-empty value selects Tokenomics mode, absent or empty stays legacy, an unknown or unreleased ID throws `TokenomicsConfigError` at construction. The return type is `PeaqosClient<"legacy"> | PeaqosClient<"tokenomics20">`; narrow on `client.mode`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} static fromEnv(): PeaqosClient; ``` Returns a fully configured `PeaqosClient`. All required env vars must be set (see [Environment variables](#environment-variables)). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = PeaqosClient.fromEnv(); ``` **Errors:** `ValidationError`: any required env var missing or empty. ### `fromWallet` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} static fromWallet( nameOrId: string, passphrase: string | undefined, owsSigning: boolean | undefined, config: Readonly>, options?: WalletOptions, ): Promise; ``` Builds a `PeaqosClient` from an OWS vault wallet. When `owsSigning` is `true` (default), signing routes through OWS: the key is decrypted only per-sign and wiped immediately after. When `false`, the key is decrypted at construction and signing uses viem directly. Wallet name or UUID in the OWS vault. Vault passphrase. Pass `undefined` to fall back to the `OWS_PASSPHRASE` env var. Route signing through OWS. Defaults to `true` when `undefined`. Client config without `privateKey` (the wallet provides the signer). Optional vault configuration (e.g. custom `vaultPath`). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const client = await PeaqosClient.fromWallet("my-machine", "s3cret", true, { rpcUrl: "https://peaq.api.onfinality.io/public", contracts: { identityRegistry: "0x...", identityStaking: "0x...", eventRegistry: "0x...", machineNft: "0x...", didRegistry: "0x...", batchPrecompile: "0x...", }, }); ``` **Errors:** `PeaqosError`: wallet not found, passphrase missing, or OWS signing failure. With `owsSigning: false` a wrong passphrase throws at construction (eager decrypt). With `owsSigning: true` a wrong passphrase surfaces on the first sign call. Key material is never decrypted at construction. ### Wallets (OWS) Wallet lifecycle helpers (`createWallet`, `importWallet`, `importWalletMnemonic`, `listWallets`, `getWallet`, `exportWallet`, `deleteWallet`, plus the `extractPeaqAddress` utility) back the [Open Wallet Standard](/peaqos/wallets) integration: mnemonic-backed encrypted vault, multi-chain accounts (peaq, Base, Ethereum, Solana, Bitcoin, etc.). Lifecycle helpers are available as module-level imports and as static methods on `PeaqosClient`; the `PeaqosClient.fromWallet` factory wires a vault wallet directly into a client (OWS-native signing by default). The JS package bundles `@open-wallet-standard/core` as a regular dependency; no separate peer install is required. The raw-key constructor and `fromEnv` flow keep working unchanged. Full reference on the [Wallets page](/peaqos/wallets#sdk-methods). ### `generateKeypair` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} static generateKeypair(): Readonly<{ address: Address; privateKey: `0x${string}`; }>; ``` Returns a frozen object with a fresh `secp256k1` `privateKey` and its derived `address`. No chain interaction. The private key never touches disk. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const kp = PeaqosClient.generateKeypair(); console.log(kp.address); // 0x... console.log(kp.privateKey); // 0x... ``` ### OWS wallet lifecycle OWS wallet helpers are available as static `PeaqosClient` methods and standalone functions. They derive multi-chain accounts, keep wallet material in an encrypted OWS vault, and return public `WalletInfo` metadata. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} static createWallet(name: string, passphrase?: string, words?: 12 | 24, options?: WalletOptions): Promise; static importWallet(name: string, privateKey: string, passphrase?: string, chain?: ImportChain, options?: WalletOptions): Promise; static importWalletMnemonic(name: string, mnemonic: string, passphrase?: string, index?: number, options?: WalletOptions): Promise; static listWallets(options?: WalletOptions): Promise; static getWallet(nameOrId: string, options?: WalletOptions): Promise; static exportWallet(nameOrId: string, passphrase?: string, options?: WalletOptions): Promise; static deleteWallet(nameOrId: string, options?: WalletOptions): Promise; static fromWallet(nameOrId: string, passphrase: string | undefined, owsSigning: boolean | undefined, config: Omit, options?: WalletOptions): Promise; ``` OWS wallet helpers are bundled with `@peaqos/peaq-os-sdk`. `createWallet`, `importWallet`, `importWalletMnemonic`, `exportWallet`, and `fromWallet` require a passphrase argument or `OWS_PASSPHRASE`. `options.vaultPath` can point at a custom vault directory. `fromWallet` can sign through OWS (`owsSigning=true`, default) so key material is decrypted only for the signing operation. Frozen object with `id`, `name`, `createdAt`, `keyType`, `peaqAddress`, and `accounts`. Each account has `accountId`, `address`, `chainId`, `network`, and `derivationPath`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosClient, IMPORT_CHAIN_EVM, extractPeaqAddress, } from "@peaqos/peaq-os-sdk"; const wallet = await PeaqosClient.createWallet("robot-001"); console.log(wallet.peaqAddress); const imported = await PeaqosClient.importWallet( "legacy-machine", "0xabc...def", undefined, IMPORT_CHAIN_EVM, ); const all = await PeaqosClient.listWallets(); const same = await PeaqosClient.getWallet(imported.id); console.log(all.length, extractPeaqAddress(same.accounts)); const client = await PeaqosClient.fromWallet(imported.id, undefined, true, { rpcUrl: "https://quicknode1.peaq.xyz", contracts: { identityRegistry: "0x...", identityStaking: "0x...", eventRegistry: "0x...", machineNft: "0x...", didRegistry: "0x0000000000000000000000000000000000000800", batchPrecompile: "0x0000000000000000000000000000000000000805", }, }); console.log(client.address); ``` `exportWallet` returns mnemonic or private-key material, and `fromWallet` consumes a vault passphrase. Keep these in local administrative tooling; do not expose them through robot control channels. ### Accessors All accessors are read-only. The private key is held in an ECMAScript `#private` field: never exposed through the public surface and redacted in `JSON.stringify` and `util.inspect`. | Accessor | Type | Description | | :------------------ | :---------------------------- | :-------------------------------------------------- | | `address` | `Address` | Checksummed owner address derived from `privateKey` | | `rpcUrl` | `string` | Configured RPC endpoint | | `contracts` | `Readonly` | Frozen contract address map | | `apiUrl` | `string` | MCR API base URL | | `operationalLimits` | `Readonly` | Per-tx + rate-limit caps | | `publicClient` | `PublicClient` (viem) | Read-only chain client | | `walletClient` | `WalletClient` (viem) | Signer-bound chain client | *** ## Tokenomics 2.0 Available on a client constructed with `tokenomics20: { deploymentId: "peaq-mainnet" | "agung-2026-08-28" }`. The seven contract addresses come from the SDK's snapshot (`TOKENOMICS_2_0_DEPLOYMENTS`, `resolveTokenomics20Deployment`), resolved at construction with no network call and verified against `InfoDesk.peer(role)` before every write. Addresses are never accepted from callers. Every function below is a `PeaqosClient` method and a package-root export taking the client first. Machine IDs are `bigint`; a `number` throws `ValidationError`. Source of truth in the repo: `docs/18_TOKENOMICS_ACTIVATION.md` and `docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md`. Concepts: [Economics 2.0](/peaqos/concepts/economics-2-0). ### `activateMachine` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async activateMachine(params: ActivateMachineParams): Promise; ``` One transaction to `MachineStateAndSync.activateMachine`: mints the ERC-721 in `MachineRegistry` (token ID equals machine ID), stores the DID document, bonds the tier in `MachineSubscription`, and records the home chain. The signer becomes owner and bond payer. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} interface ActivateMachineParams { controller: Address; verificationMethods: readonly VerificationMethodInput[]; // { id, methodType, controller, publicKeyMultibase } authentication: readonly bigint[]; // indices into verificationMethods serviceEndpoints: readonly ServiceEndpointInput[]; // { id, serviceType, serviceEndpoint } machineType: string; credentialSubject: Hex; manufacturer: Address; // recorded, never verified tier: SubscriptionTier; // 0 | 1 | 2 expectedMachineId?: bigint; maxNetPeaqAmount?: bigint; confirmations?: number; // default 1 timeoutMs?: number; // default 120_000 signal?: AbortSignal; } ``` `ActivateMachineResult`: `machineId`, `owner`, `controller`, `tier`, `bondAmount`, `voucherCreditApplied`, `netPeaqAmount`, `transactionHash`, `chainId`, `contract`, `method`, `receipt`, `events`, and the re-read subscription state. Amounts come from the receipt. Success requires `MachineOnboarded`, `MachineMinted`, and `Activated` from the right contracts (matched by emitting address) plus a matching post-state read. The write is never retried; a receipt timeout throws `RECEIPT_UNAVAILABLE` carrying the hash. ### `activateMachineWithUsdt` Same activation, bond settled in USDT through `SubscriptionTokenProvisionPool`. Requires `maxUsdtAmount`, taken from `previewMachineActivationWithUsdt(...)` after applying `slippageBps`. ### `previewMachineActivation`, `previewMachineActivationWithUsdt` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async previewMachineActivation(params: ActivateMachineParams): Promise; ``` Same validation and reads without signing, approving, or writing. Returns `machineId`, `bondAmount`, `voucherCredit`, `netPeaqAmount`, `balance`, `approvalRequired`. Throws `MACHINE_ID_MISMATCH` and `MAX_NET_PEAQ_EXCEEDED`; a low balance is returned, not thrown. ### `computeMachineId` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async computeMachineId(machineType: string, credentialSubject: Hex): Promise; ``` ### Reads ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} getMachineOwner(machineId: bigint): Promise
; // MACHINE_NOT_FOUND vs READ_FAILED kept apart getMachineActivationState(machineId: bigint): Promise; // activated when subscription.periodStart !== 0n getMachineManagementState(machineId: bigint): Promise; getMachineSubscription(machineId: bigint): Promise; getMachineAvailability(machineId: bigint): Promise; getMachineRelocationStatus({ machineId, destinationClient }): Promise; // status: pending | completed | cancelled | conflicting; a pending result carries stage: not_started | initiated | arrived ``` ### Lifecycle and subscription ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} suspendMachine(machineId: bigint, options?): Promise<...>; resumeMachine(machineId: bigint, options?): Promise<...>; previewExistingMachineActivation({ machineId, tier, payment: "peaq" | "usdt", slippageBps? }); activateExistingMachine({ machineId, tier, payment: "peaq", maxNetPeaqAmount } | { ..., payment: "usdt", maxUsdtAmount }); previewMachineRenewal({ machineId, payment, slippageBps? }); renewMachine({ machineId, payment: "peaq", maxNetPeaqAmount } | { machineId, payment: "usdt", maxUsdtAmount }); // stored tier, extends from the stored period end ``` PEAQ writes require `maxNetPeaqAmount`, USDT writes `maxUsdtAmount`; take both from the matching preview. The SDK re-quotes before simulation and never raises an accepted bound. Owner **or controller** may sign; credits accrue to the owner. ### Ownership (ERC-721) ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} approveMachine(operator: Address, machineId: bigint, options?); setMachineApprovalForAll(operator: Address, approved: boolean, options?); // every machine the signer owns, including future ones transferMachine(from: Address, to: Address, machineId: bigint, options?); safeTransferMachine(from: Address, to: Address, machineId: bigint, data?: Hex, options?); ``` Transfer changes the owner and **retains** the DID controller. Blocked while relocating. ### DID updates ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} setMachineController(machineId: bigint, controller: Address, options?); // owner only; clearing needs { confirmClearController: true } setMachineVerificationMethods(machineId: bigint, methods: readonly VerificationMethodInput[], options?); setMachineAuthentication(machineId: bigint, indices: readonly bigint[], options?); setMachineServiceEndpoints(machineId: bigint, endpoints: readonly ServiceEndpointInput[], options?); ``` Setters replace whole arrays. Shrinking verification methods below a live authentication index throws `AUTHENTICATION_REWRITE_REQUIRED` before submission. Ownership and DID writes accept a synchronous `onTransactionSubmitted(submission)` callback. ### `previewMachineAction` Side-effect-free preview for ownership and DID actions (not suspend/resume): contract, method, current state, intended effect. Never simulates, signs, or submits. ### Machine-ID helpers ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { UINT256_MAX, validateTokenomicsMachineId, parseTokenomicsMachineId, serializeTokenomicsMachineId } from "@peaqos/peaq-os-sdk"; ``` Canonical decimal strings at every JSON, URL, and log boundary. No `Number(bigint)`, no leading zeros except `"0"`. ### Disabled in Tokenomics mode | Call | Throws | | :-------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | | `registerMachine`, `mintNft`, `tokenIdOf` | `TokenomicsUnsupportedError` `LEGACY_REGISTRATION_UNSUPPORTED` | | `registerFor` | `TokenomicsUnsupportedError` `SPONSORED_ACTIVATION_UNSUPPORTED` (no replacement) | | `bridgeNft` | `TokenomicsUnsupportedError` `MACHINE_RELOCATION_UNAVAILABLE` | | `writeMachineDIDAttributes`, `writeProxyDIDAttributes` | `TokenomicsIntegrationUnavailableError` (`"legacy DID helpers"`) | | `client.orchestration` calls that bind a machine identity (pairings, machines, market orders, purchases, runtime endpoints, market search, …) | `TokenomicsIntegrationUnavailableError` (`"orchestration identity binding"`) | Enabled in Tokenomics mode since 0.7.0 (2026-09-11): `submitEvent`, `batchSubmitEvents` ([Events](#events-qualify)) and `queryMcr`, `queryMachine`, `queryOperatorMachines` ([Queries](#queries)). Still available in Tokenomics mode: orchestration health, skills, policies, market-service reads, audit reads, payment rails, delivery transports; heartbeat; provisioning; stream. `waitForBridgeArrival` is static and not gated; it is marked deprecated. *** ## Registration **Tokenomics 1.0 path.** `registerMachine` and `registerFor` address `IdentityRegistry` (1 PEAQ native bond, separate Machine NFT). Both are marked deprecated and throw `TokenomicsUnsupportedError` on a client constructed with `tokenomics20`. New machines use [`activateMachine`](#activatemachine). ### `registerMachine` Registers the caller's own address as a machine. Reads `minBond` from the `IdentityRegistry` contract (currently `1 PEAQ`) and sends that value with the transaction. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async registerMachine(): Promise; ``` The newly allocated machine ID, decoded from the `Registered` event in the transaction receipt. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const client = PeaqosClient.fromEnv(); const machineId = await client.registerMachine(); console.log(machineId); ``` * `RuntimeError`: chain revert (`AlreadyRegistered`, `IncorrectBondAmount`, or any other custom error), receipt missing the `Registered` log, or decoded `machineId` out of safe-integer range. See [errors](/peaqos/sdk-reference/errors). ### `registerFor` Registers a machine on behalf of another address. The caller becomes the proxy operator and supplies the current `minBond` (read from the IdentityRegistry contract) as `msg.value`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async registerFor(machineAddress: `0x${string}`): Promise; ``` Machine EOA. The client's signing address becomes the operator and pays the bond. The newly allocated machine ID for the proxied machine. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const client = PeaqosClient.fromEnv(); const { address: machineAddress } = PeaqosClient.generateKeypair(); const machineId = await client.registerFor(machineAddress); ``` * `ValidationError`: `machineAddress` is not a valid `0x` address. * `RuntimeError`: chain revert (`AlreadyRegistered`, `InvalidMachineAddress` for zero address, `IncorrectBondAmount`, or any other custom error), receipt missing the `Registered` log, or decoded `machineId` out of safe-integer range. The zero address is allowed through to the chain so the user-facing message comes from the `InvalidMachineAddress` revert mapping (contains `"zero address"`). *** ## Gas Station Call [`setupFaucet2FA`](#setupfaucet2fa) to enroll the owner. Call [`confirmFaucet2FA`](#confirmfaucet2fa) with a TOTP from the authenticator. Call [`fundFromGasStation`](#fundfromgasstation) to send gas to a machine wallet. ### `setupFaucet2FA` Enrolls an owner address for 2FA with the Gas Station. Returns a QR code URL (expires after \~2 minutes). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async setupFaucet2FA( ownerAddress: string, faucetBaseUrl: string, format?: FaucetQrFormat, ): Promise; ``` Owner to enroll (SS58 or hex). Gas Station base URL. QR format. Defaults to `"svg"`. The enrolled owner address. OTP auth URI for authenticator apps. QR code image URL. Expires after \~2 minutes. Render immediately, never persist. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const setup = await client.setupFaucet2FA( client.address, "https://depinstation.peaq.xyz", ); console.log(setup.otpauthUri); console.log(setup.qrImageUrl); ``` **Errors:** `ValidationError` on empty args. `RuntimeError` for `INVALID_OWNER_ADDRESS`, `INVALID_PAYLOAD`, `QR_GENERATION_FAILED`, unexpected envelope, or HTTP failure. See [errors](/peaqos/sdk-reference/errors). ### `confirmFaucet2FA` Confirms 2FA enrollment with a fresh TOTP code. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async confirmFaucet2FA( ownerAddress: string, faucetBaseUrl: string, twoFactorCode: string, ): Promise; ``` Owner address being confirmed. Gas Station base URL. Fresh 6-digit TOTP. Returns `void` on successful activation. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} await client.confirmFaucet2FA( client.address, "https://depinstation.peaq.xyz", "123456", ); ``` **Errors:** `ValidationError` on any empty argument. `RuntimeError` for `INVALID_2FA`, `2FA_NOT_CONFIGURED`, `2FA_LOCKED`, unexpected envelope, or transport failure. ### `fundFromGasStation` Sends gas tokens to a machine wallet. Returns a discriminated union on `status`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async fundFromGasStation( params: FundFromGasStationParams, faucetBaseUrl: string, ): Promise; ``` 2FA-enrolled owner (SS58 or hex). Machine EOA to fund. Faucet-configured chain identifier (e.g., `"peaq"`). Current TOTP. UUID idempotency key. Auto-generated if omitted. Transaction hash. Decimal wei. Never a JS number. **Cross-SDK behavior:** The JS SDK echoes `requestId` back in both success and skipped responses. The Python SDK does not include `request_id` in the response at all — it is a request-side idempotency key only. Do not write code that reads `requestId` from the response and expects it to work in both SDKs. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await client.fundFromGasStation( { ownerAddress: client.address, targetWalletAddress: machineAddress, chainId: "peaq", twoFactorCode: "123456", }, "https://depinstation.peaq.xyz", ); if (result.status === "success") { console.log("tx:", result.txHash, "amount:", result.fundedAmount); } else { console.log("already funded; balance:", result.currentBalance); } ``` * `ValidationError`: missing/empty required fields, `requestId` not a UUID, or `faucetBaseUrl` empty. * `RuntimeError`: any documented faucet code: `INVALID_2FA`, `2FA_NOT_CONFIGURED`, `2FA_NOT_ACTIVE`, `2FA_LOCKED`, `DUPLICATE_REQUEST`, `REQUEST_ALREADY_PROCESSED`, `RATE_LIMITED`, `CAP_EXCEEDED_OWNER`, `CAP_EXCEEDED_WALLET`, `TRANSFER_FAILED`. See [errors](/peaqos/sdk-reference/errors) for the full code → cause → retry table. *** ## NFT & DID **Tokenomics 1.0 path.** In Tokenomics mode `mintNft` and `tokenIdOf` throw `TokenomicsUnsupportedError` (minting happens inside `activateMachine`; the machine ID is the token ID) and the DID writers throw `TokenomicsIntegrationUnavailableError`. Use the [DID setters](#did-updates) instead. Machine NFT minting, token-ID lookup, and the two canonical DID attribute writers. The DID writes batch six (machine) or two (proxy) attributes into a single atomic `batchAll` transaction via the peaq Batch precompile. ### `mintNft` Mints a Machine NFT on the MachineNFT contract for a registered, bonded machine. Returns the transaction hash. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async mintNft( machineId: number, recipient: `0x${string}`, ): Promise; ``` Registered machine ID. Must be a positive integer. Address that will own the minted NFT. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const txHash = await client.mintNft(machineId, client.address); ``` * `ValidationError`: `machineId` not positive, `recipient` not a valid `0x` address, or `client.contracts.machineNft` not a valid address. * `RuntimeError`: chain revert (`MachineNotBonded`, `AlreadyMinted`, `NotMachineOwner`, `MachineNotFound`, `InvalidAddress`) or transaction failure. See [errors](/peaqos/sdk-reference/errors). ### `tokenIdOf` Reads the NFT token ID assigned to a registered machine via a view call. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async tokenIdOf(machineId: number): Promise; ``` Registered machine ID. Must be a positive integer. The NFT token ID, or `0` if no NFT has been minted for this machine. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const tokenId = await client.tokenIdOf(machineId); if (tokenId === 0) { console.log("No NFT minted yet"); } ``` * `ValidationError`: `machineId` not a positive integer, or `client.contracts.machineNft` not a valid address. * `RuntimeError`: contract returns a token ID beyond `Number.MAX_SAFE_INTEGER`. **Cross-SDK behavior:** The JS SDK returns `0` when no NFT has been minted for the machine. The Python SDK raises `RpcError` instead (the contract reverts). In polyglot codebases, check for `0` in JS and catch `RpcError` in Python — do not assume the same pattern works in both. ### `writeMachineDIDAttributes` Atomically writes the six canonical Machine DID attributes (`machineId`, `nftTokenId`, `operator`, `documentation_url`, `data_api`, `data_visibility`) to the caller's DID via a single batched transaction. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async writeMachineDIDAttributes( params: WriteMachineDIDParams, ): Promise; ``` Registered machine ID. NFT token ID assigned to the machine. Operator DID reference. May be an empty string. ASCII, ≤ 2560 bytes. Non-empty ASCII URL, ≤ 2560 bytes. Non-empty ASCII URL for the machine's data API, ≤ 2560 bytes. Visibility setting. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const txHash = await client.writeMachineDIDAttributes({ machineId: 42, nftTokenId: 1, operatorDid: "", documentationUrl: "https://docs.example.com", dataApi: "https://api.example.com", dataVisibility: "public", }); ``` ### `writeProxyDIDAttributes` Atomically writes the two canonical Proxy DID attributes (`machineId`, `machines`) to the caller's DID. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async writeProxyDIDAttributes( params: WriteProxyDIDParams, ): Promise; ``` The proxy operator's registered machine ID. Non-empty list of positive machine IDs managed by this proxy. The JSON-encoded array must be ≤ 2560 bytes. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const txHash = await client.writeProxyDIDAttributes({ proxyMachineId: 10, machineIds: [42, 43, 44], }); ``` ### `readAttribute` Reads a single DID attribute directly from the peaq DID precompile. Most consumers should prefer the [`/machine/{did}` API](/peaqos/api-reference/get-machine), which composes the full attribute set; this helper is the on-chain escape hatch. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function readAttribute( client: PeaqosClient, did: Address, name: string, ): Promise; ``` The DID account address whose attribute is being read (typically a machine address, but any DID-bearing EOA works). Attribute key, e.g. `"machineId"`, `"data_visibility"`, `"machines"`. `{ name: string; value: string; validity: number; created: bigint }`. `validity` is `0` when the attribute has no expiry. Throws `RuntimeError` if the attribute does not exist on the precompile. ### `encodeAddAttribute` Encodes ABI call data for the DID precompile's `addAttribute(didAccount, name, value, validFor)` function. Useful when constructing smart-account `executeBatch` calls that touch the DID precompile alongside other contracts. `writeMachineDIDAttributes` and `writeProxyDIDAttributes` use this helper internally; reach for it directly only when composing custom batch flows. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function encodeAddAttribute( didAccount: Address, name: string, value: string, validFor: number, ): Hex; ``` Address of the DID account being written. On-chain this must equal `msg.sender` of the resulting precompile call. Attribute name. ASCII only, ≤ 64 bytes. Attribute value. ASCII only, ≤ 2560 bytes. Validity period in blocks. `0` means no expiry. Must be a non-negative integer in the `uint32` range. ABI-encoded call data. Throws `ValidationError` if any constraint is violated. *** ## Smart accounts ERC-4337 smart accounts deployed via the `MachineAccountFactory`. Requires the client to be constructed with a `machineAccountFactory` address (or the `MACHINE_ACCOUNT_FACTORY_ADDRESS` env var via `fromEnv`). ### `deploySmartAccount` Deploys a smart account via `MachineAccountFactory.createAccount` and returns the deployed address. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async deploySmartAccount( params: DeploySmartAccountParams, ): Promise
; ``` EOA that will own the smart account. Machine EOA the account is scoped to. Non-negative CREATE2 salt. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const address = await client.deploySmartAccount({ owner: client.address, machine: machineAddress, salt: 0, }); ``` ### `getSmartAccountAddress` Read-only equivalent: computes the CREATE2 address for the given `(owner, machine, salt)` without deploying. Returns the same address `deploySmartAccount` would produce. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async getSmartAccountAddress( params: DeploySmartAccountParams, ): Promise
; ``` Same parameters as `deploySmartAccount`. No transaction, no gas. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const predicted = await client.getSmartAccountAddress({ owner: client.address, machine: machineAddress, salt: 0, }); ``` *** ## Bridge Supported routes: peaq ↔ Base. The peaq ↔ Solana lane is live on chain but not yet exposed here. In Tokenomics mode `bridgeNft` throws `MACHINE_RELOCATION_UNAVAILABLE`: Economics 2.0 relocates whole machine records and is disabled on chain today. See [Machine NFT cross-chain portability](/peaqos/concepts/machine-nft#cross-chain-portability). LayerZero v2 Machine NFT bridging between peaq and Base. Requires the `machineNftAdapter` address (or `MACHINE_NFT_ADAPTER_ADDRESS`) when sending from peaq. The SDK's `source` / `destination` literal union expands as peer contracts deploy on new chains. ### `bridgeNft` Bridges a Machine NFT from `source` to `destination`. When `source === "base"`, `baseRpcUrl` and `baseNftAddress` are required so the SDK can build a per-call viem client for the Base side. On the peaq→Base path the SDK runs an ERC-721 approval pre-flight: it checks `MachineNFT.getApproved(tokenId)` and submits a one-shot `approve(adapter, tokenId)` if the token isn't already cleared for the adapter. The Base→peaq path uses burn-and-unlock and needs no approval. Either way, callers don't handle approvals themselves. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async bridgeNft(params: BridgeNftParams): Promise; ``` Positive NFT id to bridge. Origin chain. Target chain (must differ from `source`). Destination-chain recipient. Raw LayerZero v2 `extraOptions` bytes. Defaults to `"0x"` (the contract's enforced options). Base RPC URL. Required only when `source === "base"`. `MachineNFTBase` address on Base. Required only when `source === "base"`. The source-chain transaction hash. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const txHash = await client.bridgeNft({ tokenId: 42, source: "peaq", destination: "base", recipient: "0xabc...", }); ``` ### `waitForBridgeArrival` Static method that polls the destination chain's `MachineNFT.ownerOf(tokenId)` every 10 seconds until a non-zero owner returns or the timeout elapses. No `PeaqosClient` instance required. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} static async waitForBridgeArrival( params: WaitForBridgeArrivalParams, ): Promise; ``` Destination-chain RPC endpoint. `MachineNFT` contract address on the destination. The NFT id expected to arrive. Wait budget in seconds. Defaults to 300 (5 min). Optional abort signal. When aborted, the poll stops immediately with a `RuntimeError` code `ABORTED`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const arrived = await PeaqosClient.waitForBridgeArrival({ dstRpcUrl: "https://mainnet.base.org", dstNftAddress: "0x...", tokenId: 42, timeout: 120, }); ``` *** ## Events (Qualify) **Enabled in Tokenomics mode since 0.7.0 (2026-09-11).** Both calls write to `contracts.eventRegistry`, the address in `EVENT_REGISTRY_ADDRESS`; the SDK adds no 2.0-specific address. For Economics 2.0 machines point it at the 2.0 `EventRegistry` `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e`; Tokenomics 1.0 machines keep `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA`. The two contracts share the `submitEvent` selector, so a write to the wrong address lands there and does not revert. Since 0.6.0 `machineId` on `SubmitEventParams` and `MachineEvent` is `bigint` in every mode (`1024n`, not `1024`). ### `submitEvent` Submits a single event to `EventRegistry`. Validates and normalizes the payload, then calls the contract. Returns the transaction hash and the computed `dataHash`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async submitEvent( params: SubmitEventParams, ): Promise<{ txHash: Hex; dataHash: Hex }>; ``` Param shape matches [`validateSubmitEventParams`](#validatesubmiteventparams) below. `value` is an **ISO 4217 minor-unit integer** (cents for USD/HKD, whole units for JPY/KRW/VND, thousandths for BHD). `currency` is required on revenue events (`^[A-Z0-9]{3,10}$`) and must be `""` on activity events; the SDK applies a smart default (revenue → `"USD"`, activity → `""`) when omitted on `submitEvent`. `batchSubmitEvents` is strict: every event must carry `currency` explicitly. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const { txHash, dataHash } = await client.submitEvent({ machineId: 1024, eventType: EVENT_TYPE_REVENUE, value: 12500, // $125.00 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000), rawData: new Uint8Array([1, 2, 3]), trustLevel: TRUST_ON_CHAIN_VERIFIABLE, sourceChainId: SUPPORTED_CHAIN_IDS.peaq, sourceTxHash: null, metadata: new Uint8Array([]), }); ``` * `ValidationError`: any `params` field fails validation. * `ValueCapExceeded` / `RateLimitExceeded`: client-side operational limits hit. * `RuntimeError`: chain revert or receipt failure. ### `batchSubmitEvents` Submits multiple events atomically through the peaq Batch precompile. All events land in the same transaction: all succeed or all revert. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async batchSubmitEvents( events: ReadonlyArray, ): Promise; ``` Non-empty list of event payloads. Each element is validated individually before submission. One transaction hash per input event. All hashes are identical (same batch tx). * `ValidationError`: list is empty, or any event fails validation. * `ValueCapExceeded` / `RateLimitExceeded`: operational limits hit for any event in the batch. * `RuntimeError`: batch revert or transport failure. ### `validateSubmitEventParams` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function validateSubmitEventParams(params: SubmitEventParams): void; ``` Machine ID returned by `registerMachine` / `registerFor`. `0` revenue, `1` activity. Non-negative ISO 4217 minor-unit integer. Cents for USD/HKD, whole units for JPY/KRW/VND, thousandths for BHD. Activity events: any non-negative integer or `0`. Revenue: 3-10 uppercase alphanumeric (e.g. `"USD"`, `"HKD"`, `"JPY"`). Activity: must be `""`. Omit to apply the SDK smart default (revenue → `"USD"`, activity → `""`); `batchSubmitEvents` requires it explicitly. Unix seconds. Off-chain payload hashed into `dataHash`. `0` self-reported, `1` on-chain verifiable, `2` hardware-signed. Originating chain. Use `SUPPORTED_CHAIN_IDS.peaq` for local. Cross-chain tx hash when applicable. Arbitrary bytes stored on-chain alongside the event. Use empty bytes when no metadata is needed. Returns `void`. Throws `ValidationError` on any invariant violation. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { validateSubmitEventParams, EVENT_TYPE_REVENUE, TRUST_ON_CHAIN_VERIFIABLE, SUPPORTED_CHAIN_IDS, } from "@peaqos/peaq-os-sdk"; validateSubmitEventParams({ machineId: 1024, eventType: EVENT_TYPE_REVENUE, value: 1250, // $12.50 in cents currency: "USD", timestamp: Math.floor(Date.now() / 1000), rawData: new Uint8Array([1, 2, 3]), trustLevel: TRUST_ON_CHAIN_VERIFIABLE, sourceChainId: SUPPORTED_CHAIN_IDS.base, sourceTxHash: "0xabc...", metadata: new Uint8Array([]), }); ``` ### `computeDataHash` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function computeDataHash(rawData: Uint8Array): Hex; ``` Off-chain payload bytes to hash. Returns a `keccak256` hash as `0x` + 64 hex characters. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { computeDataHash } from "@peaqos/peaq-os-sdk"; const dataHash = computeDataHash(new TextEncoder().encode("revenue: $12.50")); console.log(dataHash); ``` ### `checkOperationalLimits` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} function checkOperationalLimits( params: { machineId: number; value: number }, limits: OperationalLimits, tracker: EventTracker | null, ): void; ``` Machine ID and event value. Configured `maxValuePerTx`, `rateLimitMaxEvents`, `rateLimitWindowSeconds`. Current rate-tracking state for the machine. Pass `null` if you are not tracking window state. `EventTracker` is `{ machineId: number; count: number; windowStart: number }`. Returns `void`. Throws `ValueCapExceeded` or `RateLimitExceeded` on limit violation. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { checkOperationalLimits } from "@peaqos/peaq-os-sdk"; checkOperationalLimits( { machineId: 1, value: 100 }, client.operationalLimits, { machineId: 1, count: 50, windowStart: Date.now() / 1000 - 1800 }, ); ``` *** ## Queries **Tokenomics mode since 0.7.0 (2026-09-11).** On a `tokenomics20` client all three go to the deployment's 2.0 MCR server (`mcr-20.peaq.xyz` for `peaq-mainnet`); `client.apiUrl` is not read. Machine DIDs are `did:peaq:`, operator DIDs stay `did:peaq:0x
`, `machineId` in results is `bigint`. A malformed or non-canonical `machine_id`, or a response about a different machine or operator, throws `RuntimeError` `BAD_RESPONSE`. A deployment without a paired MCR (`agung-2026-08-28`) throws `TokenomicsConfigError` `DEPLOYMENT_UNAVAILABLE` before any HTTP. Legacy clients keep calling `mcr.peaq.xyz` with `did:peaq:0x` DIDs. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines). Read-only helpers backed by the off-chain MCR API server (`client.apiUrl`). Each function validates the DID, issues a single `GET`, and returns a frozen, shape-checked response. All three accept an optional `GetJsonOptions` with `timeoutMs` (default 30 000 ms) and a caller `AbortSignal`. ### `queryMcr` Fetches the Machine Credit Rating for a machine DID. See [`GET /mcr/{did}`](/peaqos/api-reference/get-mcr). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function queryMcr( client: PeaqosClient, did: string, options?: GetJsonOptions, ): Promise; ``` Machine DID. `did:peaq:0x
` on a legacy client, `did:peaq:` on a `tokenomics20` client (0.7.0+). Request budget in ms. Defaults to 30 000. Caller abort signal. Either signal aborting wins. Frozen object with camelCase fields: `did`, `machineId`, `mcrScore` (number, 0–100), `mcr` (`"AAA" | "AA" | "A" | "BBB" | "BB" | "B" | "NR" | "Provisioned"`), `mcrDegraded` (boolean: `true` when ≥1 scored event used a stale or unavailable FX source), `bondStatus` (`"bonded" | "unbonded"`), `negativeFlag` (boolean: `true` when the machine has been flagged for negative behaviour; consumers should down-rank or alert independently of the numeric score), `eventCount`, `revenueEventCount`, `activityEventCount`, `revenueTrend` (`"up" | "stable" | "down" | "insufficient"`), `totalRevenue` (integer USD cents; divide by 100 for display), `averageRevenuePerEvent` (USD cents as float; divide by 100 for display), `lastUpdated` (unix seconds or `null`). The MCR API returns `mcr_score: null` while a machine is still `Provisioned` or has rating `NR`. The JS SDK coerces this to `0` so `mcrScore` is always a number. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { queryMcr } from "@peaqos/peaq-os-sdk"; const score = await queryMcr(client, "did:peaq:0xMachineAddress", { timeoutMs: 5_000, }); console.log(`${score.mcrScore} → ${score.mcr}`); ``` * `ValidationError`: `did` does not start with `did:peaq:0x`. * `RuntimeError`: HTTP 404 (`NOT_FOUND`), 503 (`SERVICE_UNAVAILABLE`), other 5xx (`SERVER_ERROR`), other non-2xx (`HTTP_ERROR`), timeout (`TIMEOUT`), caller abort (`ABORTED`), transport failure (`NETWORK_ERROR`), or malformed body (`BAD_RESPONSE`). ### `queryMachine` Fetches the full machine profile (NFT Metadata JSON v1.0) for a DID. The SDK strictly validates the response against `MachineProfileResponse` and throws `BAD_RESPONSE` for any missing or malformed required field. See [`GET /machine/{did}`](/peaqos/api-reference/get-machine). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function queryMachine( client: PeaqosClient, did: string, options?: GetJsonOptions, ): Promise; ``` Machine DID. `did:peaq:0x
` on a legacy client, `did:peaq:` on a `tokenomics20` client (0.7.0+). Optional `timeoutMs` (default 30 000) and caller `signal`. Frozen, strictly-validated machine profile. Top-level fields: `schema_version` (string) and `name` (string). The `peaqos` sub-object always carries `machine_id`, `did`, `operator`, `mcr`, `mcr_score`, `bond_status`, `negative_flag`, `event_count`, `data_visibility`, and `documentation_url`. Visibility-dependent extras: `data_api`, `event_data`, `partner_data`, `partner_data_error`. The SDK throws `BAD_RESPONSE` if the server returns anything that fails the schema guard. See [`GET /machine/{did}`](/peaqos/api-reference/get-machine) for full field semantics. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { queryMachine } from "@peaqos/peaq-os-sdk"; const profile = await queryMachine(client, "did:peaq:0xMachineAddress"); console.log(profile.name, profile.peaqos.mcr, profile.peaqos.mcr_score); ``` * `ValidationError`: `did` does not start with `did:peaq:0x`. * `RuntimeError`: same HTTP / transport codes as `queryMcr`; `BAD_RESPONSE` if any required field on `MachineProfileResponse` or its `peaqos` sub-object is missing or wrong-typed. ### `queryOperatorMachines` Fetches the fleet of machines managed by a proxy operator. Each machine summary carries its DID, machine ID, score, rating tier, and `negativeFlag`; the response also includes pagination metadata. See [`GET /operator/{did}/machines`](/peaqos/api-reference/get-operator-machines). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} async function queryOperatorMachines( client: PeaqosClient, did: string, options?: GetJsonOptions, ): Promise; ``` Operator DID. Must start with `did:peaq:0x`. Optional `timeoutMs` (default 30 000) and caller `signal`. Frozen object with `operatorDid`, a frozen `machines` array, and a `pagination` object. Each machine entry exposes `did`, `machineId`, `mcrScore` (number, 0–100; `null` coerced to `0`), `mcr` rating, and `negativeFlag` (boolean: `true` when the machine has been flagged for negative behaviour). `pagination` carries `offset`, `limit`, and `total` (all non-negative integers). ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { queryOperatorMachines } from "@peaqos/peaq-os-sdk"; const fleet = await queryOperatorMachines(client, "did:peaq:0xProxyAddress"); for (const m of fleet.machines) { console.log(`${m.did} → ${m.mcrScore} (${m.mcr}), negative=${m.negativeFlag}`); } console.log(`showing ${fleet.machines.length} of ${fleet.pagination.total}`); ``` * `ValidationError`: `did` does not start with `did:peaq:0x`. * `RuntimeError`: same HTTP / transport codes as `queryMcr`; `BAD_RESPONSE` if the body or any `machines` entry is malformed. *** ## Error classes See [errors](/peaqos/sdk-reference/errors) for the full hierarchy and the 20-code faucet table. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosError, RuntimeError, ValidationError, ValueCapExceeded, RateLimitExceeded, // Economics 2.0 (0.6.0+) TokenomicsConfigError, TokenomicsActivationError, TokenomicsUnsupportedError, TokenomicsIntegrationUnavailableError, MonetizationCompatibilityError, } from "@peaqos/peaq-os-sdk"; ``` Code tables on [errors](/peaqos/sdk-reference/errors#tokenomics-2-0-errors). *** ## Type exports ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import type { // Client PeaqosClientConfig, ContractAddresses, OperationalLimits, // Events SubmitEventParams, MachineEvent, EventType, TrustLevel, EventTracker, // Identity DataVisibility, BondStatus, // API responses MCRResponse, MachineProfileResponse, OperatorMachinesResponse, MCRRating, RevenueTrend, // DID WriteMachineDIDParams, WriteProxyDIDParams, DIDWriteAttribute, DIDAttributeResult, // Wallet WalletInfo, AccountInfo, KeyType, ImportChain, WalletOptions, // Query options GetJsonOptions, // Methods DeploySmartAccountParams, BridgeNftParams, WaitForBridgeArrivalParams, // Faucet FaucetErrorCode, FaucetFundResponse, FaucetFundSkippedResponse, FaucetFundSuccessResponse, FaucetQrFormat, FaucetSetupResponse, FundFromGasStationParams, // OWS signing OwsSigningErrorCode, // Error constructor options RuntimeErrorOptions, ValidationErrorArgs, } from "@peaqos/peaq-os-sdk"; // Enums import { MachineStatus } from "@peaqos/peaq-os-sdk"; ``` *** ## Constants ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { SUBSCRIPTION_TIER_ENTRY, // 0 SUBSCRIPTION_TIER_BASIC, // 1 SUBSCRIPTION_TIER_PRO, // 2 SUBSCRIPTION_TIERS, // [0, 1, 2] DEFAULT_ACTIVATION_CONFIRMATIONS, // 1 DEFAULT_ACTIVATION_TIMEOUT_MS, // 120_000 TOKENOMICS_2_0_DEPLOYMENTS, // deployment records: agung-2026-08-28, peaq-mainnet TOKENOMICS_MONETIZATION_API_VERSION, // "tokenomics-2.0-monetization-v1" UINT256_MAX, } from "@peaqos/peaq-os-sdk"; ``` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { EVENT_TYPE_REVENUE, // 0 EVENT_TYPE_ACTIVITY, // 1 TRUST_SELF_REPORTED, // 0 TRUST_ON_CHAIN_VERIFIABLE, // 1 TRUST_HARDWARE_SIGNED, // 2 } from "@peaqos/peaq-os-sdk"; ``` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { DID_ATTR_MACHINE_ID, DID_ATTR_NFT_TOKEN_ID, DID_ATTR_OPERATOR, DID_ATTR_DOCUMENTATION_URL, DID_ATTR_DATA_API, DID_ATTR_DATA_VISIBILITY, DID_ATTR_MACHINES, DID_MAX_NAME_BYTES, // 64 DID_MAX_VALUE_BYTES, // 2560 } from "@peaqos/peaq-os-sdk"; ``` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { SUPPORTED_CHAIN_IDS, // { peaq: 3338, ethereum: 1, base: 8453, polygon: 137, arbitrum: 42161, optimism: 10 } LAYER_ZERO_EIDS, // { peaq: 30302, base: 30184 } DEFAULT_API_URL, // "http://127.0.0.1:8000" } from "@peaqos/peaq-os-sdk"; ``` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { IMPORT_CHAIN_EVM, // "evm" (default) IMPORT_CHAIN_SOLANA, // "solana" IMPORT_CHAIN_BITCOIN, // "bitcoin" IMPORT_CHAIN_COSMOS, // "cosmos" IMPORT_CHAIN_TRON, // "tron" IMPORT_CHAIN_TON, // "ton" IMPORT_CHAIN_SUI, // "sui" IMPORT_CHAIN_XRPL, // "xrpl" IMPORT_CHAIN_SPARK, // "spark" IMPORT_CHAIN_FILECOIN, // "filecoin" KEY_TYPE_MNEMONIC, // "mnemonic" KEY_TYPE_PRIVATE_KEY, // "private_key" OWS_PASSPHRASE_ENV, // "OWS_PASSPHRASE" DEFAULT_MCR_HTTP_TIMEOUT_MS, // 30_000 } from "@peaqos/peaq-os-sdk"; ``` `IMPORT_CHAIN_*` is the union behind the `ImportChain` type alias used by `importWallet`. `KEY_TYPE_*` matches the `KeyType` field on `WalletInfo`. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { OWS_ERROR_WALLET_NOT_FOUND, // "WALLET_NOT_FOUND" OWS_ERROR_INVALID_PASSPHRASE, // "INVALID_PASSPHRASE" OWS_ERROR_INVALID_INPUT, // "INVALID_INPUT" OWS_ERROR_POLICY_DENIED, // "POLICY_DENIED" OWS_ERROR_CHAIN_NOT_SUPPORTED, // "CHAIN_NOT_SUPPORTED" } from "@peaqos/peaq-os-sdk"; ``` Surface area for the OWS-native signing path used by `PeaqosClient.fromWallet(..., owsSigning: true)`. The matching `OwsSigningErrorCode` type is the union of all five string-literal codes. # SDK: Python Source: https://docs.peaq.xyz/peaqos/sdk-reference/sdk-python Python client for peaqOS. Class, static factories, methods, types, constants. Mirrors the JavaScript SDK in snake_case. `peaq-os-sdk` on PyPI is the Python equivalent of [`@peaqos/peaq-os-sdk`](/peaqos/sdk-reference/sdk-js). Every method and type mirrors the JS SDK; the key differences are listed below. **0.6.0 (2026-09-04) adds Economics 2.0.** Pass `tokenomics20=Tokenomics20Config(deployment_id=...)` to the constructor to enter Tokenomics mode: one-transaction [activation](#tokenomics-2-0), machine management, and the 2.0 monetization client. In that mode the legacy registration, mint, bridge, event, and MCR-query methods raise typed errors instead of running. Without `tokenomics20` the client behaves as before. Full-width machine IDs are Python `int`; serialize them as decimal strings. ## Differences from the JavaScript SDK | Aspect | JavaScript | Python | | :-------------- | :-------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | Constructor | `new PeaqosClient({ config })` | `PeaqosClient(**kwargs)` (keyword args) | | Keypairs | Frozen `{ address, privateKey }` object | Tuple `(address, private_key)` | | Faucet methods | Global `fetch` | `requests.Session` as first arg | | Response keys | `camelCase` | `snake_case` | | Error hierarchy | `RuntimeError` with `.code` | Separate `RpcError` + `ApiError` | | Extra accessors | n/a | `w3`, `web3`, `session`, `account`, `identity_registry`, `identity_staking`, `event_registry`, `machine_nft`, `did_precompile`, `batch_precompile` | ## Install ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install "peaq-os-sdk>=0.7.1" python-dotenv # Optional, only for OWS wallet lifecycle helpers: pip install "peaq-os-sdk[ows]>=0.7.1" ``` * **Python:** ≥ 3.12 for 0.6.0 and newer (the package does not import on 3.10 or 3.11). 0.4.0 and 0.5.0 need 3.11; 0.3.0 is the last release that imports on 3.10 * **Dependencies:** `web3>=6.0`, `eth-account`, `requests` * **Optional wallet dependency:** `open-wallet-standard` through the `[ows]` extra * Virtualenv recommended. `python-dotenv` is optional but recommended: `PeaqosClient.from_env()` reads from dotenv, so `load_dotenv()` at the top of your entry file is the simplest way to load .env. ## Environment variables Same set as the JS SDK: 8 required core vars (RPC URL, private key, and the 6 contract addresses), plus `PEAQOS_MCR_API_URL` (defaults to `http://127.0.0.1:8000`), plus 2 optional vars for smart-account deploy (`MACHINE_ACCOUNT_FACTORY_ADDRESS`) and cross-chain Machine NFT bridging (`MACHINE_NFT_ADAPTER_ADDRESS`). See [JS environment variables](/peaqos/sdk-reference/sdk-js#environment-variables) and [peaq mainnet contracts](/peaqos/sdk-reference/sdk-js#peaq-mainnet-contracts). For agung testnet addresses see [Install → Agung testnet contracts](/peaqos/install#agung-testnet-contracts). Bridging is mainnet-only since LayerZero has no DVN routes to agung. For OWS wallet lifecycle helpers, pass a `passphrase` argument or set `OWS_PASSPHRASE`. For Scale, set `PEAQOS_ORCHESTRATION_URL` (and optionally `PEAQOS_API_KEY`) before `PeaqosClient.from_env()` to enable `client.orchestration`. Full method reference: [Orchestration (Python)](/peaqos/sdk-reference/orchestration-py). *** ## Client ### `PeaqosClient` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} class PeaqosClient: def __init__( self, *, rpc_url: str, private_key: str, identity_registry: str, identity_staking: str, event_registry: str, machine_nft: str, did_registry: str, batch_precompile: str, machine_account_factory: str | None = None, machine_nft_adapter: str | None = None, api_url: str = DEFAULT_API_URL, operational_limits: OperationalLimits | None = None, orchestration_url: str | None = None, api_key: str | None = None, verbose: bool = False, tokenomics20: Tokenomics20Config | None = None, ) -> None: ... ``` `Tokenomics20Config(deployment_id="peaq-mainnet")` or `"agung-2026-08-28"`. Selects the Economics 2.0 deployment and puts the client in Tokenomics mode. The seven contract addresses come from the SDK's snapshot, resolved once in the constructor with no network call; an unknown ID raises `TokenomicsConfigError` `DEPLOYMENT_UNKNOWN`. Addresses are never accepted here. `from_env()` does not read `TOKENOMICS_DEPLOYMENT_ID`; only the CLI does. RPC endpoint. `0x` + 64 hex. Identity Registry contract address. Identity Staking contract address. Event Registry contract address. Machine NFT contract address (ONFT). DID Registry precompile address. Batch precompile address. `MachineAccountFactory` contract address. Required only for `deploy_smart_account` and `get_smart_account_address`. `MachineNFTAdapter` (LayerZero ONFT adapter) contract address on peaq. Required only for `bridge_nft` when `source="peaq"`. MCR API base URL. Defaults to `DEFAULT_API_URL`. Per-tx and rate-limit caps. Returns a `PeaqosClient` instance. `__repr__` redacts the private key. Other RPC endpoints are available. See [Public RPC endpoints](/peaqos/install#public-rpc-endpoints). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient client = PeaqosClient( rpc_url="https://peaq.api.onfinality.io/public", private_key="0xabc...def", identity_registry="0x...", identity_staking="0x...", event_registry="0x...", machine_nft="0x...", did_registry="0x...", batch_precompile="0x...", ) print(client.address) ``` **Errors:** `ValidationError`: missing/invalid constructor args. ### `from_env` Since 0.7.1 `from_env()` reads `TOKENOMICS_DEPLOYMENT_ID` (same spelling as the JavaScript SDK): a non-empty value puts the client in Tokenomics mode, absent or empty stays legacy, an unknown or unreleased ID raises `TokenomicsConfigError`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @classmethod def from_env(cls) -> "PeaqosClient": ... ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from dotenv import load_dotenv from peaq_os_sdk import PeaqosClient load_dotenv() # load envs from .env file client = PeaqosClient.from_env() ``` **Errors:** `ValidationError`: any required env var missing or empty. ### `from_wallet` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @classmethod def from_wallet( cls, name_or_id: str, passphrase: str | None = None, ows_signing: bool = True, vault_path: str | None = None, **config_kwargs: Any, ) -> "PeaqosClient": ... ``` Builds a client whose signing identity is an OWS vault wallet. Mirrors the JS SDK's `PeaqosClient.fromWallet`. The remaining keyword arguments (`rpc_url`, contract addresses, etc.) match the regular `PeaqosClient` constructor. | Param | Default | Meaning | | :------------ | :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name_or_id` | required | Wallet name or UUID in the OWS vault. | | `passphrase` | `OWS_PASSPHRASE` env | Vault passphrase. Falls back to the env var when `None`; raises `PeaqosError` when both are missing. | | `ows_signing` | `True` | When `True`, transactions sign through OWS (key never held by the SDK process). When `False`, the key is exported and decrypted at construction so the client is identical to one built with `private_key=...`. | | `vault_path` | `~/.ows/` | Optional custom vault directory. | In OWS-native mode the SDK never holds the private key — OWS decrypts it inside the Rust FFI for each `sign_hash` call and wipes it immediately. The passphrase is verified eagerly: a wrong or missing one raises `PeaqosError` at construction. Each transaction's `chainId` (from the tx dict) drives both the CAIP-2 OWS arg and the EIP-155 `v` value, so the same client transparently signs both peaq (`eip155:3338`) and Base (`eip155:8453`) bridge transactions. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient # OWS-native (default): private key never decrypted in this process. client = PeaqosClient.from_wallet( "my-machine", passphrase="s3cret", rpc_url="https://peaq-rpc.example.com", identity_registry="0x...", identity_staking="0x...", event_registry="0x...", machine_nft="0x...", did_registry="0x0000000000000000000000000000000000000800", batch_precompile="0x0000000000000000000000000000000000000805", ) # Eager-decrypt: behaves identically to PeaqosClient(private_key=...). eager = PeaqosClient.from_wallet( "my-machine", passphrase="s3cret", ows_signing=False, rpc_url="https://peaq-rpc.example.com", identity_registry="0x...", # ... ) ``` **Errors:** `PeaqosError` when the wallet is missing, the passphrase is wrong (eager-mode) or missing, or OWS itself rejects the request. See [OWS signing error codes](/peaqos/sdk-reference/errors#ows-signing-error-codes) for the canonical 5 codes mapped from `OWSAccount.sign_transaction`. ### Wallets (OWS) Wallet lifecycle helpers (`create_wallet`, `import_wallet`, `import_wallet_mnemonic`, `list_wallets`, `get_wallet`, `export_wallet`, `delete_wallet`) back the [Open Wallet Standard](/peaqos/wallets) integration: mnemonic-backed encrypted vault, multi-chain accounts (peaq, Base, Ethereum, Solana, Bitcoin, etc.). Available under `peaq_os_sdk.wallet` and as `@staticmethod`s on `PeaqosClient`. Install with the optional `[ows]` extra (`pip install 'peaq-os-sdk[ows]'`). The raw-key constructor and `from_env` flow keep working unchanged. Full reference on the [Wallets page](/peaqos/wallets#sdk-methods). Wallet returns are typed as `WalletInfo` (frozen dataclass) with an `accounts: list[AccountInfo]` field. `AccountInfo` carries `account_id` (CAIP-10), `address`, `chain_id` (CAIP-2), `network` (human-readable name like `"peaq"`, `"base"`, `"ethereum"`, `"solana"`), and `derivation_path`. The SDK synthesizes accounts for every supported EVM chain from the first EVM source returned by OWS: peaq, ethereum, base, polygon, arbitrum, and optimism all surface in `accounts` even when OWS only returns one EVM key. Both classes are re-exported from the package root. ### `generate_keypair` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @staticmethod def generate_keypair() -> tuple[Address, str]: ... ``` Returns tuple `(address, private_key)`. Both are `0x`-prefixed hex strings; `Address` is a `NewType` over `str` (checksummed). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} address, private_key = PeaqosClient.generate_keypair() print(address) ``` ### OWS wallet lifecycle OWS wallet helpers are available as static `PeaqosClient` methods. They derive multi-chain accounts, keep wallet material in an encrypted OWS vault, and return public `WalletInfo` metadata. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @staticmethod def create_wallet(name: str, passphrase: str | None = None, words: int = 12, vault_path: str | None = None) -> WalletInfo: ... @staticmethod def import_wallet(name: str, private_key: str, passphrase: str | None = None, chain: ImportChain = IMPORT_CHAIN_EVM, vault_path: str | None = None) -> WalletInfo: ... @staticmethod def import_wallet_mnemonic(name: str, mnemonic: str, passphrase: str | None = None, index: int = 0, vault_path: str | None = None) -> WalletInfo: ... @staticmethod def list_wallets(vault_path: str | None = None) -> list[WalletInfo]: ... @staticmethod def get_wallet(name_or_id: str, vault_path: str | None = None) -> WalletInfo: ... @staticmethod def export_wallet(name_or_id: str, passphrase: str | None = None, vault_path: str | None = None) -> str: ... @staticmethod def delete_wallet(name_or_id: str, vault_path: str | None = None) -> None: ... ``` Install `peaq-os-sdk[ows]` before using these helpers. `create_wallet`, `import_wallet`, `import_wallet_mnemonic`, and `export_wallet` require a passphrase argument or `OWS_PASSPHRASE`. `vault_path` can point at a custom OWS vault directory. Frozen dataclass with `id`, `name`, `created_at`, `key_type`, `peaq_address`, and `accounts`. Each account has `account_id`, `address`, `chain_id`, `network`, and `derivation_path`. Supported EVM accounts include peaq, Ethereum, Base, Polygon, Arbitrum, and Optimism; missing EVM chain entries are synthesized from the first available EVM address. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient, IMPORT_CHAIN_EVM from peaq_os_sdk.wallet.utils import extract_peaq_address wallet = PeaqosClient.create_wallet("robot-001") print(wallet.peaq_address) imported = PeaqosClient.import_wallet( "legacy-machine", "0xabc...def", chain=IMPORT_CHAIN_EVM, ) all_wallets = PeaqosClient.list_wallets() same = PeaqosClient.get_wallet(imported.id) print(len(all_wallets), extract_peaq_address(same.accounts)) ``` `export_wallet` returns mnemonic or private-key material. Keep it in local administrative tooling; do not expose it through robot control channels. ### Accessors (Python-only) | Accessor | Type | Description | | :------------------ | :----------------------------- | :--------------------------------------------------------------------------------------- | | `address` | `str` | Checksummed owner address | | `w3` / `web3` | `Web3` | Web3 instance | | `session` | `requests.Session` | HTTP session for API calls | | `account` | `LocalAccount` \| `OWSAccount` | Bound signing account; `OWSAccount` when constructed via `from_wallet(ows_signing=True)` | | `identity_registry` | `Contract` | Bound IdentityRegistry contract | | `identity_staking` | `Contract` | Bound IdentityStaking contract | | `event_registry` | `Contract` | Bound EventRegistry contract | | `machine_nft` | `Contract` | Bound MachineNFT contract | | `did_precompile` | `Contract` | Bound peaq DID precompile | | `batch_precompile` | `Contract` | Bound peaq Batch precompile | *** ## Tokenomics 2.0 Available on a client constructed with `tokenomics20`. Every function below is a `PeaqosClient` method and a package-root function taking the client as its first argument. Source of truth in the repo: `docs/18_TOKENOMICS_ACTIVATION.md` and `docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md`. Concepts: [Economics 2.0](/peaqos/concepts/economics-2-0). ### Deployments ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import TOKENOMICS_2_0_DEPLOYMENTS, resolve_tokenomics20_deployment for deployment_id, record in TOKENOMICS_2_0_DEPLOYMENTS.items(): print(deployment_id, record.network, record.chain_id, record.status) # agung-2026-08-28 agung 9990 available # peaq-mainnet peaq 3338 available ``` `resolve_tokenomics20_deployment(deployment_id, overrides=None)` cross-checks an optional `role -> address` mapping against the snapshot and raises `ADDRESS_OVERRIDE_MISMATCH` on any difference. Overrides never introduce addresses. The PEAQ token is resolved at call time from `InfoDesk.peaqToken()`. ### `activate_machine` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def activate_machine(self, params: ActivateMachineParams) -> ActivateMachineResult: ... ``` One transaction to `MachineStateAndSync.activateMachine`: mints the ERC-721 in `MachineRegistry` (token ID equals machine ID), stores the DID document, bonds the tier in `MachineSubscription`, and records the home chain. The signer becomes owner and bond payer. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} ActivateMachineParams( controller: Address, verification_methods: tuple[VerificationMethodInput, ...], # id, method_type, controller, public_key_multibase authentication: tuple[int, ...], # indices into verification_methods service_endpoints: tuple[ServiceEndpointInput, ...], # id, service_type, service_endpoint machine_type: str, credential_subject: bytes, manufacturer: Address, # recorded, never verified tier: int, # 0 Entry, 1 Basic, 2 Pro expected_machine_id: int | None = None, max_net_peaq_amount: int | None = None, confirmations: int = 1, timeout_seconds: float = 120.0, cancel: threading.Event | None = None, on_transaction_submitted: Callable[[SubmittedActivationTransaction], None] | None = None, ) ``` Returns `ActivateMachineResult` with `machine_id`, `owner`, `controller`, `tier`, `bond_amount`, `voucher_credit_applied`, `net_peaq_amount`, `transaction_hash`, `is_homed_locally`, the receipt, and the correlated events. Amounts come from the receipt, not the preflight quote. Success requires `MachineOnboarded`, `MachineMinted`, and `Activated` from the right contracts plus a matching post-state read. Sequence: local validation, chain and bytecode checks, `InfoDesk.peer(role)` match, `fullMode()`, compute ID, quote, resolve PEAQ, balance and allowance, approve exactly the net if short (spender `MachineSubscription`), re-read everything, simulate, submit once, correlate, reconcile. The write is never retried. `cancel` is honoured up to submission. **Errors:** `ValidationError`, `TokenomicsConfigError`, `TokenomicsActivationError` (codes below), `TokenomicsPendingTransactionError` (`PENDING_TRANSACTION`, carries `.submitted`). ### `activate_machine_with_usdt` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def activate_machine_with_usdt(self, params: ActivateMachineWithUsdtParams) -> ActivateMachineWithUsdtResult: ... ``` Same activation, bond settled in USDT through `SubscriptionTokenProvisionPool` (the allowance spender). Requires `max_usdt_amount`, taken from `preview_machine_activation_with_usdt(...).max_usdt_amount` after applying your `slippage_bps`. The bond, credit, and net stay in PEAQ; a fully credited bond converts nothing. ### `preview_machine_activation`, `preview_machine_activation_with_usdt` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def preview_machine_activation(self, params: ActivateMachineParams) -> ActivationPreview: ... ``` Runs the same validation and reads without signing, approving, or writing. Returns `machine_id`, `bond_amount`, `voucher_credit`, `net_peaq_amount`, `balance`, `approval_required`. Raises `MACHINE_ID_MISMATCH` when `expected_machine_id` disagrees and `MAX_NET_PEAQ_EXCEEDED` when the bound is already exceeded; a low balance is **returned**, not raised. ### `compute_machine_id` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def compute_machine_id(self, machine_type: str, credential_subject: bytes) -> int: ... ``` `uint256(keccak256(abi.encode(machine_type, credential_subject)))`, read from `MachineRegistry.computeTokenId`. ### Reads ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def get_machine_owner(self, machine_id: int) -> Address: ... # MACHINE_NOT_FOUND vs READ_FAILED kept apart def get_machine_activation_state(self, machine_id: int) -> MachineActivationState: ... # activated when .subscription.period_start != 0 def get_machine_management_state(self, machine_id: int) -> MachineManagementState: ... # owner, controller, did_document, subscription, pause_flags, is_available, home_chain_id, is_homed_locally, is_relocating def get_machine_subscription(self, machine_id: int) -> MachineSubscription: ... def get_machine_availability(self, machine_id: int) -> MachineAvailability: ... # each blocking flag kept separate def get_machine_relocation_status(self, params: MachineRelocationStatusParams) -> MachineRelocationStatus: ... # needs a destination client; state: pending | completed | cancelled | conflicting, stage: not_started | initiated | arrived ``` ### Lifecycle and subscription ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def suspend_machine(self, machine_id: int, options: MachineActionOptions | None = None) -> ...: ... def resume_machine(self, machine_id: int, options: MachineActionOptions | None = None) -> ...: ... def preview_existing_machine_activation(self, params: PreviewExistingMachineActivationParams) -> ...: ... def activate_existing_machine(self, params: ActivateExistingMachineParams | ActivateExistingMachineWithUsdtParams) -> ...: ... # a minted machine whose subscription never started def preview_machine_renewal(self, params: PreviewMachineRenewalParams) -> ...: ... def renew_machine(self, params: RenewMachineParams | RenewMachineWithUsdtParams) -> ...: ... # stored tier, extends from the stored period end ``` PEAQ writes require `max_net_peaq_amount`, USDT writes `max_usdt_amount`; take both from the matching preview. The SDK re-quotes before simulation and never raises an accepted bound. Owner **or controller** may sign; credits accrue to the owner. ### Ownership (ERC-721) ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def approve_machine(self, operator: Address, machine_id: int, options=None) -> ...: ... def set_machine_approval_for_all(self, operator: Address, approved: bool, options=None) -> ...: ... # every machine the signer owns, including future ones def transfer_machine(self, from_address: Address, to_address: Address, machine_id: int, options=None) -> ...: ... def safe_transfer_machine(self, from_address: Address, to_address: Address, machine_id: int, data: bytes | None = None, options=None) -> ...: ... ``` Transfer changes the owner and **retains** the DID controller. Blocked while relocating. ### DID updates ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def set_machine_controller(self, machine_id: int, controller: Address, options: SetMachineControllerOptions | None = None) -> ...: ... # owner only; clearing needs confirm_clear_controller=True def set_machine_verification_methods(self, machine_id: int, methods: tuple[VerificationMethodInput, ...], options=None) -> ...: ... def set_machine_authentication(self, machine_id: int, indices: tuple[int, ...], options=None) -> ...: ... def set_machine_service_endpoints(self, machine_id: int, endpoints: tuple[ServiceEndpointInput, ...], options=None) -> ...: ... ``` Setters replace whole arrays. Shrinking verification methods below a live authentication index raises `AUTHENTICATION_REWRITE_REQUIRED` before submission. ### Previews and reconciliation ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def preview_machine_action(self, params: MachineActionPreviewParams) -> ...: ... # lifecycle, ownership, and DID actions; never simulates, signs, or submits def reconcile_activation_transaction(self, submitted: SubmittedActivationTransaction, expected: ActivationReconciliationContext) -> ...: ... def reconcile_machine_management_transaction(self, submitted: SubmittedMachineManagementTransaction, expected: MachineManagementReconciliationContext) -> ...: ... ``` Reconciliation is read-only and returns `pending | confirmed | failed | conflicting`. A hash with no receipt is always `pending`; the SDK never infers a dropped transaction from age, mempool absence, or nonce. ### Machine-ID helpers ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import parse_tokenomics_machine_id, serialize_tokenomics_machine_id, validate_tokenomics_machine_id ``` Canonical base-10 strings at every JSON, URL, log, and cache boundary. `bool` is rejected wherever an `int` machine ID is expected. ### Disabled in Tokenomics mode | Call | Raises | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | | `register_machine`, `mint_nft`, `token_id_of` | `TokenomicsUnsupportedError` `LEGACY_REGISTRATION_UNSUPPORTED` | | `register_for` | `TokenomicsUnsupportedError` `SPONSORED_ACTIVATION_UNSUPPORTED` (no replacement) | | `bridge_nft` | `TokenomicsUnsupportedError` `MACHINE_RELOCATION_UNAVAILABLE` | | `write_machine_did_attributes`, `write_proxy_did_attributes`, `batch_did_write` naming machine attributes | `TokenomicsIntegrationUnavailableError` (`"legacy DID helpers"`) | | `orchestration.list_machines`, `create_machine`, `get_machine`, `update_machine`, `create_agent_pairing_challenge`, `create_machine_identity_challenge`, `search_market` | `TokenomicsIntegrationUnavailableError` (`"orchestration identity binding"`) | Enabled in Tokenomics mode since 0.7.1 (2026-09-11): `submit_event`, `batch_submit_events` ([Events](#events-qualify)) and `query_mcr`, `query_machine`, `query_operator_machines` ([Queries](#queries)). Still available in Tokenomics mode: stream, provisioning, wallets, heartbeat, orders, other orchestration calls. `wait_for_bridge_arrival` and `quote_send` are not gated (they take no client); they only warn. *** ## Registration **Tokenomics 1.0 path.** `register_machine` and `register_for` address `IdentityRegistry` (1 PEAQ native bond, separate Machine NFT). They emit `DeprecationWarning` and raise `TokenomicsUnsupportedError` on a client constructed with `tokenomics20`. New machines use [`activate_machine`](#activate_machine). ### `register_machine` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def register_machine(self) -> int: ... ``` Returns the newly allocated `machine_id`. The SDK decodes it from the `Registered` event in the transaction receipt. 1 PEAQ is sent as `msg.value`: the payable `register()` function auto-bonds the machine. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} machine_id = client.register_machine() print("registered as", machine_id) ``` **Errors:** `RpcError`: chain revert (`AlreadyRegistered`), insufficient balance for gas + 1 PEAQ bond, or any other on-chain failure. See [errors](/peaqos/sdk-reference/errors). ### `register_for` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def register_for(self, machine_address: str) -> int: ... ``` Machine EOA to register. The caller acts as proxy operator. Returns the newly allocated `machine_id` for the proxied machine. 1 PEAQ is sent as `msg.value`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} machine_address, machine_key = PeaqosClient.generate_keypair() machine_id = client.register_for(machine_address) print(f"registered machine {machine_address} with machine ID {machine_id}") ``` **Errors:** `ValidationError` on invalid `machine_address`. `RpcError` on chain revert (`AlreadyRegistered`, `InvalidMachineAddress`). *** ## Gas Station ### `setup_faucet_2fa` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def setup_faucet_2fa( self, owner_address: str, faucet_base_url: str, qr_format: Literal["svg", "png"] = "svg", ) -> FaucetSetupResponse: ... ``` Owner to enroll. Gas Station base URL. QR format. Defaults to `"svg"`. Keys: `owner_address: str`, `otpauth_uri: str` (OTP auth URI for authenticator apps), `qr_image_url: str` (expires after \~2 minutes). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} setup = client.setup_faucet_2fa( owner_address=client.address, faucet_base_url="https://depinstation.peaq.xyz", ) print(setup["otpauth_uri"]) ``` **Errors:** `ValidationError` if `owner_address` or `faucet_base_url` is empty. `ApiError` for `INVALID_OWNER_ADDRESS`, `QR_GENERATION_FAILED`, `NETWORK_ERROR`, or an unexpected response envelope. See [errors](/peaqos/sdk-reference/errors). ### `confirm_faucet_2fa` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def confirm_faucet_2fa( self, owner_address: str, faucet_base_url: str, two_factor_code: str, ) -> None: ... ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} client.confirm_faucet_2fa( owner_address=client.address, faucet_base_url="https://depinstation.peaq.xyz", two_factor_code="123456", ) ``` **Errors:** `ValidationError`, `ApiError` (`INVALID_2FA`, `2FA_NOT_CONFIGURED`, `2FA_LOCKED`). ### `fund_from_gas_station` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def fund_from_gas_station( self, owner_address: str, target_wallet_address: str, chain_id: str, two_factor_code: str, faucet_base_url: str, request_id: str | None = None, ) -> FaucetFundResponse: ... ``` 2FA-enrolled owner address. Machine EOA to fund. Chain identifier configured on the faucet, e.g. `"peaq"`. Current TOTP. Gas Station base URL. UUID idempotency key. Auto-generated if omitted. Discriminated union on `status`. Either a `FundedResponse` (`{status: "success", tx_hash: str, funded_amount: str}`, `funded_amount` is decimal wei) or a `SkippedResponse` (`{status: "skipped", current_balance: str, min_gas_balance: str}`). `request_id` is a request-side idempotency key passed by the caller; the response does not echo it back. **Cross-SDK behavior:** The Python SDK does not include `request_id` in the response. The JS SDK echoes `requestId` back in both success and skipped responses. Do not write code that reads `requestId` / `request_id` from the response and expects it to work in both SDKs. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} result = client.fund_from_gas_station( owner_address=client.address, target_wallet_address=machine_address, chain_id="peaq", two_factor_code="123456", faucet_base_url="https://depinstation.peaq.xyz", ) if result["status"] == "success": print("tx:", result["tx_hash"], "amount:", result["funded_amount"]) else: print("skipped; balance:", result["current_balance"]) ``` **Errors:** `ValidationError`, `ApiError` (any of the 20 faucet codes). See [errors](/peaqos/sdk-reference/errors). *** ## NFT & DID **Tokenomics 1.0 path.** In Tokenomics mode `mint_nft` and `token_id_of` raise `TokenomicsUnsupportedError` (minting happens inside `activate_machine`; the machine ID is the token ID) and the DID writers raise `TokenomicsIntegrationUnavailableError`. Use the [DID setters](#did-updates) instead. Machine NFT minting, token-ID lookup, and the two canonical DID attribute writers. The DID writes are submitted as a single atomic `batchAll` transaction via the peaq Batch precompile. ### `mint_nft` Mints a Machine NFT on the MachineNFT contract for a registered, bonded machine. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def mint_nft(self, machine_id: int, recipient: str) -> str: ... ``` Registered machine ID. Must be a positive integer. `0x`-prefixed 20-byte hex address that will receive the NFT. Transaction hash as a `0x`-prefixed hex string. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tx_hash = client.mint_nft(machine_id=42, recipient=client.address) ``` **Errors:** `ValidationError` on non-positive `machine_id` or invalid `recipient`. `RpcError` on chain revert (`MachineNotBonded`, `AlreadyMinted`, `NotMachineOwner`) or transaction failure. ### `token_id_of` Reads the NFT token ID assigned to a registered machine via a view call. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def token_id_of(self, machine_id: int) -> int: ... ``` Registered machine ID. Must be a positive integer. The NFT token ID, as a positive integer. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} token_id = client.token_id_of(machine_id=42) ``` **Errors:** `ValidationError` if `machine_id` is not positive. `RpcError` when the machine has no NFT minted (contract reverts). **Cross-SDK behavior:** The Python SDK raises `RpcError` when no NFT has been minted for the machine (contract reverts). The JS SDK returns `0` instead. In polyglot codebases, catch `RpcError` in Python and check for `0` in JS — do not assume the same pattern works in both. ### `write_machine_did_attributes` Atomically writes the six canonical Machine DID attributes to the caller's DID via a single `batchAll` transaction. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def write_machine_did_attributes( self, machine_id: int, nft_token_id: int, operator_did: str, documentation_url: str, data_api: str, data_visibility: str, ) -> str: ... ``` Registered machine ID. NFT token ID assigned to the machine. Operator DID reference. May be an empty string. ASCII, ≤ 2560 bytes. Non-empty ASCII URL, ≤ 2560 bytes. Non-empty ASCII URL for the machine's data API, ≤ 2560 bytes. Visibility setting. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tx_hash = client.write_machine_did_attributes( machine_id=42, nft_token_id=1, operator_did="", documentation_url="https://docs.example.com", data_api="https://api.example.com", data_visibility="public", ) ``` ### `write_proxy_did_attributes` Atomically writes the two canonical Proxy DID attributes (`machineId`, `machines`) to the caller's DID. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def write_proxy_did_attributes( self, proxy_machine_id: int, machine_ids: list[int], ) -> str: ... ``` The proxy operator's registered machine ID. Non-empty list of positive machine IDs. The JSON-encoded array must be ≤ 2560 bytes. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tx_hash = client.write_proxy_did_attributes( proxy_machine_id=10, machine_ids=[42, 43, 44], ) ``` ### `read_attribute` Reads a single DID attribute directly from the peaq DID precompile. Most consumers should prefer the [`/machine/{did}` API](/peaqos/api-reference/get-machine), which composes the full attribute set; this helper is the on-chain escape hatch. Not exported from the package root: import from `peaq_os_sdk.did.did_precompile`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.did.did_precompile import read_attribute def read_attribute( client: PeaqosClient, did_address: str, name: str, ) -> DIDAttributeResult: ... ``` The machine address whose DID is being read. Attribute key, e.g. `"machineId"`, `"data_visibility"`, `"machines"`. Keys: `name: str`, `value: str`, `validity: int` (seconds; `0` = no expiry), `created: int` (block timestamp). Raises `RpcError` if the attribute does not exist on the precompile. *** ## Smart accounts ERC-4337 smart accounts deployed via the `MachineAccountFactory`. Requires the client to be constructed with `machine_account_factory` (or `MACHINE_ACCOUNT_FACTORY_ADDRESS` when using `from_env`). ### `deploy_smart_account` Deploys a smart account via `MachineAccountFactory.createAccount` and returns the deployed address. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def deploy_smart_account( self, owner: str, machine: str, salt: int, ) -> Address: ... ``` EOA that will own the smart account. Machine EOA the account is scoped to. Non-negative CREATE2 salt. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} address = client.deploy_smart_account( owner=client.address, machine=machine_address, salt=0, ) ``` **Errors:** `ValidationError` on invalid param or client constructed without `machine_account_factory`. `RpcError` on revert or missing `AccountCreated` event. ### `get_smart_account_address` Read-only equivalent: computes the CREATE2 address without deploying. Identical result to `deploy_smart_account` for the same inputs. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def get_smart_account_address( self, owner: str, machine: str, salt: int, ) -> Address: ... ``` Same parameters as `deploy_smart_account`. No transaction, no gas. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} predicted = client.get_smart_account_address( owner=client.address, machine=machine_address, salt=0, ) ``` *** ## Bridge Supported routes: peaq ↔ Base. The peaq ↔ Solana lane is live on chain but not yet exposed here. In Tokenomics mode `bridge_nft` raises `MACHINE_RELOCATION_UNAVAILABLE`: Economics 2.0 relocates whole machine records and is disabled on chain today. See [Machine NFT cross-chain portability](/peaqos/concepts/machine-nft#cross-chain-portability). LayerZero v2 Machine NFT bridging between peaq and Base. Requires `machine_nft_adapter` (or `MACHINE_NFT_ADAPTER_ADDRESS`) when sending from peaq. The SDK's `source` / `destination` string union expands as peer contracts deploy on new chains. ### `bridge_nft` Bridges a Machine NFT from `source` to `destination`. On the peaq→Base path the SDK runs an ERC-721 approval pre-flight: it reads `MachineNFT.getApproved(token_id)` and submits a one-shot `approve(adapter, token_id)` if the token isn't already cleared for the adapter. The Base→peaq path uses burn-and-unlock and needs no approval. Callers don't handle approvals themselves. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def bridge_nft( self, token_id: int, source: str, destination: str, recipient: str, *, base_rpc_url: str | None = None, base_nft_address: str | None = None, options: bytes = b"", ) -> Hex32: ... ``` Positive NFT id to bridge. Origin chain. Target chain. Must differ from `source`. Destination-chain recipient address. Base RPC URL. Required only when `source == "base"`. `MachineNFTBase` address on Base. Required only when `source == "base"`. Raw LayerZero v2 `extraOptions` bytes. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} tx = client.bridge_nft( token_id=42, source="peaq", destination="base", recipient="0xabc...", ) ``` ### `wait_for_bridge_arrival` Static method that polls the destination chain's `MachineNFT.ownerOf(token_id)` every 10 seconds until a non-zero owner returns or the timeout elapses. Does not require a client instance. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @staticmethod def wait_for_bridge_arrival( dst_rpc_url: str, dst_nft_address: str, token_id: int, timeout: int = 300, ) -> bool: ... ``` Destination-chain RPC endpoint. `MachineNFT` contract address on the destination. The NFT id expected to arrive. Wait budget in seconds. Defaults to 300 (5 min). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} arrived = PeaqosClient.wait_for_bridge_arrival( dst_rpc_url="https://mainnet.base.org", dst_nft_address="0x...", token_id=42, ) ``` *** ## Events (Qualify) **Enabled in Tokenomics mode since 0.7.1 (2026-09-11).** Both calls write to the `event_registry` address the client was constructed with (`EVENT_REGISTRY_ADDRESS`); the SDK adds no 2.0-specific address. For Economics 2.0 machines point it at the 2.0 `EventRegistry` `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e`; Tokenomics 1.0 machines keep `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA`. The two contracts share the `submitEvent` selector, so a write to the wrong address lands there and does not revert. ### `submit_event` Submits a single event to `EventRegistry`. Returns `(tx_hash, data_hash)`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def submit_event( self, *, machine_id: int, event_type: int, value: int, timestamp: int, raw_data: bytes | None, trust_level: int, source_chain_id: int, source_tx_hash: Hex32 | None, metadata: bytes, currency: str | None = ..., # omit for smart default: revenue → "USD", activity → "" ) -> tuple[str, bytes]: ... ``` Param shape mirrors [`validate_submit_event_params`](#validate_submit_event_params) below. `value` is an **ISO 4217 minor-unit integer**: cents for USD/HKD, whole units for JPY/KRW/VND, thousandths for BHD. The MCR pipeline FX-normalizes to USD cents using the rate at `timestamp`. `currency` is a 3-10 char uppercase alphanumeric code on revenue events and `""` on activity events; omitting the kwarg applies the smart default. `batch_submit_events` is strict: `currency` must be supplied per event. `tx_hash` is a `0x`-prefixed hash string; `data_hash` is exactly 32 bytes. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import time tx_hash, data_hash = client.submit_event( machine_id=1024, event_type=EVENT_TYPE_REVENUE, value=12500, # $125.00 in cents currency="USD", timestamp=int(time.time()), raw_data=b"\x01\x02\x03", trust_level=TRUST_ON_CHAIN_VERIFIABLE, source_chain_id=SUPPORTED_CHAINS["peaq"], source_tx_hash=None, metadata=b"", ) ``` **Errors:** `ValidationError`, `ValueCapExceeded`, `RateLimitExceeded`, `RpcError`. ### `batch_submit_events` Submits multiple events atomically through the peaq Batch precompile. All succeed or all revert. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def batch_submit_events( self, events: list[dict[str, object] | SubmitEventParams], ) -> list[str]: ... ``` Non-empty list of event payloads. Items may be `SubmitEventParams` instances or dicts with matching keys. One transaction hash per input event. All hashes are identical (same batch tx). **Errors:** `ValidationError` on empty list or invalid event. `ValueCapExceeded` / `RateLimitExceeded` when operational limits hit. `RpcError` on revert or transport failure. ### `validate_submit_event_params` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def validate_submit_event_params(params: SubmitEventParams) -> None: ... ``` `SubmitEventParams` is a frozen dataclass (imported from `peaq_os_sdk.types.events`): ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} @dataclass(frozen=True, slots=True) class SubmitEventParams: machine_id: int event_type: int # 0 revenue, 1 activity value: int # ISO 4217 subunit integer timestamp: int raw_data: bytes | None trust_level: int # 0, 1, 2 source_chain_id: int source_tx_hash: Hex32 | None metadata: bytes currency: str = ... # revenue: ^[A-Z0-9]{3,10}$; activity: "" ``` The validator uses attribute access, so callers must construct a `SubmitEventParams` instance. Dict literals will raise `AttributeError`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import time from peaq_os_sdk import ( EVENT_TYPE_REVENUE, TRUST_ON_CHAIN_VERIFIABLE, SUPPORTED_CHAINS, ) from peaq_os_sdk.types.events import SubmitEventParams from peaq_os_sdk.validation import validate_submit_event_params params = SubmitEventParams( machine_id=1024, event_type=EVENT_TYPE_REVENUE, value=1250, # $12.50 in cents currency="USD", timestamp=int(time.time()), raw_data=b"revenue: $12.50", trust_level=TRUST_ON_CHAIN_VERIFIABLE, source_chain_id=SUPPORTED_CHAINS["base"], source_tx_hash="0xabc...", metadata=b"", ) validate_submit_event_params(params) ``` **Errors:** `ValidationError`: any field out of range. ### `compute_data_hash` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def compute_data_hash(raw_data: bytes) -> bytes: ... ``` Returns the 32-byte `keccak256` digest of `raw_data`. Pair with `submit_event`'s `data_hash` output (also bytes) to compare on-chain payloads. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.utils import compute_data_hash data_hash = compute_data_hash(b"revenue: $12.50") ``` ### `check_operational_limits` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def check_operational_limits( params: SubmitEventParams, limits: OperationalLimits, tracker: EventTracker | None, ) -> None: ... ``` Event with `machine_id` and `value`. Configured `max_value_per_tx`, `rate_limit_max_events`, `rate_limit_window_seconds`. Current rate-tracking state for the machine, or `None` when tracking is disabled. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.validation import check_operational_limits, EventTracker tracker = EventTracker(machine_id=1, count=5, window_start=1700000000.0) check_operational_limits(params, client.operational_limits, tracker) ``` **Errors:** `ValueCapExceeded` if `value` > `max_value_per_tx`. `RateLimitExceeded` when the event window is exceeded. *** ## Queries **Tokenomics mode since 0.7.1 (2026-09-11).** On a `tokenomics20` client all three go to the deployment's 2.0 MCR server (`mcr-20.peaq.xyz` for `peaq-mainnet`); `client.api_url` is not read. Machine DIDs are `did:peaq:`, operator DIDs stay `did:peaq:0x
`, `machine_id` is a lossless `int`. A malformed or non-canonical `machine_id`, or a response about a different machine or operator, raises `ApiError` `BAD_RESPONSE`. A deployment without a paired MCR (`agung-2026-08-28`) raises `TokenomicsConfigError` `DEPLOYMENT_UNAVAILABLE` before any HTTP. Legacy clients keep calling `mcr.peaq.xyz` with `did:peaq:0x` DIDs. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines). Read-only helpers backed by the off-chain MCR API server (`client.api_url`). Each function validates the DID, issues a single `GET` through `client.session`, and returns a shape-checked `TypedDict`. The default per-request timeout is 30 seconds, enforced inside `peaq_os_sdk.query.http_client.get_json`; the public `query_*` functions do not currently expose an override. ### `query_mcr` Fetches the Machine Credit Rating for a machine DID. See [`GET /mcr/{did}`](/peaqos/api-reference/get-mcr). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def query_mcr(client: PeaqosClient, did: str) -> MCRResponse: ... ``` Machine DID. `did:peaq:0x
` on a legacy client, `did:peaq:` on a `tokenomics20` client (0.7.1+). Snake\_case keys: `did: str`, `machine_id: int`, `mcr_score: int` (0–100), `mcr: str` (`"AAA" | "AA" | "A" | "BBB" | "BB" | "B" | "NR" | "Provisioned"`), `bond_status: str` (`"bonded" | "unbonded"`), `negative_flag: bool`, `event_count: int`, `revenue_event_count: int`, `activity_event_count: int`, `revenue_trend: str` (`"up" | "stable" | "down" | "insufficient"`), `total_revenue: float` (USD cents; divide by 100 for display), `average_revenue_per_event: float` (USD cents; divide by 100 for display), `last_updated: int | None`, `mcr_degraded: bool` (`true` when ≥1 scored event used a stale or unavailable FX source). `mcr_score` is always an `int`. The SDK coerces a `null` score from the API to `0`. `Provisioned` machines (bonded but not yet scored) surface as `0`, matching the JavaScript SDK. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.query import query_mcr mcr = query_mcr(client, "did:peaq:0xMachineAddress") print(mcr["mcr_score"], mcr["mcr"]) ``` **Errors:** `ValidationError` if `did` does not start with `did:peaq:0x`. `ApiError` with `code` in `NOT_FOUND` (HTTP 404), `SERVICE_UNAVAILABLE` (503), `SERVER_ERROR` (other 5xx), `HTTP_ERROR` (other non-2xx), `BAD_RESPONSE` (malformed body), `TIMEOUT`, `NETWORK_ERROR`. ### `query_machine` Fetches the full machine profile (NFT Metadata JSON v1.0) and validates the response shape against a strict TypedDict. The SDK raises `BAD_RESPONSE` if the server payload does not match the schema. See [`GET /machine/{did}`](/peaqos/api-reference/get-machine). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def query_machine(client: PeaqosClient, did: str) -> MachineProfileResponse: ... ``` Machine DID. `did:peaq:0x
` on a legacy client, `did:peaq:` on a `tokenomics20` client (0.7.1+). Top-level keys: `schema_version: str`, `name: str`, `peaqos: PeaqosData`. `PeaqosData` always carries `machine_id: int`, `did: str`, `operator: str | None`, `mcr: str`, `mcr_score: int`, `bond_status: str`, `negative_flag: bool`, `event_count: int`, `data_visibility: str`, `documentation_url: str | None`. Visibility-dependent extras (`data_api: str`, `event_data: list[EventEntry]`, `partner_data: dict[str, Any]`, `partner_data_error: str`) are present only when the API includes them. Use `"data_api" in profile["peaqos"]` (etc.) to test for presence rather than truthiness. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.query import query_machine profile = query_machine(client, "did:peaq:0xMachineAddress") print(profile["schema_version"], profile["name"]) print(profile["peaqos"]["mcr"], profile["peaqos"]["mcr_score"]) ``` **Errors:** `ValidationError` on invalid DID. `ApiError` with the same codes as `query_mcr`; `BAD_RESPONSE` if any required field is missing, has the wrong type, or is out of range. ### `query_operator_machines` Fetches the fleet of machines managed by a proxy operator. Each entry is individually validated. See [`GET /operator/{did}/machines`](/peaqos/api-reference/get-operator-machines). ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} def query_operator_machines( client: PeaqosClient, did: str, ) -> OperatorMachinesResponse: ... ``` Operator DID. Must start with `did:peaq:0x`. Keys: `operator_did: str`, `machines: list[OperatorMachine]`, and `pagination: Pagination`. Each `OperatorMachine` has `did: str`, `machine_id: int`, `mcr_score: int` (0–100), `mcr: str`, and `negative_flag: bool`. `Pagination` carries `offset: int`, `limit: int`, and `total: int`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.query import query_operator_machines fleet = query_operator_machines(client, "did:peaq:0xProxyAddress") for m in fleet["machines"]: print(m["machine_id"], m["mcr_score"], m["mcr"]) ``` **Errors:** `ValidationError` on invalid DID. `ApiError` with the same codes as `query_mcr`; `BAD_RESPONSE` if the body or any `machines` entry is malformed. *** ## Error classes ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( PeaqosError, ValidationError, RpcError, ApiError, ValueCapExceeded, RateLimitExceeded, # Economics 2.0 (0.6.0+) TokenomicsConfigError, TokenomicsActivationError, TokenomicsPendingTransactionError, TokenomicsUnsupportedError, TokenomicsIntegrationUnavailableError, ) from peaq_os_sdk.monetization import ( MonetizationError, MonetizationCompatibilityError, MonetizationNetworkError, MonetizationResponseError, MonetizationTimeoutError, MonetizationCancelledError, ) ``` See [errors](/peaqos/sdk-reference/errors) for the full hierarchy, the 20-code faucet table, and the [Tokenomics 2.0 code tables](/peaqos/sdk-reference/errors#tokenomics-2-0-errors). *** ## Constants ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( SUBSCRIPTION_TIER_ENTRY, # 0 SUBSCRIPTION_TIER_BASIC, # 1 SUBSCRIPTION_TIER_PRO, # 2 SUBSCRIPTION_TIERS, # (0, 1, 2) DEFAULT_ACTIVATION_CONFIRMATIONS, # 1 DEFAULT_ACTIVATION_TIMEOUT_SECONDS, # 120.0 (seconds; the JS SDK's timeoutMs is milliseconds) TOKENOMICS_2_0_DEPLOYMENTS, # read-only mapping of deployment records TOKENOMICS_MONETIZATION_API_VERSION, # "tokenomics-2.0-monetization-v1" ) ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( MACHINE_STATUS_NONE, # 0 MACHINE_STATUS_PENDING, # 1 MACHINE_STATUS_VERIFIED, # 2 MACHINE_STATUS_REJECTED, # 3 MACHINE_STATUS_DEACTIVATED, # 4 ) ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( MCR_RATING_AAA, MCR_RATING_AA, MCR_RATING_A, MCR_RATING_BBB, MCR_RATING_BB, MCR_RATING_B, MCR_RATING_NR, ) # Bonded-but-unscored machines are returned by the MCR API with the literal # rating string "Provisioned" and mcr_score coerced to 0 by the SDK. # No SDK constant yet for the "Provisioned" rating string. ``` Identical to the JS SDK: `EVENT_TYPE_REVENUE`, `EVENT_TYPE_ACTIVITY`, `TRUST_SELF_REPORTED`, `TRUST_ON_CHAIN_VERIFIABLE`, `TRUST_HARDWARE_SIGNED`, `DID_ATTR_*`, `DID_MAX_NAME_BYTES`, `DID_MAX_VALUE_BYTES`, `SUPPORTED_CHAINS`, `LAYERZERO_EIDS`, `DEFAULT_API_URL`. Same values, Python-idiomatic names. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( DATA_VISIBILITY_PUBLIC, # "public" DATA_VISIBILITY_PRIVATE, # "private" DATA_VISIBILITY_ONCHAIN, # "onchain" ) ``` Pass to `data_visibility=` on `write_machine_did_attributes`. Validated server-side; using one of these constants avoids typos. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import ( IMPORT_CHAIN_EVM, # "evm" (default) IMPORT_CHAIN_SOLANA, # "solana" IMPORT_CHAIN_BITCOIN, # "bitcoin" IMPORT_CHAIN_COSMOS, # "cosmos" IMPORT_CHAIN_TRON, # "tron" IMPORT_CHAIN_TON, # "ton" IMPORT_CHAIN_SUI, # "sui" IMPORT_CHAIN_XRPL, # "xrpl" IMPORT_CHAIN_SPARK, # "spark" IMPORT_CHAIN_FILECOIN, # "filecoin" OWS_PASSPHRASE_ENV, # "OWS_PASSPHRASE" ) ``` Used by `import_wallet(..., chain=IMPORT_CHAIN_SOLANA)`. The JS SDK exports identical constants. See [JS Constants](/peaqos/sdk-reference/sdk-js#constants). # Stream Source: https://docs.peaq.xyz/peaqos/sdk-reference/stream The stream module on the JS and Python SDKs: sign and verify machine data packages, build encrypted chunk chains, and grant buyers access. The `stream` module ships in `@peaqos/peaq-os-sdk` and `peaq-os-sdk` **0.3.0+**. Its exports are flagged `@experimental` and may change as the surface settles. This page covers the cryptographic core; selling and shipping the encrypted chunks — purchases, payment rails, and S3 / P2P delivery — is the **0.4.0+** [distribution surface](/peaqos/sdk-reference/stream-distribution). The `stream` module is the cryptographic core of [Stream](/peaqos/functions/stream): it signs the data a machine produces, chunks and encrypts it, and re-wraps chunk keys so a buyer can decrypt only what they bought. You hand it a payload and it returns signed, verifiable, encrypted artifacts — topic subscription and transport are handled by the on-machine [Edge Agent](/peaqos/functions/stream), which drives this same module from a Data Event Map. ## Module location | SDK | Import | | :---------------------- | :------------------------------------ | | JavaScript / TypeScript | `@peaqos/peaq-os-sdk` → `src/stream/` | | Python | `peaq_os_sdk` → `peaq_os_sdk.stream` | ## Functions | Function | Purpose | | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `buildSignedDataPackage` | Apply field rules to a topic payload, bundle it with the machine's identity (DID, timestamp, schema version, sequence number), and sign it EIP-191. Async. | | `verifyDataPackage` | Recover the signer and check it against the public key derived from the DID. No network needed. Async. | | `buildChunkChain` | Split data into bounded chunks, encrypt each with a fresh per-chunk key wrapped to the owner, operator, and machine, and link each chunk to the previous one. One caller-supplied Ed25519 key signs the whole chain; only the encryption key is per-chunk. | | `verifyChunkChain` | Confirm the chain is intact — no gaps, reordering, or edits. | | `createBuyerAccessEntry` | Unwrap a chunk's key with the owner's private key and re-wrap it to a buyer's public key. This is the access-grant primitive. | | `buildBuyerAccessFiles` | Re-wrap a chunk chain's keys for a buyer and write distributable access files (sharded by size). Takes the chunks, not pre-built entries. | | `decryptChunk` | Decrypt a chunk with a recipient's private key and wrapped-key entry. | | `computeChunkId` | Recompute a chunk's content-derived ID for integrity checks. | Python exposes the same surface in snake\_case (`build_signed_data_package`, `verify_data_package`, `build_chunk_chain`, `verify_chunk_chain`, `create_buyer_access_entry`, `build_buyer_access_files`, `decrypt_chunk`, `compute_chunk_id`). ## Types ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} type FieldRuleAction = "include" | "exclude" | "encrypt" | "anonymize"; type AnonymizeStrategy = "hash" | "generalize" | "redact"; type KeyStorageBackend = "tee" | "software"; type UnsignedDataPayload = { topic: string; // ROS 2 topic or feed name fields: Record; capturedAt: string; // ISO 8601 }; type TopicFieldRules = { topic: string; rules: FieldRule[]; // ordered; first matching fieldPath wins defaultAction?: FieldRuleAction; }; type FieldRule = { fieldPath: string; // dot-notation, e.g. "gps.latitude" action: FieldRuleAction; anonymizeStrategy?: AnonymizeStrategy; }; type DataPackage = { did: string; // did:peaq:0x... timestamp: string; // ISO 8601 schemaVersion: string; sequenceNumber: number; payload: Record; signature: string; // EIP-191 personal_sign signingKeyId: string; }; type VerificationResult = { valid: boolean; signerDid: string; reason?: string; }; ``` Signing credentials ride in a `SigningContext` (`{ privateKey, backend: "tee" | "software", did, keyId }` — `toJSON` redacts the key so it never leaks through serialization) and field-level encryption in an `EncryptionContext` (`{ encryptionKey, algorithm: "AES-256-GCM" }`). The field layer uses AES-256-GCM; the per-chunk layer (inside `buildChunkChain`) uses XChaCha20-Poly1305. Chunking defaults: `chunkSize` 262144 bytes (256 KiB), `hashAlgorithm` `"sha-256"`, via `ChunkingConfig`. The full envelope shape is documented under [Data streams → The chunk envelope](/peaqos/concepts/data-streams#the-chunk-envelope). ## Example Sign a reading, verify it, chunk-and-encrypt, then grant a buyer access: ```ts stream.ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { buildSignedDataPackage, verifyDataPackage, buildChunkChain, createBuyerAccessEntry, buildBuyerAccessFiles, SigningContext, EncryptionContext, } from "@peaqos/peaq-os-sdk"; const pkg = await buildSignedDataPackage({ unsignedPayload: { topic: "/vehicle/telemetry", fields: { speed: 12, gps: { latitude: 47.1, longitude: 8.5 } }, capturedAt: new Date().toISOString(), }, topicRules: { topic: "/vehicle/telemetry", rules: [ { fieldPath: "gps.latitude", action: "encrypt" }, { fieldPath: "gps.longitude", action: "encrypt" }, ], defaultAction: "include", }, signingCtx: new SigningContext({ privateKey: process.env.MACHINE_DATA_KEY!, backend: "software", did: "did:peaq:0xMACHINE", keyId: "did:peaq:0xMACHINE#keys-1", }), encryptionCtx: new EncryptionContext({ encryptionKey: process.env.FIELD_KEY!, algorithm: "AES-256-GCM", }), schemaVersion: "1", sequenceNumber: 1, }); const result = await verifyDataPackage(pkg); if (!result.valid) throw new Error(result.reason); // Chunk + encrypt; keys wrapped to owner, operator, and machine const { chain, encryptedData } = await buildChunkChain({ input: new TextEncoder().encode(JSON.stringify(pkg)), ownerPublicKeyHex: OWNER_X25519_PUB, operatorPublicKeyHex: OPERATOR_X25519_PUB, machinePublicKeyHex: MACHINE_X25519_PUB, signingPrivateKeyHex: ED25519_PRIV, signingPublicKeyHex: ED25519_PUB, machineDid: "did:peaq:0xMACHINE", machineKeyId: "did:peaq:0xMACHINE#keys-1", did: "did:peaq:0xMACHINE", // config: { chunkSize: 262144 } }); // Grant a buyer: re-wrap the purchased chunk keys to the buyer's public key const accessFiles = buildBuyerAccessFiles({ chunks: chain.chunks, ownerPrivateKeyHex: OWNER_X25519_PRIV, buyerRecipientId: "did:peaq:0xBUYER", buyerPublicKeyHex: BUYER_X25519_PUB, }); // createBuyerAccessEntry is the per-chunk primitive behind it const oneEntry = createBuyerAccessEntry({ chunk: chain.chunks[0], ownerPrivateKeyHex: OWNER_X25519_PRIV, buyerRecipientId: "did:peaq:0xBUYER", buyerPublicKeyHex: BUYER_X25519_PUB, }); ``` ```python stream.py theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.stream import ( build_signed_data_package, verify_data_package, build_chunk_chain, create_buyer_access_entry, build_buyer_access_files, ) # Same flow in snake_case: sign the payload with the machine's # signing context, verify, build the encrypted chunk chain, then # re-wrap chunk keys for the buyer. # build_signed_data_package takes keyword-only args — there is no params # object (BuildSignedDataPackageParams is JS-only). Mirrors the TS call above. pkg = build_signed_data_package( unsigned_payload=unsigned_payload, topic_rules=topic_rules, signing_ctx=signing_ctx, encryption_ctx=encryption_ctx, schema_version="1", sequence_number=1, ) result = verify_data_package(pkg) assert result.valid, result.reason chain_result = build_chunk_chain(params) # BuildChunkChainParams # Batch helper: re-wrap every purchased chunk key to the buyer (takes chunks) access_files = build_buyer_access_files( chunks=chain_result.chain.chunks, owner_private_key_hex=OWNER_X25519_PRIV, buyer_recipient_id="did:peaq:0xBUYER", buyer_public_key_hex=BUYER_X25519_PUB, ) # create_buyer_access_entry is the per-chunk primitive behind it one_entry = create_buyer_access_entry( chunk=chain_result.chain.chunks[0], owner_private_key_hex=OWNER_X25519_PRIV, buyer_recipient_id="did:peaq:0xBUYER", buyer_public_key_hex=BUYER_X25519_PUB, ) ``` `buildChunkChain` returns the chain plus `encryptedData`, a map of ciphertext bytes keyed by chunk index. The ciphertext is a pre-upload sidecar — callers store the bytes wherever they distribute from and set each chunk's `storageRef`. The buyer decrypts with `decryptChunk({ chunk, recipientPrivateKeyHex, recipientEntry, encryptedData })` — in Python, `decrypt_chunk(chunk=…, recipient_private_key_hex=…, recipient_entry=…, encrypted_data=…)` — passing the chunk's stored ciphertext bytes plus a `KeyRecipient` from `chunk.encryption.keyRecipients` or from a buyer access entry. The data itself is never re-encrypted when access is granted. From the terminal, the same publish and grant flows are [`peaqos stream publish`](/peaqos/cli#peaqos-stream) and [`peaqos stream grant`](/peaqos/cli#peaqos-stream). ## Errors | Error | When | | :---------------------- | :----------------------------------------------------------------------------------------------- | | `StreamValidationError` | Invalid input — bad DID, missing field, unknown rule, wrong key length, key-commitment mismatch. | | `StreamSigningError` | Signing or verification failed. Messages never contain key material. | | `StreamError` | Base class for the above. | ## Solana signing OWS mnemonic-derived wallets carry a Solana account (ed25519, derivation path `m/44'/501'/0'/0'`) for cross-chain payments. This is **wallet** signing — separate from Stream data signing, which uses EIP-191. Since SDK **0.4.0**, the vault signs Solana transactions natively: `PeaqosClient.solanaSignerFromWallet()` returns a signer for the active OWS wallet's Solana account, and [`transferToken`](/peaqos/sdk-reference/stream-distribution#buyer-paying-on-chain) uses it for SPL and native transfers when paying for stream data (`peaqos stream pay --chain solana` from the terminal). Solana-quoted **Machine Market** orders are still paid externally — complete the transfer with your Solana wallet and pass `--payment-tx-hash` to `peaqos scale order`; there is no `peaqos solana` command group. ## Related * [Stream: Distribution & P2P delivery](/peaqos/sdk-reference/stream-distribution) * [Stream function](/peaqos/functions/stream) * [Data streams concept](/peaqos/concepts/data-streams) * [peaqOS CLI: stream](/peaqos/cli#peaqos-stream) * [Wallets (OWS)](/peaqos/wallets) # Stream: Distribution & P2P delivery Source: https://docs.peaq.xyz/peaqos/sdk-reference/stream-distribution Sell and deliver stream data end-to-end: purchases, payment rails (transfer and x402), the seller control socket, and delivery — buyer access over S3, or encrypted chunks machine-to-machine over the peaqos-p2p transport. The distribution surface ships in `@peaqos/peaq-os-sdk` and `peaq-os-sdk` **0.4.0+** and is flagged `@experimental`. It builds on the [`stream` crypto module](/peaqos/sdk-reference/stream) (0.3.0+): everything here moves chunks that `buildChunkChain` already signed and encrypted. The [`stream` module](/peaqos/sdk-reference/stream) makes machine data sellable; this surface actually sells and ships it. It covers the whole buyer-to-seller loop: a buyer creates a **purchase** against a listing, pays over a **transfer** or x402 rail, and the seller — notified over a **control socket** — re-wraps the purchased chunk keys and delivers through a pluggable **delivery channel**: S3 (the buyer-access files plus a pre-signed download URL, with the ciphertext staying at each chunk's `storageRef`) or the `peaqos-p2p` transport (the encrypted chunks themselves, streamed machine-to-machine). The seller never decrypts or re-encrypts chunk data at any point; only the buyer-access documents (wrapped per-chunk keys) are generated during distribution. ## The distribution lifecycle ```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}} sequenceDiagram participant B as Buyer participant O as Orchestration participant S as Seller (machine) S->>O: connectControlSocket (WebSocket) B->>O: 1. createPurchase (buyer ID + X25519 key) O-->>S: purchase.created B->>O: 2. createPurchasePaymentIntent (transfer | x402) B->>B: 3. pay on-chain (peaq / Base / Solana) or sign x402 B->>O: 4. submitPurchasePaymentProof O-->>S: purchase.payment_proof.recorded S->>O: 5. prepareBuyerAccess (access entries + connect handoff) B->>O: 6. getPurchaseDelivery (session + access entries) S-->>B: 7. encrypted chunks over peaqos-p2p B->>B: 8. verify, decrypt, reassemble ``` Every step is a stateless API call — the SDK holds no purchase state. The buyer authenticates with an agent-pairing token (`x-agent-pairing-token`), the seller's machine agent with `x-machine-agent-id` + `x-machine-agent-token`, and platform reads with `x-api-key`. ## Delivery setup (before any purchase) Both sides discover what the orchestration service supports instead of hardcoding it, and machines register what they can handle: | Function | Purpose | | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `listPaymentRails` | Returns the supported rails — `X402PaymentRail` (`chains`, `tokens`, `requiresProof`) or `TransferPaymentRail` (`chains`, `tokens`, `verification`), discriminated by `type`. | | `listDeliveryTransports` | Returns the available transports, discriminated by `mode`: `p2p` (`transportId`, `version`, `features`, `protocols`, `encoding`), `storage` (`provider: "walrus" \| "s3"`), or `tunnel` (`provider: "cloudflare"`). | | `putDeliveryCapabilities` | Registers what a machine can support (buyer or seller side) — after a machine is provisioned, its capability advertisement is what makes it reachable for delivery. The server populates `mode` and `signaling`; capabilities are short-lived and renewable. | | `updateStreamListingPurchaseFields` | Adds `paymentRails` and `deliveryOptions` to an existing stream listing. Additive — existing listing fields are untouched. | Capability advertisements carry transport identity only. **Never put secrets — provider API keys, tokens, private keys — into a capability advertisement or listing**; sensitive values stay in local secrets or encrypted payloads. Shared value unions across this surface: `PurchasePaymentRail` (`"x402" | "transfer"`), `PurchaseChain` (`"peaq" | "base" | "ethereum" | "solana"`), `PurchaseToken` (`"USDC" | "USDT" | "PEAQ"`), `PurchaseDeliveryMode` (`"p2p" | "storage" | "tunnel"`), `PurchaseResourceType` (`"stream.bundle" | "stream.live"`) — each with `SCREAMING_SNAKE_CASE` constants (`PURCHASE_CHAIN_BASE`, `PURCHASE_TOKEN_USDC`, …). ## Buyer: purchases ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { createPurchase, getPurchase, createPurchasePaymentIntent, submitPurchasePaymentProof, getPurchaseDelivery, createPurchaseEvent, } from "@peaqos/peaq-os-sdk"; // 1. Declare intent: listing, optional chunk selection, buyer identity + X25519 key const purchase = await createPurchase(client, { resource: { type: "stream.listing", listingId: "listing-abc", selection: { chunkIds: ["chunk-1", "chunk-2"] }, }, buyer: { id: "did:peaq:0xBUYER", publicKey: { type: "x25519", publicKeyHex: BUYER_X25519_PUB }, }, }, pairingToken); // 2. Poll until delivery_reserved const state = await getPurchase(client, purchase.item.id); // 3. Declare the payment rail const intent = await createPurchasePaymentIntent(client, purchase.item.id, { rail: { type: "transfer", destination: { payTo: "0xSELLER" } }, }, pairingToken); // intent.instructions is a union: // { send: { chain, token, amount, to } } — transfer rail // { protocol: "x402", message } — x402 rail // 4-5. Pay on-chain, then submit proof const proof = await submitPurchasePaymentProof(client, purchase.item.id, { rail: "transfer", transactionHash: "0xtxhash...", chain: "base", token: "USDC", verificationMode: "rpc", // optional: "recorded" | "rpc" rpcUrl: "https://rpc.base.org", // optional, for RPC verification }, pairingToken); // proof.deliveryStatus: "blocked" | "releasable" // proof.receipt: { verificationMode, status, blockNumber?, verifiedAt } // 6. Retrieve the delivery session + access entries // (409 STREAM_DELIVERY_NOT_READY until the seller prepares access) const delivery = await getPurchaseDelivery(client, purchase.item.id, pairingToken); // delivery.delivery: sessionId, transportId, protocols, encoding, connect handoff // delivery.access: peaq.stream.buyer-access.v1 entries (wrapped chunk keys) ``` For the x402 rail, the proof body is `{ rail: "x402", paymentHeader }` — the signed payment header, no on-chain transfer of your own. **Purchase status** progresses `delivery_reserving → delivery_reserved → payment_pending → payment_verified → delivery_released → active → completed` (or `cancelled` / `failed`). Payment status is `pending | verified`; delivery status is `requested | ready | active | completed | failed`. Both sides record lifecycle progress with `createPurchaseEvent` — event types `delivery.started`, `delivery.completed`, `delivery.failed`, `buyer.connected`, `seller.ready`, `seller.offline`. Invalid types are rejected client-side. ## Buyer: paying on-chain Helpers for the transfer rail on peaq, Base, and Solana. Token decimals resolve from a well-known registry (USDC/USDT on peaq, USDC on Base/Solana) or an explicit `tokenDecimals` override — no on-chain `decimals()` lookups. | Function | Purpose | | :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | | `transferToken` | Native or token transfer routed by `chain`; returns `TokenPaymentResult` with the tx hash / signature. | | `submitPaymentProof` | POSTs the proof JSON to a `confirmationUrl`; expects `{ accepted: boolean }`. (Distinct from `client.orchestration.submitPaymentProof()`.) | | `payAndSubmitProof` | Chains the two. On partial failure returns `{ payment, proof: null, proofError }` so the tx hash can be resubmitted instead of paying twice. | | Chain | Extra params | Optional peer deps | | :------- | :----------------------- | :------------------------------------------------ | | `peaq` | — | `viem` (required peer) | | `base` | `rpcUrl` | `viem` | | `solana` | `rpcUrl`, `solanaSigner` | `@solana/web3.js`, `@solana/spl-token` (SPL only) | A Solana signer comes from the OWS wallet via `PeaqosClient.solanaSignerFromWallet()` — OWS-native Solana signing also ships in 0.4.0. ## Seller: getting paid and preparing access The seller's primary signal is the **control socket** — a WebSocket to the orchestration service that carries short control notifications (never bulk data): ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { connectControlSocket, prepareBuyerAccess } from "@peaqos/peaq-os-sdk"; const conn = await connectControlSocket(client, machineId, agentId, agentToken, { onConnected(msg) { console.log("connected:", msg.machineId); }, async onNotification(msg) { if (msg.type === "purchase.payment_proof.recorded") { const { purchaseId } = msg.payload as { purchaseId: string }; // payment verified — prepare buyer access now } }, onAck() {}, onError(err) { console.error(err); }, onClose() {}, }); ``` When the control socket is unavailable, `PollingConfirmationProvider` is the fallback: it polls a `confirmationUrl` (appending `?orderId=...`) every `pollInterval` ms (default 30 000) until `status === "confirmed"` or `timeout` seconds (default 3 600) elapse, then resolves a `PaymentConfirmation` (`orderId`, `buyerId`, `buyerPublicKeyHex`, `txHash`, `status`, `confirmedAt`). HTTP 5xx retries silently; 4xx throws `StreamValidationError`; timeout throws `StreamTimeoutError`; `stop()` aborts immediately. The polling surface (and `DistributePackage`) tracks the sale as `orderId`; in the orchestration purchases flow the same identifier is the `purchaseId` — `peaqos stream distribute --order-id` documents it as exactly that. On confirmation the seller submits the re-wrapped chunk keys plus a short-lived connection handoff: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} const result = await prepareBuyerAccess(client, purchaseId, { machineId, agentId, agentToken, items: buyerAccessEntries, // from buildBuyerAccessFiles delivery: { mode: "p2p", transportId: "peaqos-p2p", connect: { type: "peaqos-p2p-url", url: connectUrl, expiresAt }, }, }); // result.access — the submitted entries; result.deliverySession — session + connect handoff ``` Every request on this path is authenticated against the machine's agent identity, and delivery only proceeds for a purchase whose payment has been verified — requests against an unpaid or unprepared purchase are rejected. The `delivery.connect.url` is a secret handoff: never log it. ### `distributeData` and delivery channels `distributeData` packages the seller side into one call — generate the buyer access files and hand them to a `DeliveryChannel`: ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { distributeData, S3DeliveryChannel } from "@peaqos/peaq-os-sdk"; const channel = new S3DeliveryChannel({ bucket: "my-data-bucket", prefix: "stream/", region: "us-east-1", // optional — AWS credential chain otherwise endpoint: "https://…", // optional — MinIO, R2, Spaces presignExpiry: 7200, // seconds, default 3 600 }); const result = await distributeData({ chunks: chain.chunks, // from buildChunkChain ownerPrivateKeyHex, // never logged; wiped after key wrapping payment: confirmation, // must be status "confirmed" deliveryChannel: channel, // maxFileSize: 512_000 // bytes per access file before splitting }); // result.chunksDistributed, result.buyerAccessFiles, // result.delivery.downloadUrl — pre-signed S3 URL ("p2p" channels return null) ``` `S3DeliveryChannel` uploads each access file to `{prefix}{buyerId}/{fileName}` and needs `@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner` (optional peer deps — missing packages throw `StreamConfigError` with the install hint). Custom channels implement the single-method `DeliveryChannel` interface: `deliver(pkg: DistributePackage) => Promise`, where `pkg` carries `buyerAccessFiles`, `encryptedDataRefs`, `buyerId`, and `orderId`. ## P2P delivery The `peaqos-p2p` transport moves the encrypted chunks **directly machine-to-machine** — no storage bucket in the middle. The seller runs a long-lived channel; the buyer dials in with a transient node, receives the chunks, verifies, and decrypts. Delivery here is transport and hand-over only: what a machine is allowed to serve is still governed by its provisioning and registered capabilities. ### Seller: `P2PDeliveryChannel` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { P2PDeliveryChannel } from "@peaqos/peaq-os-sdk"; const channel = new P2PDeliveryChannel( machineId, agentId, agentToken, // secret — never logged client, // PeaqosClient resolveChunkData, // (ref: string) => Promise<{ envelope: Chunk; ciphertext: Uint8Array }> { capabilityTtlSeconds: 3600, // default 1 h connectTtlSeconds: 300, // default 5 min; must be < capabilityTtlSeconds connectionTimeoutMs: 30_000, chunkTransferTimeoutMs: 60_000, }, ); await channel.start(); // idempotent const result = await channel.deliver(pkg); // { channel: "p2p", downloadUrl: null, deliveredAt } await channel.stop(); ``` `start()` discovers the `peaqos-p2p` transport via `listDeliveryTransports`, validates it against a fixed allowlist, creates the node, registers the machine's delivery capability, schedules capability renewal, and installs a single central accept loop for inbound buyer connections. `deliver()` implements `DeliveryChannel`, so it plugs straight into `distributeData`. Called after payment confirmation, it resolves and validates every chunk against the access entries, builds a short-lived `delivery.connect` handoff (an opaque `peaqos-p2p://` URL), calls `prepareBuyerAccess`, waits for the buyer to connect, streams the chunks as frames, and emits `delivery.started` / `delivery.completed` / `delivery.failed` progress events. Inbound handshakes are routed by `(purchaseId, sessionId, buyerId)`; sessions are **single-use with tombstone replay protection**. ### Buyer: `P2PDeliveryReceiver` ```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { P2PDeliveryReceiver, getPurchaseDelivery } from "@peaqos/peaq-os-sdk"; const receiver = new P2PDeliveryReceiver( buyerId, client, recipientPrivateKeyHex, // X25519 private key — never logged { connectionTimeoutMs: 30_000, chunkReceiveTimeoutMs: 60_000 }, ); const delivery = await getPurchaseDelivery(client, purchaseId, pairingToken); const { data, chunksReceived, completedAt } = await receiver.receive({ purchaseId, delivery }); ``` `receive()` validates the negotiated session (mode, transport, encoding, connect expiry), dials the seller over `delivery.connect.url`, identifies itself with a handshake frame, and then — for each chunk — verifies the ciphertext hash, signature, and chain linking **before** decrypting with `decryptChunk`, runs `verifyChunkChain` as a final cross-check, and reassembles the plaintext in index order. The transient node is closed in `finally`, success or failure. ### Wire format and dependencies Frames use `peaqos-json-frame-v1` — newline-delimited JSON with types `handshake`, `chunk`, `complete`, `error`. The libp2p stack is wrapped entirely inside the SDK: no libp2p types, peer IDs, or multiaddrs appear in the public API, and `delivery.connect` URLs are redacted from all SDK-controlled logs, errors, and telemetry. libp2p ships as **optional peer dependencies** — install them only when you use P2P delivery: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install libp2p @libp2p/tcp @chainsafe/libp2p-yamux @chainsafe/libp2p-noise @multiformats/multiaddr ``` Without them, the channel throws `StreamConfigError` carrying exactly that install hint. ## Errors Distribution extends the [Stream error hierarchy](/peaqos/sdk-reference/stream#errors): | Error | When | Extras | | :-------------------- | :--------------------------------------------- | :---------------------------------------- | | `StreamTimeoutError` | Confirmation polling timed out. | `.timeout` (s), `.orderId` | | `StreamPaymentError` | On-chain transfer or proof submission failed. | `.txHash` (when available), `.reason` | | `StreamConfigError` | Optional dependency missing (AWS SDK, libp2p). | `.installHint` | | `StreamDeliveryError` | Seller-side P2P delivery failure. | `.channel: "p2p"`, `.purchaseId`, `.code` | | `StreamReceiveError` | Buyer-side P2P reception failure. | `.purchaseId`, `.code` | `.code` is a coarse public `StreamErrorCode`: `"config"` (missing dependency, transport/capability negotiation failure), `"protocol"` (wire/frame/session violation, verification failure), or `"transfer"` (connection, timeout, or data-transfer failure). Error messages are sanitized — no key material, no raw transport exceptions. Purchase functions throw the standard [orchestration errors](/peaqos/sdk-reference/errors) (`OrchestrationApiError`, `OrchestrationValidationError`, …). ## Python and the CLI The Python SDK ships the same distribution surface in snake\_case. From the terminal, [`peaqos stream distribute`](/peaqos/cli#peaqos-stream-distribute) (payment listener + S3 delivery), [`peaqos stream pay`](/peaqos/cli#peaqos-stream-pay) / [`payproof`](/peaqos/cli#peaqos-stream-payproof) (buyer transfer + proof), and [`peaqos stream consume --download-url`](/peaqos/cli#peaqos-stream-consume) (remote release packages) wrap it — P2P delivery itself is SDK-level and has no CLI flag yet. ## Related * [Stream crypto module](/peaqos/sdk-reference/stream) — signing, chunking, encryption, access grants * [Stream function](/peaqos/functions/stream) — what Stream is and what ships * [Data streams concept](/peaqos/concepts/data-streams) — the trust model * [Stream data marketplace API](/peaqos/api-reference/stream-marketplace) — the buyer-side HTTP surface * [peaqOS CLI: stream](/peaqos/cli#peaqos-stream) # Wallets (OWS) Source: https://docs.peaq.xyz/peaqos/wallets Encrypted, mnemonic-backed wallets for peaqOS via the Open Wallet Standard. Optional, opt-in, fully local. peaqOS supports the [Open Wallet Standard v1.3](https://docs.openwallet.sh/) for wallet generation, encrypted storage, signing, and display. The current raw-key flow uses hex private keys in `.env` files, protected only by file permissions. That works, but it has limits: * No encryption at rest * No mnemonic backup or recovery * No multi-chain visibility. One secp256k1 key derives addresses on every EVM chain, Cosmos, Solana, Bitcoin, Tron, and more, but nothing surfaces them * No standard import / export to wallets like MetaMask or hardware devices * No audit log of key creation, signing, or export events OWS solves all of these with an encrypted local vault, BIP-39 derivation, CAIP-2 chain identifiers, and an append-only audit log: fully local, no cloud, no remote services. OWS is **optional**. Use the SDK helpers below to manage wallets. The raw-key path keeps working unchanged. ## Multi-chain accounts from one mnemonic A mnemonic-derived wallet generates accounts across every OWS-supported chain family from a single 12- or 24-word phrase. | Family | Curve | Coin Type | Default Path | Address Format | | :---------------------------------------- | :-------- | :-------- | :-------------------------- | :--------------------------- | | EVM (peaq, Base, Ethereum, Polygon, etc.) | secp256k1 | 60 | `m/44'/60'/0'/0/{index}` | EIP-55 checksummed hex | | Solana | ed25519 | 501 | `m/44'/501'/{index}'/0'` | Base58 public key | | Bitcoin | secp256k1 | 0 | `m/84'/0'/0'/0/{index}` | Bech32 native segwit | | Cosmos | secp256k1 | 118 | `m/44'/118'/0'/0/{index}` | Bech32 | | Tron | secp256k1 | 195 | `m/44'/195'/0'/0/{index}` | Base58Check | | TON | ed25519 | 607 | `m/44'/607'/{index}'` | Base64url wallet v5r1 | | Sui | ed25519 | 784 | `m/44'/784'/{index}'/0'/0'` | 0x + BLAKE2b-256 hex | | XRPL | secp256k1 | 144 | `m/44'/144'/0'/0/{index}` | Base58Check | | Spark | secp256k1 | 8797555 | `m/84'/0'/0'/0/{index}` | `spark:` + compressed pubkey | | Filecoin | secp256k1 | 461 | `m/44'/461'/0'/0/{index}` | `f1` + base32 | The SDK guarantees an account entry for every protocol-recognized EVM chain (`SUPPORTED_CHAIN_IDS`). If OWS doesn't return one, it's synthesized from the wallet's primary EVM address, which is identical across all EVM chains: | Network | CAIP-2 | Chain ID | | :------- | :------------- | :------- | | peaq | `eip155:3338` | 3338 | | ethereum | `eip155:1` | 1 | | base | `eip155:8453` | 8453 | | polygon | `eip155:137` | 137 | | arbitrum | `eip155:42161` | 42161 | | optimism | `eip155:10` | 10 | Each account is exposed as a [CAIP-10](https://chainagnostic.org/CAIPs/caip-10) account ID (`eip155:3338:0x...`), so a single peaqOS wallet can sign on peaq, Base (for `bridgeNft`), and any other supported chain without ever exporting the key. ## Vault layout The vault lives at `~/.ows/` (override with `OWS_VAULT_PATH`). Structure is set by OWS; peaqOS uses it as-is. ``` ~/.ows/ ├── config.json # OWS settings (700) ├── wallets/ # (700) │ └── {uuid}.json # one per wallet (600) ├── keys/ # (700, future use) ├── policies/ # (755, future use) └── logs/ └── audit.jsonl # append-only operation log (600) ``` Each wallet file holds: * `id` (UUID v4) and `name` * `key_type`: `"mnemonic"` or `"private_key"` * `accounts[]` with one entry per supported chain (CAIP-10, address, derivation path) * `crypto` with `aes-256-gcm` ciphertext, `scrypt` KDF params, IV, salt, and auth tag * `created_at` (ISO 8601) ## SDK methods Wallet lifecycle is exposed through the SDKs. ### JavaScript / TypeScript Available as both module-level imports and static methods on `PeaqosClient`. Pick whichever matches your style. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosClient, createWallet, importWallet, importWalletMnemonic, listWallets, getWallet, exportWallet, deleteWallet, } from "@peaqos/peaq-os-sdk"; // Create const wallet = await createWallet("my-machine", passphrase, 12); // or: await PeaqosClient.createWallet("my-machine", passphrase, 12); // Import await importWallet("my-machine", "0x...64hex", passphrase); // default chain: "evm". Pass a different OWS chain (e.g. "solana", "bitcoin", "cosmos") // when the private key is for that chain's curve and address format: // await importWallet("my-machine", "0x...64hex", passphrase, "solana"); await importWalletMnemonic("my-machine", "twelve words ...", passphrase); // Inspect const all = await listWallets(); const one = await getWallet("my-machine"); // Export / delete const phrase = await exportWallet("my-machine", passphrase); await deleteWallet("my-machine"); ``` Every function takes an optional final `WalletOptions` arg with `vaultPath` to point at a vault directory other than `~/.ows/`. Passphrase falls back to `OWS_PASSPHRASE` when omitted; if both are missing, the call throws `PeaqosError`. `importWallet` validates the hex shape eagerly and throws `ValidationError` for malformed keys; `importWalletMnemonic` accepts an optional `index` (default `0`) before the `WalletOptions` arg to derive a non-default account. `WalletInfo` carries `id`, `name`, `createdAt`, `keyType`, `peaqAddress` (convenience), and `accounts: readonly AccountInfo[]` with every chain. `AccountInfo` has `accountId` (CAIP-10), `address`, `chainId` (CAIP-2), `network` (human-readable name like `"peaq"`, `"base"`, `"solana"`), and `derivationPath`. Both shapes are deep-frozen. #### Building a client straight from a vault wallet `PeaqosClient.fromWallet` skips the manual private-key plumbing. It loads a wallet from the vault and returns a configured client whose `account` is the wallet's peaq address. ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { PeaqosClient } from "@peaqos/peaq-os-sdk"; // OWS-native signing (default): key is decrypted per-sign and wiped immediately. // The passphrase is verified on the FIRST signTransaction call, not at construction. const client = await PeaqosClient.fromWallet("my-machine", "s3cret", true, { rpcUrl: "https://peaq-rpc.example.com", contracts: { /* ... */ }, }); // Raw-key mode: decrypts the key once at construction (eager passphrase check) // and signs through viem locally. Use this when you need viem features OWS // doesn't expose (e.g. signMessage, signTypedData). const eager = await PeaqosClient.fromWallet("my-machine", "s3cret", false, { rpcUrl: "https://peaq-rpc.example.com", contracts: { /* ... */ }, }); ``` Signature: `fromWallet(nameOrId, passphrase, owsSigning, config, options?)`. `owsSigning` defaults to `true`. In OWS-native mode the SDK never holds the private key: `signMessage` and `signTypedData` throw because OWS only exposes `signTransaction`. Switch to raw-key mode if you need either. OWS signing surfaces typed errors via `OwsSigningErrorCode` (`WALLET_NOT_FOUND`, `INVALID_PASSPHRASE`, `INVALID_INPUT`, `POLICY_DENIED`, `CHAIN_NOT_SUPPORTED`). `INVALID_INPUT` becomes a `ValidationError`; the rest become `PeaqosError` with the original error preserved as `.cause`. ### Python Available under `peaq_os_sdk.wallet` and as `@staticmethod`s on `PeaqosClient`. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient from peaq_os_sdk.wallet import ( create_wallet, import_wallet, import_wallet_mnemonic, list_wallets, get_wallet, export_wallet, delete_wallet, ) # Create info = create_wallet("my-machine", passphrase=passphrase, words=12) # or: PeaqosClient.create_wallet("my-machine", passphrase=passphrase, words=12) # Import import_wallet("my-machine", "0x...64hex", passphrase=passphrase) # default chain="evm". Pass a different OWS chain (e.g. "solana", "bitcoin", "cosmos") # when the private key is for that chain's curve and address format: # import_wallet("my-machine", "0x...64hex", passphrase=passphrase, chain="solana") import_wallet_mnemonic("my-machine", "twelve words ...", passphrase=passphrase) # Inspect all_wallets = list_wallets() one = get_wallet("my-machine") # Export / delete phrase = export_wallet("my-machine", passphrase=passphrase) delete_wallet("my-machine") ``` Each function accepts a final `vault_path` keyword for a custom vault directory. Passphrase falls back to the `OWS_PASSPHRASE` env var when `None`; missing both raises `PeaqosError`. On the Python side, `WalletInfo` carries `id`, `name`, `created_at`, `key_type`, `peaq_address` (convenience), and `accounts: list[AccountInfo]` with every chain. `AccountInfo` has `account_id` (CAIP-10), `address`, `chain_id` (CAIP-2), `network` (human-readable name), and `derivation_path`. #### Building a client straight from a vault wallet `PeaqosClient.from_wallet` mirrors the JS factory above. It loads the wallet, verifies the passphrase, and returns a fully configured client. ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk import PeaqosClient client = PeaqosClient.from_wallet( "my-machine", passphrase="s3cret", rpc_url="https://peaq-rpc.example.com", identity_registry="0x...", identity_staking="0x...", event_registry="0x...", machine_nft="0x...", did_registry="0x0000000000000000000000000000000000000800", batch_precompile="0x0000000000000000000000000000000000000805", ) # Eager-decrypt: identical to PeaqosClient(private_key=...). eager = PeaqosClient.from_wallet( "my-machine", passphrase="s3cret", ows_signing=False, rpc_url="https://peaq-rpc.example.com", # ... ) ``` Signature: `from_wallet(name_or_id, passphrase=None, ows_signing=True, vault_path=None, **config_kwargs)`. `passphrase` falls back to `OWS_PASSPHRASE`. In OWS-native mode the SDK never holds the private key — `OWSAccount` decrypts it inside the OWS Rust FFI for each `sign_transaction` call and wipes it immediately. `ows_signing=False` exports and decrypts the key at construction time, behaving identically to a raw-key client. Each transaction's `chainId` drives both the CAIP-2 OWS arg and the EIP-155 `v`, so the same client transparently signs both peaq (`eip155:3338`) and Base (`eip155:8453`) bridge transactions. OWS signing surfaces typed errors via 5 OWS error codes (`WALLET_NOT_FOUND`, `INVALID_PASSPHRASE`, `INVALID_INPUT`, `POLICY_DENIED`, `CHAIN_NOT_SUPPORTED`). `INVALID_INPUT` raises `ValidationError`; the rest raise `PeaqosError`. See [SDK errors: OWS signing error codes](/peaqos/sdk-reference/errors#ows-signing-error-codes). ### Pulling the peaq address out of a wallet `peaq_address` / `peaqAddress` is already populated by every wallet method, so most callers never need anything else. If you're working with a raw `accounts` list (e.g. building a wallet response by hand in tests, or reading a vault file directly), use the helper: ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import { extractPeaqAddress } from "@peaqos/peaq-os-sdk"; const address = extractPeaqAddress(wallet.accounts); ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} from peaq_os_sdk.wallet import extract_peaq_address address = extract_peaq_address(wallet.accounts) ``` It returns the `eip155:3338` account if present, otherwise the first `eip155:*` account (EVM addresses match across EVM chains), and raises `PeaqosError` when no EVM account exists. ## From the CLI Every SDK wallet helper is also exposed through `peaqos wallet`. Install the optional extra to get them: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install 'peaq-os-cli[ows]' ``` Then: ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} peaqos wallet create my-wallet # mnemonic-backed, 12 words peaqos wallet import my-wallet --mnemonic # hidden prompt for the phrase peaqos wallet list peaqos wallet show my-wallet # full multi-chain address table peaqos wallet use my-wallet # writes PEAQOS_OWS_WALLET=my-wallet to .env peaqos wallet export my-wallet # reveals phrase / key after confirm peaqos wallet delete my-wallet # secure overwrite + unlink after confirm ``` `peaqos init` also offers `wallet` as a third `Private key source` choice and writes `PEAQOS_OWS_WALLET` instead of `PEAQOS_PRIVATE_KEY`. Once a wallet is active, `peaqos activate`, `peaqos qualify event`, and the rest of the command surface sign through it. See [peaqOS CLI: wallet](/peaqos/cli#peaqos-wallet) for the full command reference. ## Security model * **Vault encryption.** AES-256-GCM with scrypt KDF (`n=65536, r=8, p=1`). * **Passphrase handling.** Sourced from the explicit argument or the `OWS_PASSPHRASE` env var; if neither is set, the SDK raises `PeaqosError` rather than prompting. Never stored on disk. In CI, pass via secret manager or env injection. * **Mnemonic exposure.** Never returned by `createWallet` / `create_wallet`. The `WalletInfo` response only carries addresses and metadata. To recover the seed phrase you must call `exportWallet` / `export_wallet` with the vault passphrase. * **Single-mnemonic blast radius.** One phrase controls accounts on every supported chain. Treat exported phrases accordingly: anyone with the phrase has access to every chain account. * **Audit log.** Every wallet operation (create, import, export, delete) appends to `~/.ows/logs/audit.jsonl`. ## See also The full peaqOS CLI command reference. The `[ows]` extra and the raw-key flow side-by-side. Full upstream Open Wallet Standard v1.3 spec. # Quickstart Source: https://docs.peaq.xyz/quickstart Activate your first machine in five minutes. Install the SDK or the CLI, pick a deployment, activate a machine in one transaction. **New to web3?** Terms with a dotted underline have a plain-English definition on hover or tap. The full list lives in the [glossary](/glossary). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install peaq-os-cli peaqos init ``` `peaqos init` writes a `.env` with your key, RPC, and `TOKENOMICS_DEPLOYMENT_ID` (`peaq-mainnet` or `agung-2026-08-28`). Needs Python 3.12 or newer. Full reference on the [peaqOS CLI page](/peaqos/cli). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} npm install @peaqos/peaq-os-sdk viem dotenv ``` Economics 2.0 activation needs `@peaqos/peaq-os-sdk` 0.7.0 or newer (2026-09-11; older releases fail preflight with `PEER_MISMATCH` since the 2026-09-08 adapter re-point). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} python3 -m venv .peaq-os source .peaq-os/bin/activate pip install "peaq-os-sdk>=0.7.1" python-dotenv ``` Needs Python 3.10 or newer. 0.7.1 (2026-09-11) is the minimum: older releases fail preflight with `PEER_MISMATCH` since the 2026-09-08 adapter re-point. ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} pip install 'peaq-os-cli[ows]<0.0.8' 'peaq-os-sdk<0.6.0' npx skills add peaqnetwork/peaq-os-skills ``` The `peaqos` skill was last aligned with CLI 0.0.6 and still drives the Tokenomics 1.0 flags, so pin the CLI below 0.0.8 until the skill updates. Pin the SDK below 0.6.0 as well: the CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, which breaks `peaqos monetize`. This pair needs Python 3.11 or newer. Auto-detects Claude Code, Cursor, or Windsurf. See the [peaqOS AI page](/peaqos/peaqos-ai). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git cd peaq-robotics-ros2 source /opt/ros/jazzy/setup.bash colcon build --packages-select \ peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples source install/setup.bash ``` Wraps the SDKs as ROS 2 services. The ROS 2 services still drive the Tokenomics 1.0 register flow. Full reference on [SDK: ROS 2](/peaqos/sdk-reference/ros2/overview). Full install reference on the [install page](/peaqos/install). Create a `.env` file (or let `peaqos init` write it): ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} # peaq mainnet RPC. See /peaqos/install#public-rpc-endpoints for alternatives. PEAQOS_NETWORK=mainnet PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public PEAQOS_PRIVATE_KEY=0x... # Economics 2.0 deployment. Selects the contract set; the addresses ship inside the SDK. # Use agung-2026-08-28 for the testnet. TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet # Required for the `peaqos activate` funding step; omit and use --skip-funding if you have your own gas. PEAQOS_GAS_STATION_URL=https://depinstation.peaq.xyz # Tokenomics 1.0 contracts. The SDK constructor still requires all six; activation does not read them. # For agung testnet addresses see /peaqos/install#agung-testnet-contracts. IDENTITY_REGISTRY_ADDRESS=0xb53Af985765031936311273599389b5B68aC9956 IDENTITY_STAKING_ADDRESS=0x11c05A650704136786253e8685f56879A202b1C7 EVENT_REGISTRY_ADDRESS=0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e # Economics 2.0 EventRegistry; Tokenomics 1.0 machines use 0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA MACHINE_NFT_ADDRESS=0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9 DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800 BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805 # Optional. Tokenomics 1.0 MCR API, read by `peaqos qualify` and `peaqos show`. PEAQOS_MCR_API_URL=https://mcr.peaq.xyz ``` The wallet behind `PEAQOS_PRIVATE_KEY` becomes the machine's owner and pays the bond plus gas. The bond is quoted per tier at the oracle rate; on 2026-09-04 at 20:20 UTC the Entry tier quoted 0.803 PEAQ on mainnet; the figure moves with the daily oracle price. See [Economics 2.0](/peaqos/concepts/economics-2-0#tiers). Full variable list on the [environment variables table](/peaqos/install#environment-variables). ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}} cat > did.json <<'JSON' { "verificationMethods": [ { "id": "#key-1", "methodType": "Ed25519VerificationKey2020", "controller": "0xYourAddress", "publicKeyMultibase": "z6Mk..." } ], "authentication": [0], "serviceEndpoints": [ { "id": "#docs", "serviceType": "Documentation", "serviceEndpoint": "https://example.com/docs" } ] } JSON # Preview the bond and the net PEAQ. Submits nothing. peaqos activate --machine-type Sensor --credential-subject-hex 0xdeadbeef \ --manufacturer 0x3333333333333333333333333333333333333333 --tier entry \ --did-document ./did.json --dry-run # Same command without --dry-run submits the one transaction. ``` ```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import "dotenv/config"; import { PeaqosClient } from "@peaqos/peaq-os-sdk"; const legacy = PeaqosClient.fromEnv(); const client = new PeaqosClient<"tokenomics20">({ rpcUrl: legacy.rpcUrl, privateKey: process.env.PEAQOS_PRIVATE_KEY!, contracts: legacy.contracts, tokenomics20: { deploymentId: "peaq-mainnet" }, }); const params = { controller: client.address, verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: client.address, publicKeyMultibase: "z6Mk..." }], authentication: [0n], serviceEndpoints: [{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" }], machineType: "Sensor", credentialSubject: "0xdeadbeef", manufacturer: "0x3333333333333333333333333333333333333333", tier: 0, }; const preview = await client.previewMachineActivation(params); console.log("Will pay", preview.netPeaqAmount, "wei of PEAQ for machine", preview.machineId); const result = await client.activateMachine(params); console.log("Activated machine ID:", result.machineId.toString()); ``` ```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}} import os from dotenv import load_dotenv from peaq_os_sdk import ( ActivateMachineParams, PeaqosClient, ServiceEndpointInput, Tokenomics20Config, VerificationMethodInput, ) load_dotenv() legacy = PeaqosClient.from_env() client = PeaqosClient( rpc_url=legacy.rpc_url, private_key=os.environ["PEAQOS_PRIVATE_KEY"], identity_registry=legacy.contracts.identity_registry, identity_staking=legacy.contracts.identity_staking, event_registry=legacy.contracts.event_registry, machine_nft=legacy.contracts.machine_nft, did_registry=legacy.contracts.did_registry, batch_precompile=legacy.contracts.batch_precompile, tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), ) params = ActivateMachineParams( controller=client.address, verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=client.address, public_key_multibase="z6Mk..."),), authentication=(0,), service_endpoints=(ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"),), machine_type="Sensor", credential_subject=bytes.fromhex("deadbeef"), manufacturer="0x3333333333333333333333333333333333333333", tier=0, ) preview = client.preview_machine_activation(params) print("Will pay", preview.net_peaq_amount, "wei of PEAQ for machine", preview.machine_id) result = client.activate_machine(params) print("Activated machine ID:", result.machine_id) ``` Fleets activate each machine with the machine's own key and record the operator as DID controller. See the [self-managed](/peaqos/guides/self-managed-onboarding) and [fleet](/peaqos/guides/proxy-operator-fleet) guides. * The client resolved the Economics 2.0 contract addresses from the `peaq-mainnet` deployment record and verified them against `InfoDesk.peer(role)` on chain. * `activateMachine` sent one transaction that minted the machine's ERC-721 in `MachineRegistry`, stored the DID document, bonded PEAQ on the Entry tier in `MachineSubscription`, and recorded peaq as the home chain. * The machine ID is `keccak256(machineType, credentialSubject)` as a `uint256`. It is also the NFT token ID and the DID is `did:peaq:`. It is the handle for every later call: `peaqos machine status`, renewals, transfers, DID updates. * The SDK only returned after the receipt carried `MachineOnboarded`, `MachineMinted`, and `Activated` and the on-chain state matched what you asked for. * The machine shows up on the [Machine Explorer](https://machines.peaq.xyz) at `machines.peaq.xyz/machine/` once the indexer has picked up the block. Machines registered through `registerMachine` / `register_machine` before 2026-09-01 live on the Tokenomics 1.0 contracts. The SDKs keep that path when you construct a client without `tokenomics20`; the 1.0 MCR API at `mcr.peaq.xyz` serves them. See [Activate: legacy flow](/peaqos/functions/activate#legacy-flow-tokenomics-1-0). ## Next Owner = operator. One machine, one wallet, funded through the Gas Station. Many machines, each owning its NFT, one operator as controller. Pair an AI agent to your machine and let it discover and consume services through the Machine Markets API. # Roadmap Source: https://docs.peaq.xyz/roadmap What's live, what's next, and where peaqOS is headed. peaqOS ships in stages. Each function adds a new capability to the Machine Economy. **Live.** Put your machine on-chain: peaqID, Machine NFT, and bond. Since 2026-09-04 (SDK 0.6.0, CLI 0.0.8) activation is one transaction against the [Economics 2.0](/peaqos/concepts/economics-2-0) contracts: the machine picks a tier, bonds PEAQ quoted at the oracle rate, and its DID document goes on chain in the same call. **Live.** Machine Credit Rating: a score built from revenue and activity history. Third parties can query any machine's creditworthiness via API. **Live.** Pair an AI agent to an activated machine, set a delegation policy (per-transaction and daily spend limits, allowed/denied skills and services), run machine-aware market search across a curated partner catalogue, and drive the full purchase loop — order, payment, escrow, execution, confirmation, and dispute — through the orchestrator. See [Scale](/peaqos/functions/scale) and the [Machine Markets API](/peaqos/api-reference/machine-markets-overview). **Live.** Sell the data your machine generates — signed, encrypted, and verifiable, with buyers paying for access to exactly what they need. See [Stream](/peaqos/functions/stream). **Live.** An early release that puts a registered, bonded machine to work as a compute provider: a signed, toggleable monetization opt-in, schema-driven provider provisioning that runs on the machine, and a presence heartbeat that reports whether it is online. Earnings route to the machine's own wallet. Compute is the first capacity type, with Akash as the first aggregator network; aggregator discovery, connections, and automated settlement roll out next. See [Monetize](/peaqos/functions/monetize). **Coming Soon.** Prove your machine is real via hardware attestation and trusted third parties. **Coming Soon.** Fractionalize your machine into an investable asset via ERC-3643. This roadmap shows the planned sequence. Functions ship when they're ready, so timelines may shift. ## Beyond the seven functions **Live on peaq mainnet since 2026-09-01.** Thirteen contracts price machine activation in USD per tier, bond PEAQ per machine, and settle through trust validators. The Python SDK, JS SDK, and CLI support activation, renewals, USDT settlement, and machine management since 2026-09-04. The 2.0 MCR server (`mcr-20.peaq.xyz`) serves ratings and monetization for 2.0 machines since 2026-09-05. The block reward split change (runtime spec 113, treasury 70 / validators and delegators 30) was enacted on 2026-09-06. Rolling out next: event submission for 2.0 machines, the SDK query helpers in Tokenomics mode, trust validator staking in the SDKs, and cross-chain machine relocation (disabled on chain today). See [Economics 2.0](/peaqos/concepts/economics-2-0). **Live pieces.** The peaq ↔ Solana Machine NFT bridge (LayerZero V2, Metaplex Core twins) runs on mainnet since 2026-08-21, the peaqID identity and staking mirrors push peaq state to Solana, and the MCR API resolves Solana addresses to peaq machines through the on-chain binding registry. **Next:** SDK and CLI surfaces for the Solana lane, onboarding machines that hold only a Solana wallet, and Economics 2.0 on Solana; the scope of each is still being defined. See [Omni-chain: Solana](/peaqos/concepts/omni-chain#solana). Mnemonic-backed encrypted vault and multi-chain account derivation. SDK wallet lifecycle (`createWallet`, `importWallet`, etc.) and OWS-native signing through `PeaqosClient.fromWallet` are live in both SDKs. CLI wallet commands (`peaqos wallet list | show | use | create | export | delete`) are live too. Optional and additive: the raw-key flow keeps working. See [Wallets (OWS)](/peaqos/wallets).