` 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.
## 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) |
## 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 four on-chain layers. The core registry and staking contracts live on peaq chain as UUPS upgradeable proxies; bridging is LayerZero V2 ONFT; smart accounts follow ERC-4337; and low-level DID, batch, and WPEAQ operations are peaq chain precompiles.
## Architecture
```
Core (peaq chain, UUPS proxies)
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)
```
* 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
Set these in your environment. The SDK reads them from `fromEnv()` / `from_env()`.
### Core
| Contract | Variable | Address |
| :--------------- | :-------------------------- | :------------------------------------------- |
| IdentityRegistry | `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` |
| IdentityStaking | `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` |
| EventRegistry | `EVENT_REGISTRY_ADDRESS` | `0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9` |
| 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` |
## Agung testnet addresses
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` |
| MachineNFTAdapter | `MACHINE_NFT_ADAPTER_ADDRESS` | `0x63fD7e64A38e50D1486Bc569B4CaCeD38528De22` | `bridgeNft` from peaq |
| AdminFlags | `ADMIN_FLAGS_ADDRESS` | `0x4181a2Aa34aFb247450FfcBd65be5aBD4Cbee658` | MCR API server (negative-flag reads + admin trust overrides) |
**Bridging cannot be exercised on agung.** `MachineNFTAdapter` is deployed, but LayerZero V2 has no DVN routes between agung and Base. `bridgeNft` / `bridge_nft` calls will not relay end-to-end. Exercise the bridge on peaq mainnet ↔ Base mainnet only.
## Core contracts
### IdentityRegistry
Central machine identity registry. 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
### 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.
**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` | Wrapped PEAQ used internally by IdentityStaking as the staking token |
## 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` |
The SDK exports the EIDs as `LAYER_ZERO_EIDS` (JS) / `LAYERZERO_EIDS` (Python) for reference.
## Upgrade pattern
All 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.
# 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.network"
);
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.network",
)
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.network",
"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.network",
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.network"
);
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.network",
)
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 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 a LayerZero V2 ONFT, portable across chains by design. It anchors a machine's identity and links to its credit history.
## What the Machine NFT represents
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.** Additional peaqOS chains are added as peer contracts deploy. Bridging is mainnet-only: LayerZero V2 has no DVN routes between agung and Base, 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** ONFT pattern. Only one live instance exists across the network at any time.
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. Bridging in mints; bridging back to peaq burns.
More bridge destinations are added as peer contracts deploy.
peaq is the home chain. peaq → Base locks on peaq + mints on Base; Base → peaq burns on Base + unlocks on peaq.
Key properties:
* **Home chain**: peaq. Minting happens on peaq after machine onboarding.
* **Cross-chain**: LayerZero V2 ONFT architecture bridges peaq ↔ Base via the `MachineNFTAdapter` lock/unlock pattern on peaq paired with burn/mint on the destination. Further peaqOS chains are added as peer contracts deploy.
* **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
# 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
Every peaqID follows the format:
```
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.
## 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 (secure enclave, TPM, or equivalent) that binds the event to a specific physical device.
Choose the highest level you can honestly attest to.
## 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. peaqID, wallet, and Machine NFT.
Activate is the entry point to peaqOS. Register a machine on peaq chain and give it a [peaqID](/peaqos/concepts/peaqid), a keypair, and a [Machine NFT](/peaqos/concepts/machine-nft): the primitives every later function builds on.
## What ships
| Component | Description |
| :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| peaqID | W3C DID anchoring the machine's on-chain identity. Portable across chains. |
| Identity NFT | Minted automatically by IdentityRegistry at registration. `tokenId == machineId`. |
| Machine NFT | LayerZero V2 ONFT linking to the machine's financial profile. Minted via a separate `mintNft` call after registration. See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics). |
| 1 PEAQ bond | Sent as `msg.value` on the `register()` call (the contract's current `minBond`). Required to graduate onto the MCR rated scale later. |
| Machine wallet | Per-machine EVM keypair, generated by the SDK. |
## How activation works
The SDK exposes each step as a discrete call so you can compose your own flow. For one-shot activation from the terminal, the CLI runs all six steps idempotently. Re-runs skip already-completed steps using `./peaqos.log` as the resume marker:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# self-managed: caller's key drives every step
peaqos activate --doc-url "https://example.com/docs" --data-api "https://example.com/events"
# proxy-managed: --for and --machine-key must be used together
peaqos activate --for 0xMachineAddress --machine-key ./machine.key --doc-url "https://example.com/docs" --data-api "https://example.com/events"
```
See [CLI reference: activate](/peaqos/cli#peaqos-activate) for all flags (including `--skip-funding`, `--doc-url`, `--data-api`, `--visibility`).
The SDK generates a new keypair for the machine. This becomes the machine's signing identity on peaq chain.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
const keypair = PeaqosClient.generateKeypair();
// { address: "0x...", privateKey: "0x..." }
```
Fund the machine's wallet with PEAQ for gas and the 1 PEAQ bond.
Projects with partner access to the peaq Gas Station can request initial gas from the faucet using the SDK (2FA-gated). Otherwise, fund the wallet directly from any PEAQ-holding address.
Two patterns depending on your deployment model.
Owner equals operator. One machine, one wallet. Use `registerMachine` to register the machine under its own keypair.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machineClient = new PeaqosClient({
rpcUrl: client.rpcUrl,
privateKey: keypair.privateKey,
contracts: client.contracts,
});
const machineId = await machineClient.registerMachine();
console.log(machineId);
// Returns the on-chain machine ID (number)
```
Full guide: [Self-managed onboarding](/peaqos/guides/self-managed-onboarding).
One owner runs a fleet. Register machines on their behalf using `registerFor`. Each machine gets its own peaqID; the proxy operator controls delegation and mints the Machine NFT in a follow-up step.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machineId = await client.registerFor(keypair.address);
console.log(machineId);
// Create Machine Client after registration to mint NFT and link DID
const machineClient = new PeaqosClient({
rpcUrl: client.rpcUrl,
privateKey: keypair.privateKey,
contracts: client.contracts,
});
```
Full guide: [Proxy operator fleet setup](/peaqos/guides/proxy-operator-fleet).
NFT and the DID attributes are written by the machine itself.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const tx = await machineClient.mintNft(machineId, keypair.address);
const nftTokenId = await machineClient.tokenIdOf(machineId);
await machineClient.writeMachineDIDAttributes({
machineId,
nftTokenId,
operatorDid: "",
documentationUrl: "https://example.com/docs",
dataApi: "https://example.com/events",
dataVisibility: "public",
});
```
NFT is minted to the machine's address, and the DID attributes are written by the machine itself. Proxy pays bond fee, and machine pays DID fees.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const tx = await client.mintNft(machineId, keypair.address);
const nftTokenId = await client.tokenIdOf(machineId);
await machineClient.writeMachineDIDAttributes({
machineId,
nftTokenId,
operatorDid: "",
documentationUrl: "https://example.com/docs",
dataApi: "https://example.com/events",
dataVisibility: "public",
});
```
Full guide: [Proxy operator fleet setup](/peaqos/guides/proxy-operator-fleet).
`mintNft` returns a new, independent `tokenId` on the MachineNFT contract. `writeMachineDIDAttributes` binds `machineId` and `nftTokenId` to the machine's DID. See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics) for the full rationale.
After all three calls the machine has a peaqID, an Identity NFT, a Machine NFT, and a 1 PEAQ bond. Query the machine's profile against the [MCR API](/peaqos/api-reference/get-machine), where `{did}` is `did:peaq:`:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl "${PEAQOS_MCR_API_URL}/machine/did:peaq:0xYourMachineAddress"
```
The machine is `Provisioned` at first: on-chain, but not yet rated. It graduates onto the rated scale once **both** gates clear: a sustained first-to-last event span, **and** either enough qualifying revenue events or enough activity events. The events condition is `OR`, so activity-only machines can graduate without revenue. See [Machine Credit Rating](/peaqos/concepts/machine-credit-rating#lifecycle-provisioned-graduation).
## After activation
Once a machine is registered, later functions add new capabilities.
| Next step | Function | Status |
| :----------------------------------------------------- | :------------------------------------- | :---------- |
| Build a credit rating from revenue and activity events | [Qualify](/peaqos/functions/qualify) | Live |
| Get attested by the peaq Foundation or an OEM | [Verify](/peaqos/functions/verify) | Coming Soon |
| List services in the Service Registry | [Monetize](/peaqos/functions/monetize) | Coming Soon |
| Pair an AI agent with delegated spending | [Scale](/peaqos/functions/scale) | Coming Soon |
| Fractionalize the machine for investor ownership | [Tokenize](/peaqos/functions/tokenize) | Coming Soon |
## Concepts
W3C DID, portable identity, cross-chain resolution.
LayerZero V2 ONFT whose `tokenURI` resolves to the machine's Machine Card (ERC-8004 JSON schema).
Revenue events (type 0) and activity events (type 1).
Moody's-style rating from a machine's history.
2FA-gated faucet for initial machine gas.
## SDK reference
* [`registerMachine`](/peaqos/sdk-reference/sdk-js#registermachine): Register a self-managed machine.
* [`registerFor`](/peaqos/sdk-reference/sdk-js#registerfor): Register a machine as a proxy operator.
* [`setupFaucet2FA`](/peaqos/sdk-reference/sdk-js#setupfaucet2fa): Initialize 2FA for Gas Station access.
* [`confirmFaucet2FA`](/peaqos/sdk-reference/sdk-js#confirmfaucet2fa): Confirm 2FA setup with TOTP code.
* [`fundFromGasStation`](/peaqos/sdk-reference/sdk-js#fundfromgasstation): Request gas funding for a machine address.
* [`generateKeypair`](/peaqos/sdk-reference/sdk-js#generatekeypair): Create a new machine keypair.
## Guides
Single machine, owner-operated.
Register and manage N machines from one identity.
# Monetize
Source: https://docs.peaq.xyz/peaqos/functions/monetize
List your machine's services. Other machines and agents discover and buy them.
**Monetize is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap).
Monetize gives machines a place to list the services they offer (data access, compute, telemetry, physical work) so other machines, agents, and applications can discover and buy them.
# 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).
## 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
Pair an AI agent that transacts and consumes services on your machine's behalf.
**Scale is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap).
Scale pairs AI agents with machines, giving an agent delegated authority to transact and consume services on a machine's behalf, and extends fleet operations across chains.
It builds on the smart account each machine receives at [Activate](/peaqos/functions/activate) and the service listings exposed by [Monetize](/peaqos/functions/monetize), letting an agent buy, sell, and coordinate across a fleet without a human in the loop.
# 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.
At launch, [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 is what closes that gap.
# Proxy operator fleet
Source: https://docs.peaq.xyz/peaqos/guides/proxy-operator-fleet
Register and manage multiple machines from one operator wallet using registerFor. Covers per-machine keys, batch registration, and fleet queries.
One owner, many machines. The proxy operator pattern lets a single wallet register and control a fleet of machines. Each machine gets its own peaqID on registration; the Machine NFT is minted in a follow-up `mintNft` call. The proxy operator holds the on-chain ownership of every identity.
## When to use proxy registration
Use `registerFor` when:
* You operate physical hardware that cannot hold its own keys (offline sensors, embedded devices)
* You manage a fleet and want centralized identity control
* You need batch onboarding of many machines from a single script. The IdentityRegistry contract puts no cap on machines per operator, but the operator's `machines` DID attribute is capped at 100 entries (newer registrations push older ones out of the index), and the [MCR API operator endpoint](/peaqos/api-reference/get-operator-machines) paginates with `limit ≤ 20` per request — iterate via `offset` against `pagination.total` to walk the full set, or enumerate against the contract directly for fleets that need more than the indexed 100.
Use [self-managed onboarding](/peaqos/guides/self-managed-onboarding) when the machine holds its own key and registers itself.
## Prerequisites
* Node.js ≥ 22 or Python 3.10+
* A funded proxy operator wallet with at least (N \* 1.1) PEAQ (1 PEAQ bond + gas per machine)
* The proxy operator must be self-registered first: `registerMachine` from the proxy's own key, exactly as in [self-managed onboarding](/peaqos/guides/self-managed-onboarding). The proxy's `machineId` is required to publish the fleet under its DID.
* Environment variables configured per the [install guide](/peaqos/install)
* 2FA set up and confirmed on the operator wallet (see [self-managed onboarding](/peaqos/guides/self-managed-onboarding) steps 3 and 4)
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`.
## Single machine registration
The proxy operator's private key signs all transactions on behalf of the fleet.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const proxy = PeaqosClient.fromEnv();
console.log("Proxy operator:", proxy.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() # load envs from .env file
proxy = PeaqosClient.from_env()
print("Proxy operator:", proxy.address)
```
Each machine needs its own address. Generate one per device.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machine = PeaqosClient.generateKeypair();
console.log("Machine address:", machine.address);
// Store machine.privateKey securely if the device needs to sign later.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
machine_address, machine_key = PeaqosClient.generate_keypair()
print("Machine address:", machine_address)
# Store machine_key securely if the device needs to sign later.
```
`registerFor` registers the machine address on the IdentityRegistry. The proxy's signing address (`msg.sender`) becomes the on-chain owner. The call sends 1 PEAQ as the bond.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machineId = await proxy.registerFor(machine.address);
console.log("Registered machine", machine.address, "with machine ID", machineId);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
machine_id = proxy.register_for(machine_address)
print(f"Registered machine {machine_address} with machine ID {machine_id}")
```
Registration only mints the Identity NFT. The proxy mints the Machine NFT to the machine's address.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
await proxy.mintNft(machineId, machine.address);
const nftTokenId = await proxy.tokenIdOf(machineId);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
proxy.mint_nft(machine_id, machine_address)
nft_token_id = proxy.token_id_of(machine_id)
```
The machine signs its own DID writes, so its wallet needs a small amount of PEAQ for gas. Use the [Gas Station](/peaqos/concepts/gas-station) from the proxy (2FA-gated), or top it up directly from any PEAQ-holding address.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const FAUCET_URL = "https://depinstation.peaq.network";
await proxy.fundFromGasStation(
{
ownerAddress: proxy.address,
targetWalletAddress: machine.address,
chainId: "peaq",
twoFactorCode: "654321", // Fresh TOTP code from the proxy operator's authenticator
// requestId is recommended for idempotent reruns
},
FAUCET_URL,
);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
FAUCET_URL = "https://depinstation.peaq.network"
proxy.fund_from_gas_station(
owner_address=proxy.address,
target_wallet_address=machine_address,
chain_id="peaq",
two_factor_code="654321", # Fresh TOTP code from the proxy operator's authenticator
faucet_base_url=FAUCET_URL,
)
```
`writeMachineDIDAttributes` always writes to the **caller's** DID. The proxy can't write to the machine's DID for it. Spin up a separate client backed by the machine's key and have the machine sign its own attribute writes. Skip this step and the machine is unreachable through the MCR API: `GET /machine/{did}` and `GET /mcr/{did}` will 404 because no `machineId` attribute is bound to the machine's address.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
// Build a per-machine client by reusing the proxy's RPC + contracts
// but signing with the machine's own key.
const machineClient = new PeaqosClient({
rpcUrl: proxy.rpcUrl,
privateKey: machine.privateKey,
contracts: proxy.contracts,
});
await machineClient.writeMachineDIDAttributes({
machineId,
nftTokenId,
operatorDid: `did:peaq:${proxy.address}`,
documentationUrl: "https://example.com/docs",
dataApi: "https://example.com/events",
dataVisibility: "public",
});
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# Construct a per-machine client directly: same RPC and contracts
# as the proxy, but signed with the machine's own key. No env mutation.
machine_client = PeaqosClient(
rpc_url=proxy.rpc_url,
private_key=machine_key,
identity_registry=proxy.contracts.identity_registry,
identity_staking=proxy.contracts.identity_staking,
event_registry=proxy.contracts.event_registry,
machine_nft=proxy.contracts.machine_nft,
did_registry=proxy.contracts.did_registry,
batch_precompile=proxy.contracts.batch_precompile,
)
machine_client.write_machine_did_attributes(
machine_id=machine_id,
nft_token_id=nft_token_id,
operator_did=f"did:peaq:{proxy.address}",
documentation_url="https://example.com/docs",
data_api="https://example.com/events",
data_visibility="public",
)
```
`writeProxyDIDAttributes` writes the proxy's `machineId` and the list of machine IDs it controls onto the proxy's DID. This is what the [`GET /operator/{did}/machines`](/peaqos/api-reference/get-operator-machines) endpoint reads from.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
// proxyMachineId is the proxy operator's own machineId from its
// self-registration (registerMachine). Persist it once and reuse.
const proxyMachineId = /* number, e.g. 7 */ 7;
await proxy.writeProxyDIDAttributes({
proxyMachineId,
machineIds: [machineId], // grow this list as you add machines
});
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# proxy_machine_id is the proxy operator's own machineId from its
# self-registration (register_machine). Persist it once and reuse.
proxy_machine_id = 7 # replace with the proxy's actual machine_id
proxy.write_proxy_did_attributes(
proxy_machine_id=proxy_machine_id,
machine_ids=[machine_id],
)
```
See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics) for the full rationale.
## Batch registration (10 machines)
Register multiple machines sequentially. Each iteration runs the full activate path (register, mint NFT, fund the machine, machine writes its own DID); then the proxy publishes the full fleet on its DID at the end.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const proxy = PeaqosClient.fromEnv();
const proxyMachineId = 7; // the proxy's own machineId from its self-registration
const FLEET_SIZE = 10;
const FAUCET_URL = "https://depinstation.peaq.network";
const fleet = [];
for (let i = 0; i < FLEET_SIZE; i++) {
const machine = PeaqosClient.generateKeypair();
try {
const machineId = await proxy.registerFor(machine.address);
await proxy.mintNft(machineId, machine.address);
const nftTokenId = await proxy.tokenIdOf(machineId);
await proxy.fundFromGasStation(
{
ownerAddress: proxy.address,
targetWalletAddress: machine.address,
chainId: "peaq",
twoFactorCode: "654321", // Fresh TOTP code per call
},
FAUCET_URL,
);
const machineClient = new PeaqosClient({
rpcUrl: proxy.rpcUrl,
privateKey: machine.privateKey,
contracts: proxy.contracts,
});
await machineClient.writeMachineDIDAttributes({
machineId,
nftTokenId,
operatorDid: `did:peaq:${proxy.address}`,
documentationUrl: "https://example.com/docs",
dataApi: "https://example.com/events",
dataVisibility: "public",
});
fleet.push({
address: machine.address,
privateKey: machine.privateKey,
machineId,
});
console.log(`[${i + 1}/${FLEET_SIZE}] Activated ${machine.address} (machineId ${machineId})`);
} catch (err) {
console.error(`[${i + 1}/${FLEET_SIZE}] Failed for ${machine.address}:`, err);
// Decide: skip this machine and continue, or abort the batch.
}
}
// Publish the proxy's full fleet in one DID write.
await proxy.writeProxyDIDAttributes({
proxyMachineId,
machineIds: fleet.map((m) => m.machineId),
});
console.log("Fleet:", fleet.length, "machines activated");
// Persist fleet to a JSON file or database for later use.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import PeaqosClient
proxy = PeaqosClient.from_env()
proxy_machine_id = 7 # the proxy's own machineId from its self-registration
FLEET_SIZE = 10
FAUCET_URL = "https://depinstation.peaq.network"
fleet = []
for i in range(FLEET_SIZE):
machine_address, machine_key = PeaqosClient.generate_keypair()
try:
machine_id = proxy.register_for(machine_address)
proxy.mint_nft(machine_id, machine_address)
nft_token_id = proxy.token_id_of(machine_id)
proxy.fund_from_gas_station(
owner_address=proxy.address,
target_wallet_address=machine_address,
chain_id="peaq",
two_factor_code="654321", # Fresh TOTP code per call
faucet_base_url=FAUCET_URL,
)
# Build a per-machine client directly: same RPC and contracts
# as the proxy, but signed with the machine's own key.
machine_client = PeaqosClient(
rpc_url=proxy.rpc_url,
private_key=machine_key,
identity_registry=proxy.contracts.identity_registry,
identity_staking=proxy.contracts.identity_staking,
event_registry=proxy.contracts.event_registry,
machine_nft=proxy.contracts.machine_nft,
did_registry=proxy.contracts.did_registry,
batch_precompile=proxy.contracts.batch_precompile,
)
machine_client.write_machine_did_attributes(
machine_id=machine_id,
nft_token_id=nft_token_id,
operator_did=f"did:peaq:{proxy.address}",
documentation_url="https://example.com/docs",
data_api="https://example.com/events",
data_visibility="public",
)
fleet.append({
"address": machine_address,
"private_key": machine_key,
"machine_id": machine_id,
})
print(f"[{i + 1}/{FLEET_SIZE}] Activated {machine_address} (machine_id {machine_id})")
except Exception as err:
print(f"[{i + 1}/{FLEET_SIZE}] Failed for {machine_address}: {err}")
# Decide: skip this machine and continue, or abort the batch.
# Publish the proxy's full fleet in one DID write.
proxy.write_proxy_did_attributes(
proxy_machine_id=proxy_machine_id,
machine_ids=[m["machine_id"] for m in fleet],
)
print(f"Fleet: {len(fleet)} machines activated")
# Persist fleet to a JSON file or database for later use.
```
The CLI does this whole flow in one command: `peaqos activate --for 0xMachineAddress --machine-key ./machine.key`. See [CLI reference](/peaqos/cli#peaqos-activate).
## Per-machine key vs shared key
Two approaches for managing machine keys in a proxy fleet:
| Approach | How it works | Trade-offs |
| :--------------- | :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ |
| Per-machine key | Generate a unique keypair per device. Store each key on-device or in a secrets vault. | Strongest isolation. If one key leaks, only one machine is compromised. Requires per-device key distribution. |
| Shared proxy key | All machines share the proxy operator's key. Machines never sign their own transactions. | Simpler operationally. Single point of failure: if the proxy key leaks, the entire fleet is exposed. |
**Recommendation:** Use per-machine keys when the device can store secrets (HSM, TEE, encrypted filesystem). Use a shared proxy key for offline or passive hardware (sensors, meters) that never needs to sign.
**On the roadmap.** Proxy key management for offline devices is evolving. The current SDK supports both patterns above. Watch the [roadmap](/roadmap) for additional device-key options as they land.
## Fleet queries
After registration, query the MCR API to list all machines under a proxy operator.
```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl "${PEAQOS_MCR_API_URL}/operator/did:peaq:0xProxyAddress/machines?offset=0&limit=20"
```
Response shape:
```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
"operator_did": "did:peaq:0xProxyAddress",
"machines": [
{ "did": "did:peaq:0xMachine1", "machine_id": 101, "mcr_score": 62, "mcr": "BB", "negative_flag": false },
{ "did": "did:peaq:0xMachine2", "machine_id": 102, "mcr_score": 45, "mcr": "B", "negative_flag": false }
],
"pagination": { "offset": 0, "limit": 20, "total": 2 }
}
```
See [GET /operator/\{did}/machines](/peaqos/api-reference/get-operator-machines) for full details.
## Error handling
| Error | Cause | Resolution |
| :----------------------------------------------------------------------------- | :----------------------------------------------------- | :--------------------------------------------------------------------------------- |
| `ValidationError: machineAddress must be a valid 0x-prefixed Ethereum address` | Malformed address string | Check the address format (0x prefix, 40 hex characters) |
| `RuntimeError: AlreadyRegistered` / `RpcError: already registered` | The machine address is already on the IdentityRegistry | Skip this address, or query the MCR API to find the existing machine ID |
| `RuntimeError: InvalidMachineAddress` / `RpcError: zero address` | Attempted to register the zero address | Pass a valid machine address |
| Insufficient balance | Proxy wallet does not hold enough PEAQ for bond + gas | Top up the proxy wallet. For a batch of N machines, ensure at least N \* 1.1 PEAQ. |
# 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...`). 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 (`client.queryMcr` / `client.query_mcr`), 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.network` 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.network"
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
Register a single machine where the owner is the operator. Generate keys, set up 2FA, fund via Gas Station, and call registerMachine.
Owner equals operator. One keypair, one machine, one identity. This guide covers the full flow from environment setup through on-chain registration.
## Prerequisites
* Node.js ≥ 22 or Python 3.10+
* A funded EVM wallet (the owner address) with at least 1.1 PEAQ (1 PEAQ bond + gas)
* Environment variables configured per the [install guide](/peaqos/install)
## Environment setup
Set these environment variables before running any SDK call. Both SDKs read the same variable names.
```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>
# Mainnet contracts. fromEnv() requires every variable below to be set.
# For agung testnet addresses see /peaqos/install#agung-testnet-contracts.
IDENTITY_REGISTRY_ADDRESS=0xb53Af985765031936311273599389b5B68aC9956
IDENTITY_STAKING_ADDRESS=0x11c05A650704136786253e8685f56879A202b1C7
EVENT_REGISTRY_ADDRESS=0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9
MACHINE_NFT_ADDRESS=0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9
DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800
BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805
```
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`.
## Full flow
`fromEnv` reads all required variables and returns a configured client. Throws `ValidationError` if any variable 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 client = PeaqosClient.fromEnv();
console.log("Signer:", client.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() # load envs from .env file
client = PeaqosClient.from_env()
print("Signer:", client.address)
```
If the machine does not already have a wallet, generate one. This is a local operation with no chain interaction.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const keypair = PeaqosClient.generateKeypair();
console.log("Address:", keypair.address);
console.log("Key:", keypair.privateKey);
// Store the private key 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)
print("Key:", private_key)
# Store the private key securely. It cannot be recovered.
```
If the machine already has a funded wallet, skip to step 5 and construct the client with that key directly. Steps 3-5 will fund the generated keypair.
The Gas Station requires 2FA before it funds any address. Call `setupFaucet2FA` to start enrollment.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const FAUCET_URL = "https://depinstation.peaq.network";
const enrollment = await client.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.network"
enrollment = client.setup_faucet_2fa(
owner_address=keypair.address,
faucet_base_url=FAUCET_URL,
)
print("OTPAuth URI:", enrollment["otpauth_uri"])
print("QR image:", enrollment["qr_image_url"])
# Scan the QR or paste the URI into your authenticator app.
# The QR URL expires after roughly 2 minutes.
```
After adding the secret to your authenticator app, submit the current TOTP code.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
await client.confirmFaucet2FA(
keypair.address,
FAUCET_URL,
"123456", // Replace with your actual TOTP code
);
// No return value. Success means 2FA is active.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.confirm_faucet_2fa(
owner_address=keypair.address,
faucet_base_url=FAUCET_URL,
two_factor_code="123456", # Replace with your actual TOTP code
)
# No return value. Success means 2FA is active.
```
Request initial gas from the Gas Station. The faucet either funds the wallet or skips if the balance is already sufficient.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const result = await client.fundFromGasStation(
{
ownerAddress: keypair.address,
targetWalletAddress: keypair.address,
chainId: "peaq",
twoFactorCode: "654321", // Fresh TOTP code
},
FAUCET_URL,
);
if (result.status === "success") {
console.log("Funded:", result.txHash, result.fundedAmount);
} else {
console.log("Skipped, current balance:", result.currentBalance);
}
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
result = client.fund_from_gas_station(
owner_address=address,
target_wallet_address=address,
chain_id="peaq",
two_factor_code="654321", # Fresh TOTP code
faucet_base_url=FAUCET_URL,
)
if result["status"] == "success":
print("Funded:", result["tx_hash"], result["funded_amount"])
else:
print("Skipped, current balance:", result["current_balance"])
```
Call `registerMachine` to register the machine on the IdentityRegistry. The call sends 1 PEAQ as a bond with the transaction.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
// Build a new client using the machine's own key
const machineClient = new PeaqosClient({
rpcUrl: client.rpcUrl,
privateKey: keypair.privateKey,
contracts: client.contracts,
});
const machineId = await machineClient.registerMachine();
console.log("Registered, machine ID:", machineId);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# Build a new client using the machine's own key
machine_client = PeaqosClient(
rpc_url=client.rpc_url,
private_key=private_key,
identity_registry=client.contracts.identity_registry,
identity_staking=client.contracts.identity_staking,
event_registry=client.contracts.event_registry,
machine_nft=client.contracts.machine_nft,
did_registry=client.contracts.did_registry,
batch_precompile=client.contracts.batch_precompile,
)
machine_id = machine_client.register_machine()
print("Registered, machine ID:", machine_id)
```
`registerMachine` mints the Identity NFT (`tokenId == machineId`) and stakes the bond. It does not mint the Machine NFT or write DID attributes; those are two separate calls in the next step.
Call `mintNft` to create the machine's financial NFT, then `writeMachineDIDAttributes` to bind `machineId` and the new `nftTokenId` to the machine's DID.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const tx = await machineClient.mintNft(machineId, keypair.address);
const nftTokenId = await machineClient.tokenIdOf(machineId);
await machineClient.writeMachineDIDAttributes({
machineId,
nftTokenId,
operatorDid: "",
documentationUrl: "https://example.com/docs",
dataApi: "https://example.com/events",
dataVisibility: "public",
});
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
machine_client.mint_nft(machine_id, address)
nft_token_id = machine_client.token_id_of(machine_id)
machine_client.write_machine_did_attributes(
machine_id=machine_id,
nft_token_id=nft_token_id,
operator_did="",
documentation_url="https://example.com/docs",
data_api="https://example.com/events",
data_visibility="public",
)
```
The Machine NFT gets an independent `tokenId` separate from `machineId`. See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics) for the full rationale.
After all three calls, the machine has a peaqID, an Identity NFT, a Machine NFT, and a 1 PEAQ bond. It starts as `Provisioned`: on-chain, but not yet rated. It will graduate onto the rated MCR scale once it accumulates enough revenue and activity events.
## Error handling
| Error | Cause | Resolution |
| :---------------------------------------------------------------------------------------------- | :--------------------------------------- | :----------------------------------------------------------------- |
| `ValidationError: PEAQOS_PRIVATE_KEY is required` | Missing environment variable | Check your `.env` file and shell export |
| `AlreadyRegistered` (both SDKs) | The address is already registered | Query the MCR API with the address to find the existing machine ID |
| `RuntimeError: Invalid 2FA code` / `ApiError: Invalid 2FA code` | TOTP code expired or mistyped | Wait for a fresh code from your authenticator and retry |
| `RuntimeError: 2FA locked` / `ApiError: 2FA locked` | Too many invalid 2FA attempts | Wait before retrying |
| `RuntimeError: Faucet rate limit exceeded` / `ApiError: Faucet rate limit exceeded` | Too many funding requests | Wait and retry after a short interval |
| `RuntimeError: Owner daily funding cap exceeded` / `ApiError: Owner daily funding cap exceeded` | Daily funding cap reached for this owner | Fund manually or wait until the cap resets |
| Insufficient balance for gas + bond | Wallet does not hold 1 PEAQ + gas | Top up the machine wallet and retry |
## What the machine receives
After completing the full flow (register + mint NFT + write DID attributes):
* **peaqID** (W3C DID) anchoring the machine's identity
* **Identity NFT** minted automatically at registration (`tokenId == machineId`)
* **Machine NFT** (LayerZero V2 ONFT) minted via `mintNft`, with an independent `tokenId`
* **1 PEAQ bond** deposited into the IdentityStaking contract
* **Gas Station** funding (if requested)
# 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.
## 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: 1,
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` | Positive integer | `machineId must be a positive integer` |
| `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}$` / `currency must be empty for activity events` |
| `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` |
| `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: "0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658"
```
```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: 1,
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: 1,
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: 1,
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: 1, 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.10 (for the Python SDK) |
| 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)
pip install peaq-os-cli
# 2. Add the peaqos skill to your agent (auto-detects Claude Code, Cursor, or Windsurf)
npx @peaqos/skills add peaqos
```
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/skills` installer 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 --doc-url "https://example.com/docs" --data-api "https://example.com/events"
```
Drives the same flows as the SDKs from your terminal: `peaqos init`, `peaqos whoami`, `peaqos activate`, `peaqos qualify event`, `peaqos qualify mcr`, `peaqos show machine`, `peaqos show operator machines`, `peaqos wallet`. 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.0.2" python-dotenv
# Optional: include the OWS wallet helpers (createWallet, importWallet, …)
pip install "peaq-os-sdk[ows]>=0.0.2"
```
`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.10
* `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.network` for the public peaq-hosted Gas Station. Required for the `peaqos activate` funding step unless `--skip-funding` is set or the wallet already meets the gas threshold. |
| `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` | MCR API base URL. The default is for self-hosted dev. Set to `https://mcr.peaq.xyz` to read from the public peaq-hosted MCR. |
Full reference with defaults on [SDK JS environment](/peaqos/sdk-reference/sdk-js) and [SDK Python environment](/peaqos/sdk-reference/sdk-python).
## 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).
### Required
| Variable | Address |
| :-------------------------- | :------------------------------------------------------------------- |
| `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` |
| `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` |
| `EVENT_REGISTRY_ADDRESS` | `0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9` |
| `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).
### Required
| 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) |
| `MACHINE_NFT_ADAPTER_ADDRESS` | `0x63fD7e64A38e50D1486Bc569B4CaCeD38528De22` | `bridgeNft` / `bridge_nft` when `source="peaq"` (LayerZero V2 ONFT) |
| `ADMIN_FLAGS_ADDRESS` | `0x4181a2Aa34aFb247450FfcBd65be5aBD4Cbee658` | MCR API server (negative-flag + trust-override reads) |
**Bridging cannot be exercised on agung.** `MachineNFTAdapter` is deployed, but LayerZero V2 has no DVN routes between agung and Base. `bridgeNft` / `bridge_nft` calls will not relay end-to-end. 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 operating system for the machine economy. 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. Machine NFTs bridge between peaq and Base via LayerZero V2 ONFT; additional peaqOS chains are added as peer contracts deploy.
## Functions
Put your machine on-chain: peaqID, Machine NFT, and bond. Self-managed and proxy operator onboarding 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 that transacts and consumes services on your machine's behalf. Fleet orchestration and omnichain expansion.
Read the [Scale function](/peaqos/functions/scale).
Prove a machine is real via hardware attestation and trusted third parties.
Read the [Verify function](/peaqos/functions/verify).
List your machine's services. Other machines and agents discover and buy them.
Read the [Monetize function](/peaqos/functions/monetize).
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, `registerMachine`.
One owner runs a fleet. Register many machines via `registerFor`.
## 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 self-managed or proxy-managed architecture, then runs the CLI to register, mint the Machine NFT, 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. |
| **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
peaqos --version
```
Python 3.10 or newer is required.
### 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 @peaqos/skills add peaqos
# Or target a specific runtime
npx @peaqos/skills add peaqos --agent claude-code
npx @peaqos/skills add peaqos --agent cursor
npx @peaqos/skills add peaqos --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, 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
```
All four concrete classes extend `PeaqosError` directly. `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.
***
## 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
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` | `mintNft` / `mint_nft` | Operator-mint guard: recipient must be the registered machine owner |
| `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`. Surfaced as `RuntimeError.code` (JS) or `ApiError.code` (Python).
| 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)
# 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.network"
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: `0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9`
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: "0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9"
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 |
| `peaq-os-sdk` | ≥ 0.0.2 (PyPI, pulled in by the workspace) |
| 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.network"
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: "0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9"
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.
## 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.
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.
# 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.
## 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)
* **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 |
| `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 `PEAQOS_*_ADDRESS` variables 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` | `0x43c6c12eecAf4fB3F164375A9c44f8a6Efc139b9` |
| `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.
## Client
### `PeaqosClient`
```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;
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`
```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 |
***
## Registration
### `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.network",
);
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.network",
"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.
```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.network",
);
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
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`.
### `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. Additional peaqOS chains are added as peer contracts deploy. 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)
### `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
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. Must start with `did:peaq:0x`.
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` (whole USD as float, **not cents**), `averageRevenuePerEvent` (whole USD as float), `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. Must start with `did:peaq:0x`.
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,
} from "@peaqos/peaq-os-sdk";
```
***
## 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 {
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.
## 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.0.2" python-dotenv
# Optional, only for OWS wallet lifecycle helpers:
pip install "peaq-os-sdk[ows]>=0.0.2"
```
* **Python:** ≥ 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`.
***
## 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,
) -> None: ...
```
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`
```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 |
***
## Registration
### `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.network",
)
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.network",
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.
```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.network",
)
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
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).
### `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. Additional peaqOS chains are added as peer contracts deploy. 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)
### `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
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. Must start with `did:peaq:0x`.
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), `average_revenue_per_event: float` (USD), `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. Must start with `did:peaq:0x`.
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,
)
```
See [errors](/peaqos/sdk-reference/errors) for the full hierarchy and the 20-code faucet table.
***
## Constants
```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).
# 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
Register your first machine in five minutes.
Install the SDK, generate a keypair, register a machine.
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
pip install peaq-os-cli
npx @peaqos/skills add peaqos
```
Auto-detects Claude Code, Cursor, or Windsurf. Invoke `/peaqos` in Claude Code and it drives the whole flow below. 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"}}
pip install peaq-os-cli
peaqos init && peaqos activate --doc-url "https://example.com/docs" --data-api "https://example.com/events"
```
Wraps the SDK into terminal commands. 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
```
```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 python-dotenv
```
**Coming soon.**
```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. Full reference on [SDK: ROS 2](/peaqos/sdk-reference/ros2/overview).
Full install reference on the [install page](/peaqos/install).
Create a `.env` file:
```bash 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...
# Mainnet contracts auto-populated by fromEnv(); shown here for reference. Override only for custom deploys.
# For agung testnet 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
# Optional. Defaults to http://127.0.0.1:8000
PEAQOS_MCR_API_URL=https://mcr.peaq.xyz
```
Mainnet contract addresses (identity, staking, event, NFT, DID, batch) populate automatically with `PeaqosClient.fromEnv()`, so you don't need to set them manually. See the [environment variables table](/peaqos/install#environment-variables) for the full list and the [public RPC endpoints](/peaqos/install#public-rpc-endpoints) section for QuickNode primary plus OnFinality/PublicNode fallbacks.
```typescript JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
// Register the client's own wallet as a new machine.
// The call sends the minimum bond (1 PEAQ) with the transaction.
const machineId = await client.registerMachine();
console.log("Registered machine ID:", machineId);
```
```python 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()
# Register the client's own wallet as a new machine.
# The call sends the minimum bond (1 PEAQ) with the transaction.
machine_id = client.register_machine()
print("Registered machine ID:", machine_id)
```
Proxy operators register machines on behalf of another wallet. See the [self-managed](/peaqos/guides/self-managed-onboarding) and [proxy operator](/peaqos/guides/proxy-operator-fleet) guides for the fleet flow with `registerFor`.
* `PeaqosClient.fromEnv()` bound the signing key from `PEAQOS_PRIVATE_KEY` and loaded contract addresses from the environment.
* `registerMachine` sent 1 PEAQ as `msg.value`, registered the identity in IdentityRegistry, and returned the newly assigned machine ID.
* The machine ID is the handle for every downstream call: minting the Machine NFT (`mintNft`), writing DID attributes, submitting events, and MCR queries.
## Next
Owner = operator. One machine, one wallet.
One owner, many machines, `registerFor`.
# 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. Machines get a self-sovereign identity that travels across chains.
**Live.** Machine Credit Rating: a score built from revenue and activity history. Third parties can query any machine's creditworthiness via API.
**Coming Soon.** Pair an AI agent that transacts and consumes services on your machine's behalf. Fleet management and omnichain expansion.
**Coming Soon.** Prove your machine is real via hardware attestation and trusted third parties.
**Coming Soon.** List your machine's services. Other machines and agents discover and buy them.
**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 six functions
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 are next. Optional and additive: the raw-key flow keeps working. See [Wallets (OWS)](/peaqos/wallets).