`. There is no separate Identity NFT, no `mintNft` call, and no `tokenIdOf` lookup. Transferring the NFT moves ownership of the machine (the account that can rotate the DID controller and transfer again) and leaves the controller in place. Cross-chain movement of a 2.0 machine is a whole-record relocation through `MachineBridgeAdapter`, which is disabled on chain today.
Everything below this line describes the **Tokenomics 1.0** model that machines onboarded before 2026-09-01 still use, and whose LayerZero V2 ONFT bridge to Base and Solana is live.
## What the Machine NFT represents (Tokenomics 1.0)
The Machine NFT is a financial digital twin, separate from the Identity NFT that IdentityRegistry mints at registration. It carries revenue history, Machine Credit Rating, documentation links, and bond status in its metadata. The two tokens live in different token spaces and are linked by the on-chain `machineId`:
| Token | Contract | Token ID | Purpose | Transferability |
| :----------- | :--------------- | :--------------------------------------------- | :----------------------------------------------------------- | :--------------------------------------------------------------------------- |
| Identity NFT | IdentityRegistry | Equal to `machineId` | On-chain identity credential, governs protocol authorization | Soulbound: transfers revert with `IdentityNFTSoulbound` |
| Machine NFT | MachineNFT | Independent tokenId (not equal to `machineId`) | Financial digital twin, carries MCR and revenue metadata | Transfer moves the financial representation; identity stays with the machine |
Selling or bridging the Machine NFT does not affect the machine's DID, event submission rights, or protocol authorization.
**Machine ID ≠ Machine NFT token ID.** The `registerMachine` SDK method (`register()` on IdentityRegistry) and `registerFor` SDK method (`registerFor(machineAddress)` on IdentityRegistry) both return a `machineId`. The `mintNft(machineId, recipient)` SDK method (`mint(machineId, recipient)` on MachineNFT) results in a different token ID. MachineNFT has its own auto-incrementing tokenId sequence. Read the Machine NFT token ID back with `tokenIdOf(machineId)`.
## Machine Card
The Machine Card is a peaqOS registration document that follows the ERC-8004 registration file pattern and is served by the MCR API at `/machines/{machine_id}`:
```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
"type": "peaqos:registration:v1",
"name": "Machine #42",
"description": "peaqOS machine",
"did": "did:peaq:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
"active": true,
"services": [
{ "name": "web", "endpoint": "https://example.com/machines/42/api" }
],
"data_visibility": "private",
"documentation_url": "https://example.com/machines/42",
"operator": "did:peaq:0xOperatorAddress",
"bond_status": "bonded",
"event_count": 150,
"registrations": [
{
"type": "peaqos:registration:v1",
"machineId": 42,
"machineRegistry": "eip155:3338:0xIdentityRegistryAddress"
}
]
}
```
For the full MCR score and revenue summary, use [GET /machine/](/peaqos/api-reference/get-machine) or [GET /mcr/](/peaqos/api-reference/get-mcr); both are computed live by the MCR API on each request.
## Ownership semantics
Unlike the Identity NFT, the Machine NFT is **not** minted automatically during registration. It's a two-step flow owned by the operator or machine:
The proxy or machine calls **IdentityRegistry**'s `register()` (proxy uses `registerFor(machineAddress)`) and receives a `machineId`. IdentityRegistry simultaneously mints the Identity NFT to `machineAddress` with `tokenId == machineId`. SDK helpers: `registerMachine` / `registerFor`.
The operator or machine then calls **MachineNFT**'s `mint(machineId, recipient)`. MachineNFT assigns a new, independent `tokenId`. Read the assigned token ID back with `tokenIdOf(machineId)`. SDK helper: `mintNft`.
Call `writeMachineDIDAttributes` to store `machineId` and `nftTokenId` on the machine's DID, binding the identity to its financial twin. These attribute writes must be signed by the machine's own key (the DID subject); the proxy can't write them on the machine's behalf. Registration does not write them for you.
## Metadata flow
`tokenURI()` returns a URL pointing to the MCR API. The API reads onchain events and DID attributes, computes the current MCR score, and returns the full machine profile JSON.
A consumer or marketplace calls `tokenURI(tokenId)` on the **MachineNFT** contract and receives the metadata URL.
The consumer calls [`GET /metadata/{token_id}`](/peaqos/api-reference/get-metadata) on the MCR API.
The API reads onchain events and DID attributes, computes the current MCR score, and returns the same shape as [`GET /machine/{did}`](/peaqos/api-reference/get-machine): the full machine profile.
The `baseURI` is updatable by the protocol admin. If the API domain changes, only the contract's `baseURI` is updated. No token migration needed.
The lighter [Machine Card](#machine-card) (ERC-8004 registration document) lives at a separate endpoint: [`GET /machines/{machine_id}`](/peaqos/api-reference/get-machine-card).
## Cross-chain portability
**Supported routes: peaq ↔ Base (SDK and CLI) and peaq ↔ Solana (live on chain since 2026-08-21, operated with peaq's tooling; the SDKs accept only `"base"` as a destination today).** Bridging is mainnet-only: LayerZero V2 has no DVN routes from agung, so `bridgeNft` / `bridge_nft` cannot be exercised against the testnet.
The Machine NFT implements the LayerZero V2 ONFT standard. peaq is the home chain: its NFT supply is canonical and uses a **lock/unlock** adapter (`MachineNFTAdapter`); destination chains use the standard **burn/mint** pattern. A token is either transferable on peaq or represented by exactly one live twin on one spoke, never both.
Canonical Machine NFT contract. Minting and DID linking happen here. Bridging out locks the NFT in `MachineNFTAdapter`; bridging back unlocks it.
Bridged destination via LayerZero V2 ONFT (EID 30184). Bridging in mints; bridging back to peaq burns.
Bridged destination via LayerZero V2 (EID 30168). The twin is a Metaplex Core asset in collection `4Xa1sDAHNWJ4WMkV9ZYvycbinkf8iwwdwNmYRxqZDPtz`, not an SPL token; its address changes on every crossing, so key on the peaq token ID. Return legs need a v0 transaction with an address lookup table.
peaq is the home chain. peaq → spoke locks on peaq + mints on the spoke; spoke → peaq burns on the spoke + unlocks on peaq.
Key properties:
* **Home chain**: peaq. Minting happens on peaq after machine onboarding.
* **Cross-chain**: LayerZero V2 bridges peaq ↔ Base and peaq ↔ Solana via the `MachineNFTAdapter` lock/unlock pattern on peaq paired with burn/mint on the destination. The Solana program is `HraxgdzfcAi3AnxRP5sGSrXGAb9ZT1tZTMNuh9vQLxTu`; its `tokenURI` equivalent points at the same MCR API metadata.
* **Metadata**: `tokenURI()` resolves to the same MCR API URL regardless of which chain holds the NFT.
* **Identity independence**: The machine's peaqID, DID attributes, and event submission stay on peaq regardless of where the NFT sits.
## Cross-links
* [peaqID](/peaqos/concepts/peaqid) is the DID linked to the Machine NFT via the `nftTokenId` attribute
* [GET /metadata/](/peaqos/api-reference/get-metadata) returns the full NFT metadata JSON
* [Activate function](/peaqos/functions/activate) handles the minting flow during onboarding
# Omni-chain
Source: https://docs.peaq.xyz/peaqos/concepts/omni-chain
How peaqOS mirrors identity, DID, and credit state from peaq to every supported chain via signed-push Lite contracts and an off-chain Signer Daemon.
**Omni-chain V1 shipped with Scale.** Lite contracts and the Signer Daemon are live across supported EVM chains; per-chain proxy addresses live in the launch manifest. **Solana joined in August 2026**: see [Solana](#solana) below for what is live there and what is not.
peaq stays canonical. Every other chain runs a thin, read-only mirror of peaq state, kept in sync by a Signer Daemon that watches finalized peaq events, packages them into EIP-712 batches, signs them with a per-chain push key, and pushes them to satellite Lite contracts.
Two things this page does not cover. [Economics 2.0](/peaqos/concepts/economics-2-0) relocates whole machine records between registered chains through `MachineBridgeAdapter` and `CrossChainMirror` over LayerZero V2; that mechanism is switched off on chain today and the SDKs expose status reads only. And the 1.0 Machine NFT bridge (peaq ↔ Base, peaq ↔ Solana) is on the [Machine NFT](/peaqos/concepts/machine-nft#cross-chain-portability) page.
This means a contract or app on Base, Ethereum, Polygon, or any other supported chain can resolve a machine's peaqID, identity, MCR, and bond status without an RPC hop to peaq, and without trusting an off-chain oracle on top.
## What the mesh looks like
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
peaq home chain
┌────────────────────────┐
│ IdentityRegistry │
│ peaq DID precompile │
│ IdentityStaking │
│ MCR pipeline │
└──────────┬─────────────┘
│ finalized events
▼
┌────────────────────────┐
│ Signer Daemon │
│ (off-chain, peaq-side) │
│ batches + EIP-712 sign │
└─┬────────┬─────────────┘
│ │
┌──────────┘ └──────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Base satellite│ │ other chains │
│ IdentityLite │ │ IdentityLite │
│ DIDLite │ │ DIDLite │
│ StakingLite │ │ StakingLite │
└───────────────┘ └───────────────┘
```
Domain separator: `name = "PeaqosLite"`, `version = "1.0.0"`. Each Lite is identified by `(chainId, verifyingContract)` per EIP-712.
## DIDLite
DIDLite mirrors the peaq DID precompile's per-attribute records onto every satellite chain. Consumers on the satellite resolve any peaq DID account's attributes (including the locked `"peaqID"` attribute) without an extra cross-chain hop. Records are written exclusively by signed batches from the Signer Daemon; consumers only read.
### Public consumer views
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function readAttribute(address didAccount, bytes calldata attrName)
external view
returns (
bytes memory value,
uint32 validity,
uint64 lastUpdatedHomeBlock,
uint64 lastBatchAcceptedAtTs
);
function readAttributeRaw(address didAccount, bytes calldata attrName)
external view
returns (DIDAttributeRecord memory record, uint64 lastBatchAcceptedAtTs);
// admin/debug only; ignores the pause gate; returns soft-deleted records as-is
function lastHomeBlockApplied() external view returns (uint64);
struct DIDAttributeRecord {
bytes value; // raw bytes from peaq; empty after soft-delete
uint32 validity; // peaq DID validity field
uint64 lastUpdatedHomeBlock;
bool removed; // soft-delete sentinel
}
```
`readAttribute` reverts `LitePaused` while the Lite is paused. `readAttributeRaw` is an admin/debug carve-out that ignores the gate. `lastHomeBlockApplied()` is intentionally not pause-gated so consumers can still read the global watermark when reads are paused.
To resolve a peaqID, compose: `readAttribute(didAccount, "peaqID")`. The orchestrator does not ship a dedicated `peaqIDOf` view; clients decode the 32-byte value themselves.
### Consumer view errors
* `AttributeNotFound(address didAccount, bytes attrName)`
* `AttributeRemoved(address didAccount, bytes attrName, uint64 removedAtHomeBlock)`
* `LitePaused()`
## IdentityLite
IdentityLite mirrors the peaq `IdentityRegistry` per-machineId record. Consumers gate writes that depend on having seen a specific peaq approval by reading `lastCursorPacked()`.
### Public consumer views
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function getIdentity(uint256 machineId)
external view
returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt);
// pause-gated; lastBatchAcceptedAt == 0 is the cold-start sentinel
function getIdentityRaw(uint256 machineId)
external view
returns (IdentityRecord memory record, uint64 lastBatchAcceptedAt);
// admin/debug only; ignores pause gate
function lastCursor()
external view
returns (uint64 blockNumber, uint32 txIndex, uint32 logIndex);
// unpacked; NOT pause-gated (cross-Lite read invariant)
function lastCursorPacked() external view returns (uint128);
// packed = (block << 64) | (txIndex << 32) | logIndex
// NOT pause-gated
function lastUpdatedHomeBlock(uint256 machineId) external view returns (uint64);
// per-record cursor; pause-gated
// Home-style accessors (additive to getIdentity)
function getOwnerIfExists(uint256 machineId)
external view
returns (address owner_, bool exists_);
function operatorOf(uint256 machineId) external view returns (address);
function getMachineStatus(uint256 machineId) external view returns (MachineStatus);
function getMachineURI(uint256 machineId) external view returns (string memory);
struct IdentityRecord {
address owner; // immutable post-Registered (soulbound on peaq)
address operator; // mutable; address(0) clears
string machineURI; // immutable post-Registered
MachineStatus status;
uint64 lastHomeBlock;
}
enum MachineStatus { None, Pending, Verified, Rejected, Deactivated }
// Ordinals match peaq IdentityRegistry. Do not reorder.
```
### Consumer view errors
* `MachineNotFound(uint256 machineId)`
* `LiteUninitialized(address lite)` — not raised by the Lite itself. Read `(record, lastAt)` and `require(lastAt > 0, LiteUninitialized(address(this)))`.
* `LitePaused()`
### Cold-start pattern
The Lite returns `(record, 0)` when it has never accepted a batch, rather than reverting, so callers choose the policy:
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
(IdentityRecord memory rec, uint64 lastAt) = identityLite.getIdentity(machineId);
require(lastAt > 0, LiteUninitialized(address(identityLite)));
require(rec.status == MachineStatus.Verified, "not verified");
```
## StakingLite
StakingLite mirrors the peaq `IdentityStaking` per-machine stake record. Consumers gate authorisation or service eligibility on stake state without crossing chains.
### Public consumer views
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
function getStake(uint256 machineId)
external view
returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_);
// pause-gated; lastBatchAcceptedAt_ == 0 is the cold-start sentinel
function getStakeRaw(uint256 machineId)
external view
returns (StakeRecord memory record, uint64 lastBatchAcceptedAt_);
// admin/debug only; ignores pause gate
function isStaked(uint256 machineId) external view returns (bool);
// pause-gated
function isAuthorized(address wallet)
external view
returns (bool isAuth, uint64 lastBatchAcceptedAt_);
// pause-gated; cold-start sentinel applies
function totalStaked() external view returns (uint256);
// pause-gated; aggregate of applied Staked.amount events
```
Same cold-start pattern as IdentityLite — `require(lastBatchAcceptedAt_ > 0, LiteUninitialized(address(this)))` before trusting reads. StakingLite has its own pause flag and its own EIP-712 schema (`StakingEvent`), but shares the `PeaqosLite` domain and the cross-language daemon parity gate.
## EIP-712 schemas
### 7-field IdentityEvent
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
IdentityEvent(uint8 kind,uint256 machineId,address subject,string machineURI,uint64 homeBlockNumber,uint32 txIndex,uint32 logIndex)
```
`txIndex` and `logIndex` were added vs the earlier 5-field shape so the on-chain cursor can be sub-block-precise. The daemon-side encoder is bit-identical to Solidity (parity-gated by the EIP-712 fixture suite).
### DIDEvent
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
DIDEvent(uint8 kind,address didAccount,bytes attrName,bytes attrValue,uint32 validity,uint64 homeBlockNumber)
```
Both `bytes` fields are pre-hashed with `keccak256(bytes(...))` per EIP-712 dynamic-bytes rule. `kind` ordinal: `0 = Add, 1 = Update, 2 = Remove`. Removes must carry `value.length == 0` and `validity == 0`.
### Batch envelope (shared)
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
Batch(uint64 nonce,uint64 deadline,uint8 schemaVersion,bytes32 eventsRoot,uint64 cursorLo_block,uint32 cursorLo_txIndex,uint32 cursorLo_logIndex,uint64 cursorHi_block,uint32 cursorHi_txIndex,uint32 cursorHi_logIndex)
```
Schema version is per-Lite. A typehash bump requires lockstep upgrade of both the Lite and the daemon.
## Signer Daemon
Off-chain. Python package, one instance per `(home, satellite, Lite)` triple. V1 shipped 2026-05-21 with a **six-daemon fleet** across two pipelines: three for peaq mainnet (home) → Agung (satellite) and three for Agung (home) → Base Sepolia (satellite). Each daemon binds to one `LITE_NAME` (`IdentityLite | DIDLite | StakingLite`), one push key (`currentSigner` on the Lite), one `HEALTH_PORT`, and one `CURSOR_FILE_PATH`. Six unique push keys total — reuse triggers nonce races. A direct peaq mainnet → Base Sepolia pipeline is registered in the SDK satellite registry but not yet rolled out as a daemon deployment.
| Property | Behaviour |
| :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Finality | GRANDPA-finalized only. No `latest-N` fallback. Daemon freezes if GRANDPA stalls rather than serve unfinalized state. |
| Batching | Critical events flush immediately; non-critical aggregate up to `flush_after_sec=30` or `max_batch_size=50`. `MAX_BATCH_SIZE` env override bounded `[1, 50]`; required `=10` for DIDLite (byte-payload events exceed Agung block gas at 50). |
| Nonce ordering | Per-chain EVM nonce lock + per-Lite monotonic batch nonce. `AlreadyApplied` → skip-advance. `NonceOutOfOrder` → backfill missing nonce(s). |
| Restart catchup | On boot, `resume_cursor = max(localCursor, liteCursor)`. Prevents replay after a crash between Lite acceptance and local commit. |
| Pause retries | `WritesPaused` from `applyBatch` triggers exponential backoff `(1, 5, 15, 60, 300)s` rather than fatal exit, so admin pauses do not crash the fleet. |
| Poison events (DIDLite) | Orphan `Update`/`Remove` events whose `Add` predates the deploy block trigger `AttributeDoesNotExist` or `AttributeAlreadySoftDeleted`. The daemon decodes the revert, isolates the offending event, pushes the rest of the batch, and increments `poison_event_skipped_per_lite[lite]` on `/health`. Under partial-history replay the satellite is best-effort cache, not authoritative. |
| Cross-language parity | EIP-712 typehashes are constants in Solidity (`EVENT_TYPEHASH_V1`, `BATCH_TYPEHASH_V1`) and in Python. A Hardhat task dumps canonical fixtures; Python recomputes identical hashes (17 fixtures, all 65-byte signatures bit-identical). |
| Hot-wallet floor | `HOT_WALLET_MIN_BALANCE_WEI` (default `0.02` native). Below the floor the daemon stops signing and `/health.hot_wallet_low[chain_id] = true`. |
### Health endpoint
Each daemon exposes a loopback-only `/health` (default port unique per instance, conventionally 8080–8085):
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
signer_key_id,
last_push_at,
hot_wallet_low: { [chain_id]: bool },
queue_depth,
poison_event_skipped_per_lite: { [lite_name]: int }
}
```
Operator runbooks (env vars, deploy steps, monitoring, troubleshooting) for the Signer Daemon will live in the Operate section: `signer-daemon-deploy`, `signer-daemon-monitoring`, and `signer-daemon-troubleshooting` (coming soon).
## Pause and emergency model
Every Lite has two independent pause flags and one emergency flag, all `external onlyOwner`:
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
pauseLite(string reason) // pause user-facing reads
unpauseLite() // resume reads (blocked while in emergency)
pauseApplyBatch(string reason) // pause inbound writes
unpauseApplyBatch() // resume writes (allowed even in emergency)
setSigner(address newSigner) // routine PUSH_KEY rotation
emergencyRotatePushKey(address) // incident-response rotation
exitEmergencyMode() // requires currentSigner != snapshot
adminReplayEvents(...) // owner-driven replay/rewind
```
Routine `setSigner` rotation opens a `GRACE_BLOCKS = 600` (\~1h) window where the previous PUSH\_KEY remains valid so in-flight signed batches do not fail mid-flight. Emergency rotation does not keep the previous key valid.
### EmergencyMode
EmergencyMode is two booleans plus a snapshot address, not an enum:
```solidity theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
bool inEmergencyMode;
address emergencyEnteredBySigner; // snapshot at first incident pause
bool litePaused; // read pause
bool applyBatchPaused; // write pause
```
A first `pauseLite` or `pauseApplyBatch` sets `inEmergencyMode = true` and snapshots `emergencyEnteredBySigner = currentSigner`. To exit, the owner calls `emergencyRotatePushKey(newKey)` (rotation must actually change the signer) then `exitEmergencyMode()`. Pause flags are not auto-cleared.
### Hot/cold key collapse defense
The cold key is `owner()` (admin and upgrade authority). The hot key is `currentSigner` (the PUSH\_KEY on the daemon server). A single transaction must never collapse them onto the same address. Enforcement points:
* `initialize(owner_, pushKey_)` reverts if `pushKey_ == owner_`.
* `setSigner(newSigner)` reverts if `newSigner ∈ {owner(), pendingOwner()}`.
* `emergencyRotatePushKey(newPushKey)` reverts on the same membership check.
* `transferOwnership` / `_transferOwnership` reject `newOwner ∈ {currentSigner, previousSigner}`.
* `renounceOwnership` is permanently disabled.
A stolen daemon key cannot also seize upgrade authority.
## Staleness policy
The Lite does **not** staleness-revert. SDK and ops layers apply staleness gates. Consumers should compare `lastBatchAcceptedAt` to `block.timestamp` and reject reads older than their own SLO. The Lite only blocks cold-start via the consumer-side sentinel pattern.
## Soft delete (DIDLite)
Removed DID attributes stay in storage with `removed = true` and `value = ""`. `readAttribute` reverts `AttributeRemoved(...)` for these. `readAttributeRaw` returns them as-is for admin/debug.
## What an integrator does
1. **Resolve a peaqID on a satellite chain.** Call `readAttribute(didAccount, "peaqID")` and decode the returned `bytes` as a `bytes32`. Always pair with `require(lastBatchAcceptedAtTs > 0)` to handle cold start.
2. **Read identity status.** `getIdentity(machineId)` returns the full record. Same cold-start check. Or use the granular home-style getters: `getOwnerIfExists`, `operatorOf`, `getMachineStatus`, `getMachineURI`.
3. **Read stake state.** `StakingLite.getStake(machineId)`, `isStaked(machineId)`, `isAuthorized(wallet)`, or `totalStaked()`.
4. **Gate on a specific peaq approval.** Read `IdentityLite.lastCursorPacked() >= myExpectedCursor` before letting a satellite action depend on it. Use the unpaused `lastCursor()` view if you need the unpacked form.
5. **Apply your own staleness SLO.** Compare `lastBatchAcceptedAt` to `block.timestamp`. The Lite intentionally does not enforce one.
## Solana
Solana is the first non-EVM satellite. Four Anchor programs for the Tokenomics 1.0 mirrors are deployed on Solana mainnet-beta (and devnet): a signer daemon pushes finalized peaq mainnet state into three of them, and the fourth is a trustless address-binding registry. Since 2026-09-09 the Economics 2.0 programs are deployed as well, nine since 2026-09-11 (addresses on [Smart contracts](/peaqos/concepts/contracts#economics-2-0-solana-mainnet)). What is reachable today:
| Capability | Status on 2026-09-09 | How to use it |
| :------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
| Resolve a Solana address to a peaq machine and its MCR | **Live, public API** | [`GET /solana/mcr/{solana_addr}`](/peaqos/api-reference/get-solana-mcr) and `GET /solana/operator/{solana_addr}/machines` on `mcr.peaq.xyz` |
| Bind a Solana wallet to a peaq address | **Live on chain**, permissionless dual-signed `bind` / `revoke` on the binding program. No SDK or CLI surface yet; peaq runs the binding for partner fleets | Program `GrsCoPkeLUAoXjuwPfCcuch77Chco1Gx95zCCQNYGqG6` |
| Read peaq machine identity (owner, operator, status) on Solana | **Live**, mirror pushed from peaq mainnet in batches, not on a fixed schedule (the last push before this snapshot was on 2026-09-05), so the Solana copy can lag peaq by hours or days. Read-only; no SDK package published yet, so consumers derive the PDA themselves | Program `AWGibJPvQnj8mhfK8QWdUhvJBySCcwYadqAdtj2N137m`, PDA `["identity", keccak256(machineId)]` |
| Read peaq machine stake on Solana | **Live**, same batches as the identity mirror | Program `4PyqZKtej7CWcV1jZ5h5FMd8EoR5edTTaZxAxEQmVDvs`, PDA `["stake", keccak256(machineId)]` |
| Read peaq DID attributes on Solana | **Deployed**, stores the keccak hash of each attribute value (not the value). The stream has not received new batches since mid-August 2026; do not rely on it for freshness | Program `AMQgyqNcWSJy6cK2wSmXRykgjoPhSroKkjKtN8Y11WMc` |
| Bridge a Machine NFT peaq → Solana and back | **Live on mainnet since 2026-08-21** (LayerZero V2, Metaplex Core twin). Operated with peaq's tooling; the SDKs accept only `"base"` as a destination today | [Machine NFT](/peaqos/concepts/machine-nft#cross-chain-portability) |
| Pay for streams and market orders in SOL or SPL tokens | **Live** since SDK 0.4.0 / CLI 0.0.6 | [`peaqos stream pay --chain solana`](/peaqos/cli#solana-payments) |
| Onboard a machine that holds only a Solana wallet | **Not available yet.** Under development; no public endpoint | |
| Economics 2.0 on Solana | **Programs live on mainnet since 2026-09-09, the full set since 2026-09-11, nothing for users yet.** Nine Anchor programs (InfoDesk, MachineBridgeAdapter, MachineRegistry, CoordinationFeeCollector, MachineStateAndSync, EventRegistry, PriceOracle, TreasuryPool, TrustValidatorStaking) are deployed and peaq's `MachineBridgeAdapter` has the Solana adapter registered as its LayerZero peer; the bridging and relocation flags are off on both chains. Activating a machine on Solana is not possible yet: bonding PEAQ for a Solana machine is in development, and the SDKs and CLI have no Economics 2.0 Solana surface. Subscription and staking authority stay on peaq | |
The Solana mirrors are pushed by peaq's signer daemon with a secp256k1 push key, not by LayerZero: reading a mirror record means trusting that key holder pushed faithfully. They are a queryable replica of peaq, never a second source of truth. The binding registry is the one trustless piece: anyone can bind, and the record is only ever written by the two keys it links.
Mirror records are keyed by `keccak256(machineId)` under each program's ID; check the program's global account for `paused` and `last_finalized_home_block` before trusting a read. Identity and staking mirrors carry Tokenomics 1.0 machine IDs today. There is no `peaqos solana` command group.
## Addresses
Per-chain proxy addresses are pasted into operator `.env` files generated from templates in `signer-daemon/deploy/` and surfaced through the SDK satellite registry. There is no single deploy-manifest JSON in the repo. Track the launch announcement for the canonical Agung and Base Sepolia address list, or pull them from `peaqos/concepts/contracts` once published.
## Related
* Satellite mirrors — propagation and cursor-independence model (coming soon)
* Signer daemon — runtime surface (coming soon)
* Satellite SDK overview — JS/Python bindings (coming soon)
* Lite views (Solidity) — Solidity consumer surface (coming soon)
* Operate → Signer daemon deploy — production runbooks (coming soon)
* [Scale function](/peaqos/functions/scale)
* [Activate function](/peaqos/functions/activate)
* [Qualify function](/peaqos/functions/qualify)
* [Machine NFT concept](/peaqos/concepts/machine-nft)
* [peaqID concept](/peaqos/concepts/peaqid)
* [Roadmap](/roadmap)
# peaqID
Source: https://docs.peaq.xyz/peaqos/concepts/peaqid
W3C DID for machines, portable across every chain peaqOS supports.
peaqID is a W3C DID that identifies a machine across every chain it transacts on.
## DID format
There are two peaqID formats, one per generation of machine.
### Economics 2.0 machines (activated since 2026-09-01)
```
did:peaq:
```
The machine ID is `uint256(keccak256(abi.encode(machineType, credentialSubject)))`, the same value as the machine's ERC-721 token ID in `MachineRegistry`. The DID document (controller, verification methods, authentication indices, service endpoints) is stored in `MachineRegistry` itself at activation and updated with the [DID setters](/peaqos/sdk-reference/sdk-python#did-updates); `id` is computed on read, never supplied. Documentation and data API URLs live in `serviceEndpoints`. There is no mapping from an address-form DID to a 2.0 machine ID. See [Economics 2.0](/peaqos/concepts/economics-2-0#machine-id-and-did).
### Tokenomics 1.0 machines (onboarded before 2026-09-01)
```
did:peaq:<0x-address>
```
The address is the machine's EOA (externally owned account) on peaq chain. The DID resolves to a flat key-value attribute store on the peaq DID precompile (`0x0000000000000000000000000000000000000800`). Any consumer can start from the DID and traverse to the machine's identity, financial history, and credit rating through the 1.0 MCR API. The rest of this page describes this format.
## Per-machine vs per-proxy
peaqOS assigns one DID per machine and one DID per proxy operator. The two serve different roles:
| DID type | Registered by | Attributes | Purpose |
| :---------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| Machine DID | `registerMachine` (self-managed) or `registerFor` (proxy delegation) | `machineId`, `nftTokenId`, `operator`, `documentation_url`, `data_api`, `data_visibility` | Identifies a single machine, links to its Machine NFT and event history |
| Proxy DID | `registerMachine`. The proxy itself is a registered machine. | `machineId`, `machines` | Identifies an operator, lists the machine IDs of all machines it manages |
A machine that self-manages has no `operator` attribute. A proxy operator's `machines` attribute is a JSON array of machine IDs (e.g., `[123, 456, 789]`).
**DID writes always go to the caller's DID.** The DID precompile keys attributes by `msg.sender`. A proxy that calls `registerFor` mints the machine's identity NFT and pays the bond, but it cannot write attributes to the machine's DID; the machine must sign its own `writeMachineDIDAttributes` call. Skipping this leaves the machine unreachable through the MCR API.
## DID attribute table
After registration, the operator or machine must explicitly call `writeMachineDIDAttributes` (or `writeProxyDIDAttributes`) to write these attributes to the DID. Registration itself only mints the identity and creates the on-chain machine ID; the DID attribute writes are a separate transaction.
| Attribute | Type | Description |
| :------------------ | :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `machineId` | `uint256` | On-chain machine ID assigned by IdentityRegistry. Primary key for all queries. |
| `nftTokenId` | `uint256` | Token ID of the machine's Machine NFT. This is a separate ERC-721 token space and is not equal to `machineId`. |
| `operator` | `did:peaq:0x...` | Proxy operator's DID. Absent if the machine self-manages. |
| `documentation_url` | URL string | Link to machine documentation maintained by the project. |
| `data_api` | URL string | Raw data API endpoint. The MCR API reads this when `data_visibility` is `public`. |
| `data_visibility` | `public` / `private` / `onchain` | Controls how the MCR API exposes raw event data. Unset or empty defaults to `private`. |
| `machines` | JSON array string | Proxy operator DID only. List of managed machine IDs. The DID precompile stores the full JSON; the MCR API truncates to the first 100 valid IDs when reading the `machines` attribute. |
### Byte limits
Both SDKs enforce these constraints before any DID write reaches the chain:
| Constant | Value | Applies to |
| :-------------------- | :---- | :-------------- |
| `DID_MAX_NAME_BYTES` | 64 | Attribute name |
| `DID_MAX_VALUE_BYTES` | 2560 | Attribute value |
**Migration.** peaq chain has approximately 3.5 million existing peaqID holders. Existing holders retain their peaqIDs. A migration path to the current DID format is on the [roadmap](/roadmap); the new format is what onboards from peaqOS today.
## Resolving a peaqID
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
// Fetch machine profile by DID
const response = await fetch(
`${client.apiUrl}/machine/did:peaq:0xMachineAddress`
);
const machine = await response.json();
console.log(machine.peaqos.did); // "did:peaq:0x..."
console.log(machine.peaqos.machine_id); // 123
console.log(machine.peaqos.mcr); // "BBB"
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from dotenv import load_dotenv
import requests
from peaq_os_sdk import PeaqosClient
load_dotenv() # load envs from .env file
client = PeaqosClient.from_env()
# Fetch machine profile by DID
response = requests.get(
f"{client.api_url}/machine/did:peaq:0xMachineAddress"
)
machine = response.json()
print(machine["peaqos"]["did"]) # "did:peaq:0x..."
print(machine["peaqos"]["machine_id"]) # 123
print(machine["peaqos"]["mcr"]) # "BBB"
```
## Data visibility modes
The `data_visibility` attribute controls how the MCR API handles raw event data for this machine:
| Mode | MCR API behavior | Raw data location |
| :-------- | :-------------------------------------------------------------------------------------------------------------- | :------------------------ |
| `public` | Fetches from `data_api`, includes in response | Project's API |
| `private` | Returns the `data_api` URL only; consumer fetches directly | Project's API |
| `onchain` | Parses JSON metadata from EventRegistry events into `event_data[]`, capped at the first 100 events per response | Onchain (higher gas cost) |
`private` is the default when `data_visibility` is unset or empty.
## Cross-links
* [Activate function](/peaqos/functions/activate) registers machines and writes DID attributes
* [GET /machine/](/peaqos/api-reference/get-machine) returns the full machine profile, including DID-sourced metadata
* [Machine NFT](/peaqos/concepts/machine-nft) is linked to the peaqID via the `nftTokenId` attribute
# Trust levels
Source: https://docs.peaq.xyz/peaqos/concepts/trust-levels
Three tiers classifying how trustworthy a submitted event is.
Trust level classifies how trustworthy the data in a submitted event is. Higher trust levels indicate stronger guarantees about the event's authenticity.
## The three levels
The machine or operator attests to the event. No external verification: the submitter's word is the source of truth.
The event is backed by an on-chain reference (transaction hash, receipt, or cross-chain proof) that anyone can independently verify.
The event is signed by attested hardware (a secure element on the device) that binds the event to a specific physical device.
Choose the highest level you can honestly attest to. Trust levels are a field on each `EventRegistry` event and are self-declared today. Third-party attestation of the machine itself arrives with [Verify](/peaqos/functions/verify), which in its first release accepts one certified chip family (Infineon OPTIGA Trust M) rather than any TPM or enclave.
## Trust weight in the MCR
Higher trust levels contribute more to the [MCR](/peaqos/concepts/machine-credit-rating) score. Hardware-signed events carry the strongest weight, on-chain verifiable events sit in the middle, and self-reported events the lowest.
Self-reported machines with a sustained track record (a year or more of event history) earn a small graduation bonus on their trust contribution, though they remain below the on-chain tier.
A machine flagged with a [negative event](/peaqos/api-reference/get-mcr) incurs a temporary penalty on its trust contribution for a fixed window from the flag timestamp, after which the penalty expires automatically.
## Admin overrides
peaq operates two on-chain admin functions (`AdminFlags` contract) that adjust how the MCR treats a machine. Both are owner-only and emit events.
| Override | Effect |
| :----------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setTrustOverride(machineId, value)` | Replaces the trust contribution for this machine within a permitted range. Reverts with `InvalidTrustOverride(value)` if the value is out of range. Use `clearTrustOverride` to remove. |
| `flagMachine(machineId)` | Sets the negative-event flag at `block.timestamp`. The penalty applies for a fixed window from this timestamp. Use `clearFlag` to lift early. |
Submitting an event with `trustLevel > 2` reverts on `EventRegistry.submitEvent` with `InvalidTrustLevel()`.
# Activate
Source: https://docs.peaq.xyz/peaqos/functions/activate
Put your machine on-chain in one transaction. peaqID, Machine NFT, and a tier bond.
Activate is the entry point to peaqOS. One transaction registers a machine on peaq chain, mints its [Machine NFT](/peaqos/concepts/machine-nft), stores its [peaqID](/peaqos/concepts/peaqid) document, and bonds it on a subscription tier under [Economics 2.0](/peaqos/concepts/economics-2-0).
**Versions.** The one-transaction flow on this page needs `@peaqos/peaq-os-sdk` 0.7.0+, `peaq-os-sdk` 0.7.1+ (Python 3.10 or newer), or `peaq-os-cli` 0.0.8+ running on `peaq-os-sdk` 0.7.1+. Releases before 2026-09-11 carry the `MachineBridgeAdapter` address from before the 2026-09-08 re-point and fail preflight with `PEER_MISMATCH`. The earlier register-then-mint flow (`registerMachine`, `mintNft`, `writeMachineDIDAttributes`) still works in the SDKs when you do not select a Tokenomics 2.0 deployment, and is documented in the [legacy section](#legacy-flow-tokenomics-1-0) below. The CLI has no legacy path since 0.0.8.
## What ships
| Component | Description |
| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| peaqID | W3C DID stored on-chain as a DID document (verification methods, authentication, service endpoints). Its id is `did:peaq:`. |
| Machine NFT | One ERC-721 in `MachineRegistry`. The machine ID **is** the token ID. No separate Identity NFT and no separate mint call. |
| Subscription bond | PEAQ bonded on the tier you choose (Entry, Basic, or Pro). Quoted per tier at the oracle rate when you activate. Not withdrawable. See [Economics 2.0](/peaqos/concepts/economics-2-0). |
| Home chain record | `CrossChainMirror` records peaq as the machine's home chain. |
| Machine wallet | Per-machine EVM keypair, generated by the SDK or the CLI. |
## Machine ID
The machine ID is derived, not sequential:
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
machineId = uint256(keccak256(abi.encode(machineType, credentialSubject)))
```
Only `machineType` and the exact `credentialSubject` bytes contribute. Neither can change after activation, and the same pair can never be activated twice. IDs are full-width `uint256` values (routinely 77 digits): `bigint` in JavaScript, `int` in Python, and a decimal **string** in JSON, URLs, and CLI output. The 2.0 DID form is `did:peaq:`, not an address.
## How activation works
Select a deployment (`peaq-mainnet` or `agung-2026-08-28`), preview the quote, then activate. Contract addresses travel with the deployment record inside the SDK; you never set them.
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# .env: TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet (peaqos init writes it)
peaqos activate \
--machine-type Sensor \
--credential-subject-hex 0xdeadbeef \
--manufacturer 0x3333333333333333333333333333333333333333 \
--tier entry \
--did-document ./did.json \
--dry-run # preview the bond and the net PEAQ, submit nothing
# Same command without --dry-run submits the one transaction.
```
Add `--payment usdt --slippage-bps 50` to settle the PEAQ-quoted bond in USDT. `--for 0xMachine --machine-key ./machine.key` switches to machine-owned, operator-controlled mode (see [Two ownership modes](#two-ownership-modes)). Full flag table on [CLI: activate](/peaqos/cli#peaqos-activate).
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const client = new PeaqosClient<"tokenomics20">({
rpcUrl: process.env.PEAQOS_RPC_URL!,
privateKey: process.env.PEAQOS_PRIVATE_KEY!,
contracts: PeaqosClient.fromEnv().contracts, // the six legacy addresses are still required by the constructor
tokenomics20: { deploymentId: "peaq-mainnet" }, // or "agung-2026-08-28"
});
const params = {
controller: client.address,
verificationMethods: [
{
id: "#key-1",
methodType: "Ed25519VerificationKey2020",
controller: client.address,
publicKeyMultibase: "z6Mk...",
},
],
authentication: [0n],
serviceEndpoints: [
{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" },
{ id: "#data", serviceType: "DataApi", serviceEndpoint: "https://example.com/events" },
],
machineType: "Sensor",
credentialSubject: "0xdeadbeef",
manufacturer: "0x3333333333333333333333333333333333333333",
tier: 0, // 0 Entry, 1 Basic, 2 Pro
};
const preview = await client.previewMachineActivation(params);
console.log(preview.machineId, preview.bondAmount, preview.voucherCredit, preview.netPeaqAmount);
const result = await client.activateMachine(params);
console.log(result.machineId); // bigint, the permanent machine ID
console.log(result.netPeaqAmount); // PEAQ actually transferred
console.log(result.isHomedLocally); // true, confirmed on-chain
```
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import os
from dotenv import load_dotenv
from peaq_os_sdk import (
ActivateMachineParams,
PeaqosClient,
ServiceEndpointInput,
Tokenomics20Config,
VerificationMethodInput,
)
load_dotenv()
legacy = PeaqosClient.from_env() # since 0.7.1 from_env() also reads TOKENOMICS_DEPLOYMENT_ID; the explicit constructor below is equivalent
client = PeaqosClient(
rpc_url=legacy.rpc_url,
private_key=os.environ["PEAQOS_PRIVATE_KEY"],
identity_registry=legacy.contracts.identity_registry,
identity_staking=legacy.contracts.identity_staking,
event_registry=legacy.contracts.event_registry,
machine_nft=legacy.contracts.machine_nft,
did_registry=legacy.contracts.did_registry,
batch_precompile=legacy.contracts.batch_precompile,
tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), # or "agung-2026-08-28"
)
params = ActivateMachineParams(
controller=client.address,
verification_methods=(
VerificationMethodInput(
id="#key-1",
method_type="Ed25519VerificationKey2020",
controller=client.address,
public_key_multibase="z6Mk...",
),
),
authentication=(0,),
service_endpoints=(
ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"),
ServiceEndpointInput(id="#data", service_type="DataApi", service_endpoint="https://example.com/events"),
),
machine_type="Sensor",
credential_subject=bytes.fromhex("deadbeef"),
manufacturer="0x3333333333333333333333333333333333333333",
tier=0, # 0 Entry, 1 Basic, 2 Pro
)
preview = client.preview_machine_activation(params)
print(preview.machine_id, preview.bond_amount, preview.voucher_credit, preview.net_peaq_amount)
result = client.activate_machine(params)
print(result.machine_id) # int, the permanent machine ID
print(result.net_peaq_amount) # PEAQ actually transferred
print(result.is_homed_locally) # True, confirmed on-chain
```
### What the call does
1. Validates every input locally.
2. Verifies the connected chain, that all seven Tokenomics 2.0 contracts have bytecode, that the peer addresses match `InfoDesk.peer(role)`, and that `MachineSubscription.fullMode()` is true.
3. Computes the machine ID and quotes the bond: `bond = requiredPeaqAmount(tier)`, `voucher = min(pendingVoucherCredit, bond)`, `net = bond - voucher`.
4. Resolves the PEAQ token from `InfoDesk.peaqToken()`, checks balance and allowance, and approves exactly `net` to `MachineSubscription` if the allowance is short.
5. Re-reads the quote, simulates, and submits the one `MachineStateAndSync.activateMachine` transaction.
6. Requires three correlated receipt events (`MachineOnboarded`, `MachineMinted`, `Activated`) and reconciles the resulting state (`ownerOf`, `controllerOf`, subscription tier and period, `isHomedLocally`) before returning.
The transaction sender becomes the machine's owner **and** bond payer. `manufacturer` is recorded on-chain and never verified by the contract. On peaq, PEAQ is the native-balance precompile at `0x…0809`, so bond and gas come out of the same balance: a wallet holding exactly the net amount still fails on gas.
A transaction whose receipt does not arrive in time is reported as pending (`PENDING_TRANSACTION` in the Python SDK, `RECEIPT_UNAVAILABLE` in the JS SDK, `PENDING` at exit 2 in the CLI) with its hash. It may still mine. Never submit a second activation for the same machine; reconcile the recorded hash instead (`reconcile_activation_transaction` in Python, re-running the same command in the CLI).
## Two ownership modes
| Mode | Who signs and pays | Controller | Use when |
| :--------------------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------- | :----------------------------------------------- |
| Self-owned | Your configured signer owns the machine, pays the bond, and is its DID controller. | Same address as the owner | One machine, one wallet. |
| Machine-owned, operator-controlled | The **machine** wallet signs, owns the NFT, and pays gas and the bond. | Your operator address, recorded in the DID document | Fleets where the operator manages many machines. |
Rights differ by role and both parties should know them:
| Who | Can | Cannot |
| :--------------------- | :-------------------------------------------------------------- | :----------------------------------------------- |
| Machine wallet (owner) | Transfer the NFT; rotate or clear the controller | |
| Operator (controller) | Lifecycle (suspend, resume), subscription renewals, DID updates | Transfer the NFT; rotate or clear the controller |
**Operator-sponsored onboarding has no 2.0 equivalent.** `MachineStateAndSync.activateMachine` makes `msg.sender` the owner and the payer, so there is no way for an operator to activate a machine it does not own. `registerFor` / `register_for` throw `SPONSORED_ACTIVATION_UNSUPPORTED` in Tokenomics mode. The supported fleet pattern is the machine-owned, operator-controlled mode above. Full guide: [Fleet onboarding](/peaqos/guides/proxy-operator-fleet).
## Confirm activation
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
state = client.get_machine_activation_state(machine_id)
activated = state.subscription.period_start != 0 # tier 0 is a valid tier, so never test the tier
```
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
peaqos machine status --json
```
Or open the machine on the [Machine Explorer](https://machines.peaq.xyz): `https://machines.peaq.xyz/machine/` for a machine activated under Economics 2.0, `https://machines.peaq.xyz/machine/0x` for a Tokenomics 1.0 or mirrored legacy machine. The explorer indexes from chain, so allow a few minutes after the transaction.
Do not test activation by querying `mcr.peaq.xyz/machine/did:peaq:0x…`: 2.0 machines are addressed by decimal ID, and the MCR queries in the SDK are still disabled in Tokenomics mode. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines).
## After activation
| Next step | Function | Status |
| :----------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- |
| Manage the machine: suspend, resume, renew, transfer, DID updates | [CLI: machine](/peaqos/cli#peaqos-machine), [SDK](/peaqos/sdk-reference/sdk-python#tokenomics-2-0) | Live |
| Build a credit rating from revenue and activity events | [Qualify](/peaqos/functions/qualify) | Live for 1.0 machines; 2.0 ratings served by `mcr-20.peaq.xyz` over HTTP since 2026-09-05, 2.0 event submission rolling out |
| Pair an AI agent with delegated spending and run market search | [Scale](/peaqos/functions/scale) | Live |
| Provision the machine as a compute provider and earn to its wallet | [Monetize](/peaqos/functions/monetize) | Live; 2.0 opt-in reaches `mcr-20.peaq.xyz` since 2026-09-05 |
| Get attested by the peaq Foundation or an OEM | [Verify](/peaqos/functions/verify) | Coming Soon |
| Fractionalize the machine for investor ownership | [Tokenize](/peaqos/functions/tokenize) | Coming Soon |
## Legacy flow (Tokenomics 1.0)
Machines onboarded before Economics 2.0 live in `IdentityRegistry` with a 1 PEAQ native bond and a separate `MachineNFT` token. The SDKs keep this path when you construct a client **without** `tokenomics20`: `registerMachine` / `register_machine` (self-managed) or `registerFor` / `register_for` (proxy), then `mintNft`, `tokenIdOf`, and `writeMachineDIDAttributes`. Each of these emits a deprecation warning and throws a typed error in Tokenomics mode. The 1.0 MCR API at `mcr.peaq.xyz` serves these machines by `did:peaq:0x`.
Legacy machines can be mirrored into Economics 2.0 through the `MachineMigrationHub`; that path is operated by peaq for partner fleets, not exposed in the SDKs. See [Economics 2.0: legacy machines](/peaqos/concepts/economics-2-0#legacy-machines).
## Concepts
Tiers, bonds, credits, grace and runoff, trust validators.
W3C DID, DID document, portable identity.
The ERC-721 whose token ID is the machine ID.
Addresses on peaq mainnet and agung.
2FA-gated faucet for initial machine gas.
Rating from a machine's history.
## SDK reference
* [`activateMachine` / `activate_machine`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): the one-transaction activation.
* [`previewMachineActivation` / `preview_machine_activation`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): quote without signing.
* [`getMachineActivationState` / `get_machine_activation_state`](/peaqos/sdk-reference/sdk-python#tokenomics-2-0): confirm the result.
* [`fundFromGasStation` / `fund_from_gas_station`](/peaqos/sdk-reference/sdk-js#fundfromgasstation): request gas for a fresh wallet.
* [`generateKeypair` / `generate_keypair`](/peaqos/sdk-reference/sdk-js#generatekeypair): create a machine keypair.
## Guides
Single machine, owner-operated.
Machine-owned, operator-controlled: N machines from one operator.
# Monetize
Source: https://docs.peaq.xyz/peaqos/functions/monetize
Put your machine to work: provision it as a compute provider and earn to its machine wallet.
Monetize is how a machine earns from its own spare capacity. A [registered, bonded](/peaqos/functions/activate) machine opts in, provisions itself as a compute provider, and reports that it is online, so compute networks can put it to work and pay it. Earnings land on the machine wallet, not the operator's.
Where [Scale](/peaqos/functions/scale) lets a machine *buy* services and [Stream](/peaqos/functions/stream) lets it sell *data*, Monetize lets it sell *capacity*: the machine's own processing power. Compute is the first supported capacity type; v1 ships with a worked flow for onboarding a machine to aggregator networks such as Akash.
**Status for Economics 2.0 machines (2026-09-05).** SDK 0.6.0 and CLI 0.0.8 moved the opt-in client to the 2.0 MCR server at `mcr-20.peaq.xyz`, which publishes the compatibility signal since 2026-09-05. Opt-in reads work against the mirrored 2.0 machines; writes were not exercised yet. Provisioning and the heartbeat are unchanged and work. For Tokenomics 1.0 machines, pin `@peaqos/peaq-os-sdk@0.5.0`, `peaq-os-sdk==0.5.0` (Python 3.11 or newer), or `peaq-os-cli<0.0.8` together with `peaq-os-sdk<0.6.0`, which talk to `mcr.peaq.xyz`. Details on the [opt-in reference](/peaqos/sdk-reference/monetization-opt-in).
## What ships
| Component | Description |
| :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Monetization opt-in | A separate, toggleable decision on top of activation. The machine's owner or DID controller (under Economics 2.0; the owner, machine wallet, or on-chain operator for Tokenomics 1.0 machines) signs an EIP-191 message and submits it to the MCR API. Opting in requires the machine to be activated, bonded, and not deactivated; opting out is always allowed. Anyone can read a machine's current state. Both SDKs wrap this as an [opt-in client](/peaqos/sdk-reference/monetization-opt-in), and the CLI exposes it as [`peaqos monetize`](/peaqos/cli#peaqos-monetize). |
| Provider provisioning | A schema-driven runner in both SDKs that turns a published provisioning manifest into an ordered, auditable install, entirely on the machine. Pre-flight checks gate the run, secrets are redacted end to end, sudo is explicit and scoped, and the node counts as live only when the manifest's verification probes pass. The CLI drives the whole flow with [`peaqos monetize provision`](/peaqos/cli#peaqos-monetize-provision). |
| Presence heartbeat | A machine-side client that signs and pushes a presence heartbeat to the peaqOS heartbeat service at a configurable interval. A valid heartbeat keeps the machine online; when heartbeats stop, the server marks it offline. The machine never self-reports offline. |
| Machine-wallet payout | The provisioning runner exposes the machine's own wallet as the default commission and payout address. Money earned by the machine routes to the machine, keeping its revenue history clean for its [credit rating](/peaqos/functions/qualify). |
## How it works
```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
flowchart LR
Reg["Activated + bonded
machine (Activate)"] --> OptIn["Monetization opt-in
(signed, toggleable)"]
OptIn --> Prov["Manifest provisioning
(runs on the machine)"]
Prov --> HB["Presence heartbeat
(to the peaqOS heartbeat service)"]
Prov --> Agg["Aggregator network
(e.g. Akash)"]
Agg -->|workloads| Prov
Agg -->|payment| Wallet["Machine wallet"]
```
1. **Opt in.** Monetization is a distinct decision, not part of activation. The owner or controller (owner, machine wallet, or operator for 1.0 machines) opts in through the [SDK](/peaqos/sdk-reference/monetization-opt-in), the [CLI](/peaqos/cli#peaqos-monetize) (`peaqos monetize opt-in`), or the raw [monetization API](/peaqos/api-reference/put-machine-monetization). The machine must be bonded first; opting out works at any time.
2. **Provision.** The machine pulls a versioned provisioning manifest from a public repo and runs it locally through the SDK's [provisioning runner](/peaqos/sdk-reference/provisioning) or the CLI's `peaqos monetize provision run`: pre-flight checks, ordered install steps in manual or auto mode, owner handoffs for things only a human can do (funding, DNS), and verification probes that prove the provider node is actually live.
3. **Report presence.** The machine starts the [heartbeat client](/peaqos/sdk-reference/heartbeat) and pushes a signed heartbeat at a regular interval. Presence is server-derived: online while heartbeats keep arriving, offline when they stop.
4. **Earn.** A provider node that passes verification is live on its aggregator network, which dispatches workloads and pays through its own rails. peaqOS supplies the machine wallet as the payout context, never the operator's, and the manifest maps it into the provider's commission field.
## v1 scope
Compute is the only supported capacity type in v1, and Akash is the first supported aggregator network. The machine-side flow (opt-in, provisioning, presence heartbeat) is what ships now. Aggregator-side discovery, connection management, per-job proofs, and automated settlement through peaqOS rails are rolling out next; see the [roadmap](/roadmap).
Treat v1 as an early release: it proves the machine-side flow end to end, and is not yet hardened for production scale.
## Build with Monetize
Toggle monetization on or off and read the state, from JavaScript and Python.
The manifest runner for JavaScript and Python: fetch, pre-flight, provision, verify.
The presence client for JavaScript and Python: start, stop, and query presence.
Opt in, provision, and verify a provider node from the terminal.
The wire contract: canonical message, freshness window, and error codes.
Where Monetize sits and what ships next.
## Related
* [Activate](/peaqos/functions/activate): the identity and bond Monetize builds on
* [Qualify](/peaqos/functions/qualify): the credit rating machine earnings feed into
* [Stream](/peaqos/functions/stream): sell the machine's data instead of its capacity
* [Wallets (OWS)](/peaqos/wallets)
# Qualify
Source: https://docs.peaq.xyz/peaqos/functions/qualify
Credit rate your machine from its revenue and activity history.
Qualify turns a machine's on-chain history into a [Machine Credit Rating (MCR)](/peaqos/concepts/machine-credit-rating): a Moody's-style letter rating that any protocol, agent, or frontend can query from any chain.
## What ships
| Component | Description |
| :------------------- | :---------------------------------------------------------------------------------------------------- |
| EventRegistry | On-chain store for revenue (type `0`) and activity (type `1`) events, with a cross-chain audit trail. |
| MCR scoring pipeline | Computes AAA-to-NR ratings from a bonded machine's event history. |
| MCR API | Public read API. Any chain, any caller, no auth. |
| SDK helpers | `submitEvent`, `validateSubmitEventParams`, `computeDataHash`, `queryMcr` on both JS and Python. |
## How it works
Each time a machine earns revenue or performs a trackable activity, the operator submits an event to the EventRegistry. The SDK validates the payload, computes a `keccak256` data hash, and writes the minimal on-chain record.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import {
PeaqosClient,
EVENT_TYPE_REVENUE,
TRUST_SELF_REPORTED,
SUPPORTED_CHAIN_IDS
} from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
const { txHash, dataHash } = await client.submitEvent({
machineId,
eventType: EVENT_TYPE_REVENUE,
value: 500, // $5.00 in cents
currency: "USD",
timestamp: Math.floor(Date.now() / 1000) - 10, // Must be after block time
rawData: new TextEncoder().encode(JSON.stringify({ session: "abc" })),
trustLevel: TRUST_SELF_REPORTED,
sourceChainId: SUPPORTED_CHAIN_IDS.peaq,
sourceTxHash: null,
metadata: new Uint8Array([]),
});
```
Full walkthrough: [Submit events](/peaqos/guides/submit-events).
A freshly registered machine is **Provisioned** until it has enough history to score. Events feed the scoring pipeline, which blends revenue trend, activity cadence, bond status, and trust level.
Any consumer (an agent, protocol, frontend, or another chain) can fetch the current rating from the public API.
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl ${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc...
```
Response includes the rating (AAA / AA / A / BBB / BB / B / NR / Provisioned), score, event counts, revenue trend, and bond status. Full walkthrough: [Query MCR](/peaqos/guides/query-mcr).
## Cross-chain revenue
Revenue earned on another chain (e.g., Base) is recorded on peaq with `sourceChainId` and `sourceTxHash` pointing to the origin transaction. Consumers can verify each event against its source chain.
See [Events](/peaqos/concepts/events#cross-chain-revenue-accounting) and [Trust levels](/peaqos/concepts/trust-levels).
**Multichain:** the MCR API stays canonical on peaq. Omni-chain V1 shipped `IdentityLite`, `DIDLite`, and `StakingLite` on supported chains (Agung and Base Sepolia at launch), so consumers can read machine identity and stake state locally. A dedicated MCR oracle on supported chains is reserved and follows V1. See the [Omni-chain concept](/peaqos/concepts/omni-chain) and the [roadmap](/roadmap).
## Concepts
AAA-to-NR scale, lifecycle from Provisioned to rated.
Revenue and activity records that feed MCR.
Self-reported, on-chain verifiable, hardware-signed.
## SDK reference
* [`submitEvent`](/peaqos/sdk-reference/sdk-js#submitevent): Write a single event to EventRegistry.
* [`validateSubmitEventParams`](/peaqos/sdk-reference/sdk-js#validatesubmiteventparams): Validate event params client-side.
* [`computeDataHash`](/peaqos/sdk-reference/sdk-js#computedatahash): keccak256 of raw event data.
* [`queryMcr`](/peaqos/sdk-reference/sdk-js#querymcr): Fetch a machine's rating from the MCR API.
## API reference
Rating, score, trend, bond status for a single machine.
Paginated operator fleet with per-machine MCR.
## Guides
Event types, validation, data hashing, cross-chain pattern.
Fetch ratings from curl, JS, or Python.
# Scale
Source: https://docs.peaq.xyz/peaqos/functions/scale
Delegate bounded authority to an AI agent so it can discover, buy, and consume services on your machine's behalf.
Scale pairs an AI agent to an activated, bonded machine and gives it delegated authority over the machine's wallet, bounded by spend limits and an allow/denylist. The agent discovers, buys, and consumes services through the [Machine Markets API](/peaqos/api-reference/machine-markets-overview). The agent is provisioned outside peaq; pairing, policy, discovery, ordering, payment, and execution happen through peaqOS.
It builds on the smart account each machine receives at [Activate](/peaqos/functions/activate) and the credit signal from [Qualify](/peaqos/functions/qualify). The machine's MCR and trust level carry through to providers when the agent buys.
## What ships
| Component | Description |
| :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Machine Agent pairing | Bind a third-party AI agent (Claude, OpenAI, Virtuals, Teneo, etc.) to a machine via a signed EIP-191 challenge. peaqOS verifies the signature, persists the agent's identity and verification metadata, and issues a signed HS256 session JWT (`pairingToken`) the agent presents as `x-agent-pairing-token` on market writes. Tokens rotate via a sessions endpoint. |
| Delegation policy | Per-transaction limit, daily spend limit, currency, and allowed/denied skill keys and service IDs. Enforced server-side on every market call. Policy changes invalidate the session token's `delegationPolicyHash`; rotate after a `PATCH`. |
| Skill registry and service catalogue | Curated capabilities (`oracle.price-feed`, `compute.marketplace`, `compute.confidential`, `storage.object`, `data.location`, `identity.proof-of-person`, `device.control`, `machine.commerce`, `network.partner-console`) and the concrete provider services that fulfil them. |
| Market search | `POST /market/search` takes machine context, requirements, region, budget, and execution preferences and returns ranked `MarketQuote`s with reasons. |
| Market orders | `POST /market/orders` locks a service from a quote into an order with state machine `created → payment_pending → ready → executing → delivered → confirmed` (or `disputed` / `cancelled` / `failed` / `handoff`). Server-side policy and spend checks fire on create. |
| Payment intent and escrow | `payment-intent` selects a rail; `payment-proof` records or RPC-verifies on-chain payment; `payment/escrow-lock`, `payment/release`, `payment/refund` cover the escrow lifecycle. Solana proofs are recorded; EVM proofs verify via RPC against the ERC-20 Transfer log. |
| Execute and runtime endpoints | `POST /orders/:orderId/execute` materialises the order as a task plus route, dispatches to a native provider or a machine-side runtime endpoint registered via `PUT /machines/:machineId/runtime-endpoints/:providerKey` or heartbeated by an on-machine runtime agent, and writes a `Run` and `Outcome`. |
| Confirm and dispute | `POST /confirm` closes the order and releases escrow; `POST /dispute` opens a dispute record and freezes payment. Pairings with non-terminal orders cannot be revoked (`OPEN_MARKET_ORDERS`). |
| Machine identity proof | DID-controller challenge and EIP-191 signature flow that ties orchestration writes to the on-chain machine identity. |
## How it works
```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
flowchart LR
Owner["Proxy Operator /
Machine Owner"] -->|provisions| Agent["Machine Agent
(third-party AI)"]
Owner -->|pairs + sets policy| Orch["Machine Markets API
peaqOS Orchestration"]
Agent -->|x-agent-pairing-token| Orch
Orch -->|verifies identity| MCR["peaqOS MCR
(on-chain truth)"]
Orch -->|ranked quotes| Agent
Agent -->|order, pay, execute, confirm| Orch
Orch -->|consume| Providers["Skill providers
(QVAC, agentic.market, pay.sh...)"]
```
1. **Activate the machine.** Run [Activate](/peaqos/functions/activate) to mint peaqID, Machine NFT, smart account, and post the bond. Pairing requires an active, bonded machine.
2. **Prove identity ownership.** `POST /machine-identity/challenges` returns a message that the DID-controller key signs (EIP-191). The signed proof is attached when the machine is registered with the orchestration service.
3. **Provision a Machine Agent.** Get an agent from any provider you trust: Claude, OpenAI, Virtuals, Teneo, your own runtime. peaq does not provide the agent.
4. **Pair the agent.** Pairing is challenge-based.
1. `POST /machines/:machineId/agent-pairings/challenges` with the agent's `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`. The response is an `AgentPairingChallenge` with `challengeId`, `message`, and `expiresAt`.
2. The Machine Agent signs `message` (EIP-191 `personal_sign`) with the wallet key behind `agentAddress`.
3. `POST /machines/:machineId/agent-pairings` with `agentProof: { challengeId, signature }` and the delegation policy. The orchestrator verifies the signature, persists the pairing with `verification` metadata, and returns the `AgentPairing` with a signed session JWT in `pairingToken`. Store it client-side and send it as `x-agent-pairing-token` on market writes.
4. Tokens expire (default 1 hour). Rotate by issuing a fresh challenge and calling `POST /machines/:machineId/agent-pairings/:pairingId/sessions` with the new proof. Rotate after any `PATCH` to the delegation policy.
5. **Search the market.** The agent calls `POST /market/search` with machine context, service type, capabilities, region, budget, and optional `providerCredentials`. The orchestrator returns ranked `MarketQuote`s with reasons for each ranking.
6. **Place an order.** `POST /market/orders` with `machineId`, `agentPairingId`, `serviceId`, and optional `searchId` + `quoteId` creates an order (status `created`). The orchestrator enforces the delegation policy and per-transaction and daily spend limits at create. The recommended payment rail is copied from the service into `order.payment`.
7. **Settle payment.** `POST /market/orders/:orderId/payment-intent` mints a payment record on the chosen rail. For wallet-based rails, submit `POST /payment-proof` after the on-chain transfer (EVM proofs are RPC-verified against the ERC-20 Transfer log). For the x402 rail there is no buyer transfer — the agent wallet signs the provider's payment challenge and the signed header is the proof; the provider is paid during execute. For escrow rails, call `POST /payment/escrow-lock` with the on-chain lock transaction. Once funds clear, the order moves to `ready`.
8. **Execute.** `POST /market/orders/:orderId/execute` materialises the order as a task plus route, dispatches to a native provider or a machine-side runtime endpoint, and writes a `Run` plus an `Outcome`. Successful runs land in `delivered`. External-handoff services return a structured handoff and land in `handoff`.
9. **Confirm or dispute.** `POST /market/orders/:orderId/confirm` closes the order and triggers `payment/release` for held funds. If the service did not deliver, `POST /market/orders/:orderId/dispute` opens a dispute and freezes payment until peaq resolves it.
### Read-only shortcut
Free, read-only operations can skip the order flow entirely. `POST /market/services/:serviceId/execute` runs the operation directly with `machineId`, `agentPairingId`, `operation`, and optional `input` / `providerCredentials`. The orchestrator dispatches through the same task + route + run + outcome machinery as `executeMarketOrder` but does not create an order, payment intent, or escrow record. Paid, external-handoff, and state-changing operations still go through `POST /market/orders`.
## Payment model
peaqOS does not collect marketplace payments. The payment-intent and payment-proof endpoints record provider/service payment state only when the selected adapter requires direct provider payment. Operators or machines pay the selected provider directly through that provider's supported rail. Raw payment headers are never stored.
## Payment rails
The orchestrator quotes services in the rail the provider supports. Active rails:
| Rail | Where it shows up |
| :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x402` | [agentic.market](https://agentic.market) services and most [pay.sh](https://pay.sh) services. Agent wallet responds to an HTTP 402 payment-required challenge; orchestrator replays the request with the agent's payment headers via the `paidHttp` adapter. |
| `mpp` | Solana micro-payment protocol. Used by some pay.sh services on Solana. |
| `vault-stripe` / `external` | Card-mediated or external handoff. |
| `wallet` / `wdk-usdt-transfer` | Direct USDT transfer on peaq (default token `USDT`, decimals from `PEAQOS_USDT_DECIMALS`). |
| `onchain-escrow` / `escrow` | Funds locked into a service-specified escrow contract via `payment/escrow-lock`. |
| `not-required` | Free or pre-authorised services. Order proceeds straight to `ready`. |
| `offchain-record` | Off-chain attestation, recorded but not RPC-verified. |
The orchestrator handles cross-chain conversion when a service quotes in a chain or token the agent does not natively hold.
## Service providers
**At launch:**
* **QVAC** private inference (Tether strategic, co-announced).
* The **[agentic.market](https://agentic.market)** suite over x402: Claude, ChatGPT, 2Captcha, Firecrawl, Wolfram|Alpha, Exa.
* The **[pay.sh](https://pay.sh)** suite: Gemini, BigQuery, Document AI, StableUpload, Cloud Translation.
**Added since launch:**
* **Akash** — decentralized compute.
* **Aethir**: decentralized GPU compute.
* **GEODNET** — RTK precision-positioning data.
* **Arcium**: confidential compute (`compute.confidential`).
* **Acurast** — decentralized compute; its Deploy Agent runs as a native x402 provider runtime in the orchestrator.
* **Naver Maps** — mapping and geolocation APIs.
* **Korea Public Data** (Data.go.kr): EV charging stations and chargers, next-hour weather, and AirKorea PM2.5. Needs a portal service key, sent with every request and never stored.
* **World ID**: proof of personhood (`identity.proof-of-person`).
* **Walrus**: object storage.
* **Home Assistant**: device control on the machine's local network.
* **Aave**: treasury deposits.
* **peaq Telemetry Export**: fleet telemetry export and machine data publishing.
The live catalogue is on [robotic.sh](https://robotic.sh); query it programmatically via [`GET /market/services` and market search](/peaqos/api-reference/machine-markets-discovery). The adapter registry (`GET /market/adapter-credential-stack`) is the authoritative list of what needs credentials or setup.
## Delegation policy
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
type DelegationPolicy = {
allowedSkillKeys: string[]; // e.g. ["storage.object", "compute.marketplace"]
deniedSkillKeys: string[];
allowedServiceIds: string[]; // exact service IDs from /market/services
deniedServiceIds: string[];
perTransactionLimit?: number | null;
dailySpendLimit?: number | null;
currency?: string | null; // ISO 4217 or token symbol
};
```
Policy is enforced server-side on every call. Update it at any time via `PATCH /machines/:machineId/agent-pairings/:pairingId`. Revoking a pairing (`DELETE`) terminates the agent's delegated access immediately, unless the pairing has open market orders (`OPEN_MARKET_ORDERS`).
## Multichain
peaqID, Machine NFT, and the Service Registry stay canonical on peaq. Smart accounts and Machine NFTs deploy to supported chains (Agung and Base Sepolia at launch). Machine Agents pay across chains using the chain and token a service quotes in.
A six-daemon Signer Daemon fleet watches finalized peaq and Agung events, packages them into EIP-712 batches, and pushes them to per-chain Lite contracts (`DIDLite`, `IdentityLite`, `StakingLite`). Read full architecture in the [Omni-chain concept](/peaqos/concepts/omni-chain). See the [roadmap](/roadmap) for chain rollout.
## Build with Scale
Base path, access model, error codes, and the full HTTP endpoint surface.
How pairing, skills, services, orders, payments, and execution hang together.
`peaqos scale agent pair`, `search`, and the `order` family from your terminal.
`client.orchestration` for [JS](/peaqos/sdk-reference/orchestration-js) and [Python](/peaqos/sdk-reference/orchestration-py).
## Related
* [Machine Markets concept](/peaqos/concepts/machine-markets)
* [Activate](/peaqos/functions/activate)
* [Qualify](/peaqos/functions/qualify)
* [Wallets (OWS)](/peaqos/wallets)
# Stream
Source: https://docs.peaq.xyz/peaqos/functions/stream
Sell the data your machine generates: signed, encrypted, and verifiable, with buyers paying for access to exactly what they need.
Stream is the *data* side of the machine economy. A machine signs the data it generates, encrypts whatever is sensitive, and sells access to it, so robots and devices earn from the one thing they produce constantly: machine data.
Where [Scale](/peaqos/functions/scale) lets a machine *buy* services and [Monetize](/peaqos/functions/monetize) lets it sell its *capacity*, Stream lets it sell **data**. It builds on the identity a machine receives at [Activate](/peaqos/functions/activate) — peaqID, Machine NFT, and a cross-chain wallet — and uses that identity to prove which machine produced a given data package.
## What ships
| Component | Description |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| peaqOS Edge Agent | A persistent on-machine process, installed as a ROS 2 node, that signs, encrypts, and ships the data the machine generates. It provisions identity from [Activate](/peaqos/functions/activate) and runs the signing pipeline automatically once configured. |
| Data Event Map | The policy file that controls what streams out: topic subscriptions, per-field rules (`include`, `exclude`, `encrypt`, `anonymize`), signing policy, and output destinations. |
| Signed data packages | Every package carries the machine's peaqID, a timestamp, a schema version, and a sequence number, signed with the machine's key. Anyone verifies it with the public key derived from the DID. Field rules run before signing, so protected fields never leave in the clear while the package stays verifiable. |
| Chunking + encryption | Data is grouped into bounded chunks, each encrypted under its own key and linked into a tamper-evident chain. Chunks roll up into datasets with a Merkle root. |
| Data marketplace | Owners list datasets; buyers verify signatures and hashes, pay over a transfer or x402 rail, and receive an access grant — the purchased chunk keys re-wrapped to their public key. The backend stores manifests and grants, never plaintext. |
| Distribution | Signed, encrypted chunks leave over the owner's chosen vector: a direct machine-to-machine [P2P transfer](/peaqos/sdk-reference/stream-distribution#p2p-delivery) (`peaqos-p2p`), S3-compatible cloud storage, Walrus, or a direct API. |
## How it works
```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
flowchart LR
ROS["ROS 2 topics /
sensors / feeds"] -->|raw data| Agent["peaqOS Edge Agent
(on the machine)"]
Owner["Machine Owner"] -->|Data Event Map| Agent
Agent -->|sign + encrypt
+ chunk| Out["Signed, encrypted
chunks + manifests"]
Out --> Dist["P2P feed / cloud /
Walrus / direct API"]
Dist --> Buyer["Context Provider
(buyer)"]
Buyer -->|verify + pay| Backend["peaqOS backend
(manifests, grants)"]
Backend -->|access grant| Buyer
```
1. **Install and configure.** The Machine Owner installs the Edge Agent as a [ROS 2 package](/peaqos/sdk-reference/ros2/stream-agent) and writes a Data Event Map: which topics to read, which fields to keep, drop, or encrypt, and where signed data goes.
2. **Sign and encrypt.** As data is captured, field rules run first, then the package is signed with the machine's key and grouped into encrypted chunks.
3. **List and sell.** Chunks roll up into datasets the owner lists. Buyers verify the machine's signatures and chunk hashes before paying.
4. **Grant and deliver.** After payment, an access grant re-wraps the purchased chunk keys to the buyer's key. The buyer decrypts only what they bought.
See [Data streams](/peaqos/concepts/data-streams) for the full trust model.
## Sell across chains
Buyers pay on the chain a listing quotes in — the transfer rail covers peaq, Base, and Solana, and the x402 rail covers signed pay-per-request authorizations — while the sale record and the machine's identity stay canonical on peaq. Broader omnichain settlement is still rolling out — see the [roadmap](/roadmap).
## Build with Stream
The trust model: signing, chunk chains, encryption, datasets, and buyer access grants.
The `stream` module for JavaScript and Python.
Purchases, payment rails, and delivery — buyer access over S3, or encrypted chunks streamed machine-to-machine over P2P.
Run the on-machine node that signs, encrypts, chunks, and streams your data.
Publish signed, encrypted chunks and grant buyer access from the terminal.
The buyer side: discover listings, order, pay, and download as a Context Provider.
Where Stream sits and what ships next.
## Related
* [Data streams concept](/peaqos/concepts/data-streams)
* [Activate](/peaqos/functions/activate)
* [Monetize](/peaqos/functions/monetize)
* [Wallets (OWS)](/peaqos/wallets)
# Tokenize
Source: https://docs.peaq.xyz/peaqos/functions/tokenize
Fractionalize your machine into an investable asset via ERC-3643.
**Tokenize is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap).
Tokenize lets machine owners fractionalize a machine into a tradeable, regulatory-compliant security token, turning machine revenue into an investable asset for third parties.
The underlying asset is the [Machine NFT](/peaqos/concepts/machine-nft) minted at [Activate](/peaqos/functions/activate). Tokenize wraps that NFT in an ERC-3643 share class so revenue and activity tracked by [Qualify](/peaqos/functions/qualify) flow back to token holders.
# Verify
Source: https://docs.peaq.xyz/peaqos/functions/verify
Prove your machine is real via hardware attestation and trusted third parties.
**Verify is coming soon.** Details on this page are preliminary and may change. Track progress on the [roadmap](/roadmap).
Verify is the attestation layer for peaqOS. It lets manufacturers, labs, and the peaq Foundation co-sign a machine's identity, producing a portable signal that the machine exists and is who it claims to be.
Today, [trust levels](/peaqos/concepts/trust-levels) are self-attested when an event is submitted. `Hardware-signed = 2` is a valid value but there's no third-party attestation infrastructure behind it yet. Verify attests the **machine** (not individual events) and is what closes that gap.
## What Verify v1 will be
The first release is specified and its contract is written; nothing is deployed and no SDK or API surface exists yet.
| Piece | What it does |
| :------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Attestation registry | An on-chain registry of M-of-N attester quorums. For each subject and topic it answers one of four states: `unverified`, `verified`, `expired`, `revoked`. Revocation dominates expiry. There is no aggregate "verified" boolean across topics. |
| Two topics | **KYB** on the machine's current operator address, and **chip** on the machine ID. Each is reported independently. |
| Chip proof | Hardware attestation against one certified chip family in v1: an Infineon OPTIGA Trust M v3 secure element whose leaf certificate chains to the Infineon CA. No TPM, software key, or other secure element is accepted in v1. |
| Authority split | The SDK validates chip evidence locally and reads attestation state through the peaqOS API; it never touches the chip, submits evidence, or writes the registry. A verifier backend issues challenges, checks evidence, and anchors the result. |
Contract addresses, endpoint paths, and SDK methods will appear here when they ship.
# Fleet onboarding (operator-controlled)
Source: https://docs.peaq.xyz/peaqos/guides/proxy-operator-fleet
Activate and manage many machines from one operator under Economics 2.0: each machine owns its NFT and pays its bond, the operator is its DID controller.
One operator, many machines. Under [Economics 2.0](/peaqos/concepts/economics-2-0) the account that signs `activateMachine` becomes the machine's owner and pays its bond, so a fleet is built the other way round from Tokenomics 1.0: each **machine** signs its own activation with its own key, and your operator address is recorded as the machine's DID **controller**. The operator then runs lifecycle, renewals, and DID updates for the whole fleet.
**Operator-sponsored registration is gone.** `registerFor` / `register_for` (the 1.0 proxy pattern where the operator paid and owned every identity) has no Economics 2.0 equivalent: `MachineStateAndSync.activateMachine` makes `msg.sender` both owner and payer, and the SDKs throw `SPONSORED_ACTIVATION_UNSUPPORTED` in Tokenomics mode. If a machine cannot hold a key and funds, there is no fallback; do not make the operator the owner instead, because the owner is the account that can transfer the NFT.
## Rights in the machine-owned, operator-controlled model
| Who | Can | Cannot |
| :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
| Machine wallet (owner) | Transfer the NFT; rotate or clear the controller; everything the controller can | |
| Operator (controller) | Suspend and resume; renew the subscription (paying from the operator wallet); update verification methods, authentication, and service endpoints | Transfer the NFT; rotate or clear the controller |
Credits from renewals (bonding reward and voucher credit) land on the **owner**, even when the controller pays. Both parties should know this before onboarding.
## Prerequisites
* Node.js ≥ 22 with `@peaqos/peaq-os-sdk` 0.7.0+, or Python 3.10+ with `peaq-os-sdk` 0.7.1+, or `peaq-os-cli` 0.0.8+ on `peaq-os-sdk` 0.7.1+
* An operator wallet for gas (it signs nothing during activation, but signs later renewals and DID updates)
* Each machine wallet funded with its tier bond plus gas. The bond is quoted per tier at the oracle rate; preview one activation first and fund every machine to that amount plus a gas margin. The operator can fund machine wallets from the [Gas Station](/peaqos/concepts/gas-station) (2FA-gated) for gas and top up the bond from any PEAQ-holding address.
* Environment variables configured per the [install guide](/peaqos/install)
JS examples load `.env` via `import "dotenv/config"`. Python's `from_env()` reads from the shell, so export the file first with `set -a && source .env && set +a` or call `load_dotenv()`.
## From the terminal
The CLI does one machine per command. `--for` and `--machine-key` switch to machine-owned mode: the machine key signs and pays, your `PEAQOS_PRIVATE_KEY` address becomes controller.
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
peaqos activate \
--for 0xMachineAddress --machine-key ./machine.key \
--machine-type Sensor --credential-subject-hex 0x \
--manufacturer 0x3333333333333333333333333333333333333333 \
--tier entry --did-document ./did.json --dry-run # drop --dry-run to submit
```
Loop it over your key files with `--json --yes` for scripts. Afterwards the operator manages every machine with `peaqos machine ...` (status, suspend, resume, subscription renew, DID setters). See [CLI: activate](/peaqos/cli#peaqos-activate) and [CLI: machine](/peaqos/cli#peaqos-machine).
## Single machine, from code
Used for funding, previews, and later management. It never signs an activation.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const operator = PeaqosClient.fromEnv();
console.log("Operator:", operator.address);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from dotenv import load_dotenv
from peaq_os_sdk import PeaqosClient
load_dotenv()
operator = PeaqosClient.from_env()
print("Operator:", operator.address)
```
Each machine needs its own address. Store the key on the device or in a secrets vault: it is the owner key of that machine's NFT.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machineKey = PeaqosClient.generateKeypair();
console.log("Machine address:", machineKey.address);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
machine_address, machine_private_key = PeaqosClient.generate_keypair()
print("Machine address:", machine_address)
```
Gas from the Gas Station (2FA on the operator wallet), the bond from any PEAQ-holding address.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const FAUCET_URL = "https://depinstation.peaq.xyz";
await operator.fundFromGasStation(
{ ownerAddress: operator.address, targetWalletAddress: machineKey.address, chainId: "peaq", twoFactorCode: "654321" },
FAUCET_URL,
);
// Then transfer the previewed netPeaqAmount (plus a margin) to machineKey.address from your treasury wallet.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
FAUCET_URL = "https://depinstation.peaq.xyz"
operator.fund_from_gas_station(
owner_address=operator.address, target_wallet_address=machine_address, chain_id="peaq",
two_factor_code="654321", faucet_base_url=FAUCET_URL,
)
# Then transfer the previewed net_peaq_amount (plus a margin) to machine_address from your treasury wallet.
```
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machine = new PeaqosClient<"tokenomics20">({
rpcUrl: operator.rpcUrl,
privateKey: machineKey.privateKey,
contracts: operator.contracts,
tokenomics20: { deploymentId: "peaq-mainnet" },
});
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import Tokenomics20Config
machine = PeaqosClient(
rpc_url=operator.rpc_url,
private_key=machine_private_key,
identity_registry=operator.contracts.identity_registry,
identity_staking=operator.contracts.identity_staking,
event_registry=operator.contracts.event_registry,
machine_nft=operator.contracts.machine_nft,
did_registry=operator.contracts.did_registry,
batch_precompile=operator.contracts.batch_precompile,
tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"),
)
```
`controller` is the operator address. The machine signs, owns the NFT, and pays the bond.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const params = {
controller: operator.address,
verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: operator.address, publicKeyMultibase: "z6Mk..." }],
authentication: [0n],
serviceEndpoints: [{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" }],
machineType: "Sensor",
credentialSubject: "0x",
manufacturer: "0x3333333333333333333333333333333333333333",
tier: 0,
};
const preview = await machine.previewMachineActivation(params);
const result = await machine.activateMachine(params);
console.log("machine", result.machineId.toString(), "owner", result.owner, "controller", result.controller);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import ActivateMachineParams, ServiceEndpointInput, VerificationMethodInput
params = ActivateMachineParams(
controller=operator.address,
verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=operator.address, public_key_multibase="z6Mk..."),),
authentication=(0,),
service_endpoints=(ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"),),
machine_type="Sensor",
credential_subject=bytes.fromhex(""),
manufacturer="0x3333333333333333333333333333333333333333",
tier=0,
)
preview = machine.preview_machine_activation(params)
result = machine.activate_machine(params)
print("machine", result.machine_id, "owner", result.owner, "controller", result.controller)
```
From here the operator client (constructed with the same `tokenomics20` deployment) runs the fleet. Renewals paid by the operator credit the owner.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const ops = new PeaqosClient<"tokenomics20">({ ...operatorConfig, tokenomics20: { deploymentId: "peaq-mainnet" } });
const state = await ops.getMachineManagementState(result.machineId);
await ops.suspendMachine(result.machineId);
await ops.resumeMachine(result.machineId);
const renewal = await ops.previewMachineRenewal({ machineId: result.machineId, payment: "peaq" });
await ops.renewMachine({ machineId: result.machineId, payment: "peaq", maxNetPeaqAmount: renewal.maxNetPeaqAmount });
await ops.setMachineServiceEndpoints(result.machineId, [
{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs/v2" },
]);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import PreviewMachineRenewalParams, RenewMachineParams
ops = PeaqosClient(..., tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet")) # operator key
state = ops.get_machine_management_state(result.machine_id)
ops.suspend_machine(result.machine_id)
ops.resume_machine(result.machine_id)
renewal = ops.preview_machine_renewal(PreviewMachineRenewalParams(result.machine_id, payment="peaq"))
ops.renew_machine(RenewMachineParams(result.machine_id, payment="peaq", max_net_peaq_amount=renewal.max_net_peaq_amount))
ops.set_machine_service_endpoints(result.machine_id, (
ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs/v2"),
))
```
## Batch activation
Activate machines sequentially; each iteration builds a machine client from that machine's key and records the operator as controller. Persist every returned machine ID: there is no on-chain fleet index in 2.0 yet (the 1.0 `machines` DID attribute and `GET /operator/{did}/machines` do not cover 2.0 machines).
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const operator = PeaqosClient.fromEnv();
const keys = loadMachineKeys(); // [{ address, privateKey, credentialSubject }], funded with bond + gas
const fleet: { address: string; machineId: string }[] = [];
for (const [i, k] of keys.entries()) {
const machine = new PeaqosClient<"tokenomics20">({
rpcUrl: operator.rpcUrl, privateKey: k.privateKey, contracts: operator.contracts,
tokenomics20: { deploymentId: "peaq-mainnet" },
});
const params = {
controller: operator.address,
verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: operator.address, publicKeyMultibase: "z6Mk..." }],
authentication: [0n],
serviceEndpoints: [],
machineType: "Sensor",
credentialSubject: k.credentialSubject,
manufacturer: "0x3333333333333333333333333333333333333333",
tier: 0,
};
try {
const result = await machine.activateMachine(params);
fleet.push({ address: k.address, machineId: result.machineId.toString() }); // decimal string at the JSON boundary
console.log(`[${i + 1}/${keys.length}] activated ${k.address} as ${result.machineId}`);
} catch (err) {
console.error(`[${i + 1}/${keys.length}] failed for ${k.address}:`, err);
// A RECEIPT_UNAVAILABLE here means the tx may still mine. Never re-run activation for that machine; check getMachineActivationState.
}
}
// Persist `fleet` to a file or database. It is your fleet index.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import ActivateMachineParams, PeaqosClient, Tokenomics20Config, VerificationMethodInput
operator = PeaqosClient.from_env()
keys = load_machine_keys() # [(address, private_key, credential_subject_bytes)], funded with bond + gas
fleet = []
for i, (address, key, anchor) in enumerate(keys):
machine = PeaqosClient(
rpc_url=operator.rpc_url, private_key=key,
identity_registry=operator.contracts.identity_registry, identity_staking=operator.contracts.identity_staking,
event_registry=operator.contracts.event_registry, machine_nft=operator.contracts.machine_nft,
did_registry=operator.contracts.did_registry, batch_precompile=operator.contracts.batch_precompile,
tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"),
)
params = ActivateMachineParams(
controller=operator.address,
verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=operator.address, public_key_multibase="z6Mk..."),),
authentication=(0,), service_endpoints=(),
machine_type="Sensor", credential_subject=anchor,
manufacturer="0x3333333333333333333333333333333333333333", tier=0,
)
try:
result = machine.activate_machine(params)
fleet.append({"address": address, "machine_id": str(result.machine_id)}) # decimal string at the JSON boundary
print(f"[{i + 1}/{len(keys)}] activated {address} as {result.machine_id}")
except Exception as err:
print(f"[{i + 1}/{len(keys)}] failed for {address}: {err}")
# A PENDING_TRANSACTION here means the tx may still mine. Never re-run activation for that machine; reconcile it.
# Persist `fleet` to a file or database. It is your fleet index.
```
## Key management
| Approach | How it works | Trade-offs |
| :-------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- |
| Per-machine key on the device | The device holds its owner key and signs its own activation and, later, its own transactions. | Strongest isolation. Required if the device should ever transfer its NFT or rotate its controller. |
| Per-machine key in the operator's vault | The operator generates and stores one key per machine (HSM, secrets manager) and signs activations from a provisioning host. | The machine still owns its NFT on chain; operationally the operator controls both roles. A vault leak exposes the fleet. |
A single shared key for every machine is not possible under 2.0: each machine ID maps to one owner, and a wallet may own many machines, but the machine's DID controller and owner are what the SDK checks. Use `setMachineApprovalForAll` / `peaqos machine approve-all` if a second address must be able to transfer the fleet's NFTs.
## Fleet queries
The 1.0 fleet listing (`GET /operator/{did}/machines`, built on the proxy's `machines` DID attribute) does not know 2.0 machines. Read 2.0 machines by ID from your own index:
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
for m in fleet:
state = ops.get_machine_management_state(int(m["machine_id"]))
print(m["machine_id"], state.subscription.status, state.is_available, state.is_relocating)
```
## Error handling
| Error | Cause | Resolution |
| :-------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | :----------------------------------------------------------------- |
| `TokenomicsUnsupportedError` `SPONSORED_ACTIVATION_UNSUPPORTED` | `registerFor` / `register_for` called in Tokenomics mode | Activate with the machine key and set the operator as `controller` |
| `TokenomicsActivationError` `INSUFFICIENT_BALANCE` | The machine wallet does not hold the net PEAQ plus gas | Fund the machine wallet, not the operator's |
| `TokenomicsActivationError` `NOT_OWNER_OR_CONTROLLER` | Operator tried a management write on a machine that does not list it as controller | Have the owner call `setMachineController` |
| `TokenomicsActivationError` `NOT_MACHINE_OWNER` | Operator tried `setMachineController`, `transferMachine`, or a clear-controller | Owner-only operations; sign with the machine key |
| `TokenomicsPendingTransactionError` `PENDING_TRANSACTION` (Python) / `TokenomicsActivationError` `RECEIPT_UNAVAILABLE` (JS) | Receipt did not arrive in time | Never resubmit; reconcile the recorded hash |
Full code tables on [errors](/peaqos/sdk-reference/errors#tokenomics-2-0-errors).
## Fleets onboarded before Economics 2.0
Machines registered with `registerFor` / `register_for` before 2026-09-01 are Tokenomics 1.0 machines owned by the proxy. They keep working on a client constructed without `tokenomics20` and through the 1.0 MCR API (`GET /operator/{did}/machines`). peaq mirrors partner fleets into Economics 2.0 through the `MachineMigrationHub`; see [Economics 2.0: legacy machines](/peaqos/concepts/economics-2-0#legacy-machines).
# Query Machine Credit Rating
Source: https://docs.peaq.xyz/peaqos/guides/query-mcr
Fetch a machine's MCR from the peaqOS MCR API. Covers curl, JavaScript, and Python.
The MCR API returns a machine's credit rating along with event counts, revenue trend, and bond status. It's a public read API. No authentication required.
## Endpoint
```
GET {PEAQOS_MCR_API_URL}/mcr/{did}
```
`{did}` accepts either a full DID (`did:peaq:0xabc...`) or a raw EVM address (`0xabc...`). This is the Tokenomics 1.0 MCR API; machines activated under [Economics 2.0](/peaqos/concepts/economics-2-0) (decimal machine IDs) are served by the 2.0 server at `https://mcr-20.peaq.xyz` (`GET /mcr/did:peaq:`, since 2026-09-05), and the SDK query helpers are disabled in Tokenomics mode. See [API reference](/peaqos/api-reference/overview#tokenomics-2-0-machines). Set `PEAQOS_MCR_API_URL` to the root of the MCR API server. The public peaq-hosted MCR is at `https://mcr.peaq.xyz`. Self-hosted deployments default to `http://127.0.0.1:8000`.
## Fetch the MCR
```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl -s "${PEAQOS_MCR_API_URL}/mcr/did:peaq:0xabc1230000000000000000000000000000000001"
```
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const did = "did:peaq:0xabc1230000000000000000000000000000000001";
const response = await fetch(
`${PEAQOS_MCR_API_URL}/mcr/${encodeURIComponent(did)}`
);
if (!response.ok) {
throw new Error(`MCR API returned ${response.status}`);
}
const mcr = await response.json();
console.log("Rating:", mcr.mcr);
console.log("Score:", mcr.mcr_score);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import os
import requests
did = "did:peaq:0xabc1230000000000000000000000000000000001"
response = requests.get(
f"{os.environ['PEAQOS_MCR_API_URL']}/mcr/{did}"
)
response.raise_for_status()
mcr = response.json()
print("Rating:", mcr["mcr"])
print("Score:", mcr["mcr_score"])
```
## Response shape
```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
"did": "did:peaq:0xabc1230000000000000000000000000000000001",
"machine_id": 1,
"mcr_score": 45,
"mcr": "BB",
"mcr_degraded": false,
"bond_status": "bonded",
"negative_flag": false,
"event_count": 12,
"revenue_event_count": 7,
"activity_event_count": 5,
"revenue_trend": "stable",
"total_revenue": 35000,
"average_revenue_per_event": 5000.0,
"last_updated": 1711900000
}
```
`total_revenue` and `average_revenue_per_event` are USD cents. Divide by 100 for display ($350 and $50 in this example).
A newly registered machine that hasn't accumulated enough history returns `mcr: "Provisioned"` with `mcr_score: 0`. Unbonded machines return `mcr: "NR"` with `mcr_score: 0`. See [GET /mcr/\{did}](/peaqos/api-reference/get-mcr) for the full field reference.
## Query an operator's fleet
Use the operator endpoint to list all machines registered under a proxy operator, paginated with MCR scores per machine.
```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl -s "${PEAQOS_MCR_API_URL}/operator/did:peaq:0xProxyAddress/machines?offset=0&limit=20"
```
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const operatorDid = "did:peaq:0xProxyAddress";
const response = await fetch(
`${PEAQOS_MCR_API_URL}/operator/${encodeURIComponent(operatorDid)}/machines?offset=0&limit=20`
);
const { machines } = await response.json();
for (const m of machines) {
console.log(`Machine ${m.machine_id}: ${m.mcr}`);
}
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import os
import requests
operator_did = "did:peaq:0xProxyAddress"
response = requests.get(
f"{os.environ['PEAQOS_MCR_API_URL']}/operator/{operator_did}/machines",
params={"offset": 0, "limit": 20},
)
response.raise_for_status()
for m in response.json()["machines"]:
print(f"Machine {m['machine_id']}: {m['mcr']}")
```
See [GET /operator/\{did}/machines](/peaqos/api-reference/get-operator-machines) for the full response shape and pagination details.
## Caching
The server applies a 1-hour TTL on MCR responses by default, configurable via the `MCR_CACHE_TTL` env var (`0` to disable). Repeat requests within the window return cached values. The `last_updated` field tells you when the underlying events were most recently added.
## Error handling
The MCR API returns standard HTTP status codes: `404` when the DID is unregistered, `503` when the chain is unavailable, `400` for malformed inputs. Bodies are JSON with a `detail` field carrying the upstream message.
If you call through the SDK (`queryMcr(client, did)` in JS / `query_mcr(client, did)` in Python), HTTP failures surface as `RuntimeError` (JS) or `ApiError` (Python). The `code` attribute carries `NOT_FOUND`, `SERVICE_UNAVAILABLE`, `SERVER_ERROR`, `TIMEOUT`, etc. See [SDK errors reference](/peaqos/sdk-reference/errors) for the full code map.
## Next steps
* [GET /mcr/\{did}](/peaqos/api-reference/get-mcr): full API reference for this endpoint
* [GET /machine/\{did}](/peaqos/api-reference/get-machine): full machine profile
* [Machine Credit Rating concept](/peaqos/concepts/machine-credit-rating): rating scale and lifecycle
# ROS 2 machine runtime
Source: https://docs.peaq.xyz/peaqos/guides/ros2-machine-runtime
Run peaqOS machine onboarding, MCR, events, smart accounts, and Machine NFT bridge flows from ROS 2.
This guide is for teams that use ROS 2 as the robot control plane and want peaqOS as the machine identity, credit, and asset layer.
The ROS 2 node wraps the same peaqOS capabilities documented in the [JavaScript SDK](/peaqos/sdk-reference/sdk-js) and [Python SDK](/peaqos/sdk-reference/sdk-python), but changes the security boundary: callers pass EVM addresses over ROS, while signing keys stay in a local registry file.
## Prerequisites
* ROS 2 Jazzy on a native host, or the repository Docker image with ROS 2 Humble
* A peaq EVM RPC endpoint
* `https://mcr.peaq.xyz` for MCR reads
* `https://depinstation.peaq.xyz` for Gas Station flows
* A funded peaq EVM signer for registration, minting, events, smart accounts, and bridge transactions
* Base ETH on the signer only if you need Base to peaq bridge operations
## Configure
Create a local config:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
cp peaq_ros2_examples/config/peaq_robot.example.yaml \
peaq_ros2_examples/config/peaq_robot.yaml
```
Set the peaqOS section:
```yaml theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
peaq_os:
enabled: true
rpc_url: "https://quicknode1.peaq.xyz"
api_url: "https://mcr.peaq.xyz"
faucet:
base_url: "https://depinstation.peaq.xyz"
qr_format: "svg"
wallet_registry:
path: "~/.peaq_robot/peaqos_wallets.json"
```
Do not commit `peaq_robot.yaml` after adding real addresses, local registry paths, or operational credentials.
## Build and start
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
source /opt/ros/jazzy/setup.bash
# In the Docker image, use: source /opt/ros/humble/setup.bash
python3 -m pip install -r requirements.txt
colcon build --packages-select peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples
source install/setup.bash
ros2 run peaq_ros2_peaqos peaqos_node --ros-args \
-p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml
```
Open another terminal, source the same ROS environment, then call services.
## 1. Create a local machine wallet
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/wallet/create \
peaq_ros2_interfaces/srv/PeaqosCreateWallet \
"{label: 'robot-001'}"
```
Save the returned address. The private key is stored in the local registry and is not returned.
You can inspect or remove local wallet metadata without exposing keys:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/wallet/list \
peaq_ros2_interfaces/srv/PeaqosListWallets \
"{}"
ros2 service call /peaqos_node/wallet/get \
peaq_ros2_interfaces/srv/PeaqosGetWallet \
"{address: ''}"
```
The returned wallet JSON includes public peaq EVM account metadata:
`address`, `account_id`, `chain_id`, `network`, `label`, and `created_at`.
## 2. Fund the machine wallet
Registration signs from the machine wallet and requires enough native peaq for gas plus the IdentityRegistry registration bond. Fund the returned `` before calling `register`.
You can use the Gas Station flow with owner 2FA:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/faucet/setup_2fa \
peaq_ros2_interfaces/srv/PeaqosSetupFaucet2FA \
"{owner_address: '', qr_format: 'svg'}"
ros2 service call /peaqos_node/faucet/confirm_2fa \
peaq_ros2_interfaces/srv/PeaqosConfirmFaucet2FA \
"{owner_address: '', two_factor_code: '123456'}"
ros2 service call /peaqos_node/wallet/fund \
peaq_ros2_interfaces/srv/PeaqosFundWallet \
"{owner_address: '', target_address: '', chain_id: '3338', two_factor_code: '123456', request_id: ''}"
```
Or transfer native peaq directly to `` from an already funded wallet. Wait for the balance to be available before registration.
## 3. Register the machine
For a self-managed machine, the node looks up `` in the local registry and signs the registration transaction with that wallet.
For a self-managed machine:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/machine/register \
peaq_ros2_interfaces/srv/PeaqosRegisterMachine \
"{address: ''}"
```
For a proxy operator:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/machine/register_for \
peaq_ros2_interfaces/srv/PeaqosRegisterFor \
"{proxy_address: '', machine_address: ''}"
```
Registration returns `machine_id`. This is the IdentityRegistry machine ID. It is not necessarily the Machine NFT token ID.
## 4. Mint the Machine NFT and write DID attributes
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/nft/mint \
peaq_ros2_interfaces/srv/PeaqosMintNft \
"{signer_address: '', machine_id: 1, recipient: ''}"
```
Read the Machine NFT token ID:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/nft/token_id_of \
peaq_ros2_interfaces/srv/PeaqosTokenIdOf \
"{signer_address: '', machine_id: 1}"
```
Link the machine's DID to its Machine NFT and metadata endpoints:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/did/write_machine_attributes \
peaq_ros2_interfaces/srv/PeaqosWriteMachineDidAttributes \
"{signer_address: '', machine_id: 1, nft_token_id: 1, operator_did: 'did:peaq:', documentation_url: 'https://docs.example/robot-001', data_api: 'https://api.example/robot-001', data_visibility: 'onchain'}"
```
Verify a single attribute:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/did/read_attribute \
peaq_ros2_interfaces/srv/PeaqosReadDidAttribute \
"{signer_address: '', did_address: '', name: 'machineId'}"
```
## 5. Query MCR and machine profile
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/mcr/query \
peaq_ros2_interfaces/srv/PeaqosQueryMcr \
"{did: 'did:peaq:'}"
```
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/mcr/machine \
peaq_ros2_interfaces/srv/PeaqosQueryMachine \
"{did: 'did:peaq:'}"
```
Newly registered machines can return `Provisioned` until enough event history exists.
## 6. Validate and submit events
Validate before broadcasting:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/events/validate \
peaq_ros2_interfaces/srv/PeaqosValidateEvent \
"{machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x73656e736f723a6f6b', trust_level: 0, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}"
```
Submit one event:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/events/submit \
peaq_ros2_interfaces/srv/PeaqosSubmitEvent \
"{signer_address: '', machine_id: 1, event_type: 1, value: 1, timestamp: 1770000000, raw_data_hex: '0x73656e736f723a6f6b', trust_level: 0, source_chain_id: 3338, source_tx_hash: '', metadata_hex: '0x7b7d'}"
```
Batch submission uses `events_json`, a JSON array using the same event field names.
## 7. Predict or deploy a machine smart account
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/smart_account/address \
peaq_ros2_interfaces/srv/PeaqosGetSmartAccountAddress \
"{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}"
```
Deploy with the same parameters:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/smart_account/deploy \
peaq_ros2_interfaces/srv/PeaqosDeploySmartAccount \
"{signer_address: '', owner: '', machine: '', daily_limit: '', salt: '0'}"
```
## 8. Bridge Machine NFT from peaq to Base
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/bridge/nft \
peaq_ros2_interfaces/srv/PeaqosBridgeNft \
"{signer_address: '', token_id: 1, source: 'peaq', destination: 'base', recipient: '', base_rpc_url: '', base_nft_address: '', options_hex: ''}"
```
Wait for the NFT to appear on Base:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
ros2 service call /peaqos_node/bridge/wait_arrival \
peaq_ros2_interfaces/srv/PeaqosWaitForBridgeArrival \
"{dst_rpc_url: 'https://mainnet.base.org', dst_nft_address: '0xee8A521eA434b11F956E2402beC5eBfa753Babfa', token_id: 1, timeout: 900}"
```
## Production checklist
* Keep `peaq_os.wallet_registry.path` local to the robot or machine.
* Keep the wallet registry file permissioned as `0600`.
* Quote all EVM addresses in ROS YAML service payloads.
* Record pre and post balances for production bridge or payment tests.
* Confirm contract addresses before release if peaqOS publishes a new deployment.
* Fund Base ETH before attempting Base to peaq bridge operations.
## References
* [ROS 2 service catalog](/peaqos/sdk-reference/ros2/services)
* [ROS 2 configuration](/peaqos/sdk-reference/ros2/configuration)
* [Machine NFT concept](/peaqos/concepts/machine-nft)
* [Events concept](/peaqos/concepts/events)
* [Python SDK reference](/peaqos/sdk-reference/sdk-python)
* [JavaScript SDK reference](/peaqos/sdk-reference/sdk-js)
# Self-managed onboarding
Source: https://docs.peaq.xyz/peaqos/guides/self-managed-onboarding
Activate a single machine where the owner is the operator. Generate keys, set up 2FA, fund via Gas Station, preview, and activate in one transaction.
Owner equals operator. One keypair, one machine, one identity. This guide covers the full flow from environment setup through on-chain activation under [Economics 2.0](/peaqos/concepts/economics-2-0).
## Prerequisites
* Node.js ≥ 22 with `@peaqos/peaq-os-sdk` 0.7.0+, or Python 3.10+ with `peaq-os-sdk` 0.7.1+
* A funded EVM wallet holding the tier bond plus gas. The bond is quoted per tier at the oracle rate (Entry quoted 0.803 PEAQ on mainnet on 2026-09-04 at 20:20 UTC; the figure moves with the daily oracle price); preview it before you fund. On peaq, bond and gas come out of the same PEAQ balance.
* Environment variables configured per the [install guide](/peaqos/install)
Prefer the terminal? `peaqos init` then `peaqos activate ... --dry-run` does everything on this page in two commands. See [CLI: activate](/peaqos/cli#peaqos-activate).
## Environment setup
```bash .env theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# peaq mainnet RPC. See /peaqos/install#public-rpc-endpoints for alternatives
PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public
PEAQOS_PRIVATE_KEY=0x<64-hex-chars>
# Tokenomics 1.0 contracts. The SDK constructor still requires every variable below;
# activation does not read them. For agung addresses see /peaqos/install#agung-testnet-contracts.
IDENTITY_REGISTRY_ADDRESS=0xb53Af985765031936311273599389b5B68aC9956
IDENTITY_STAKING_ADDRESS=0x11c05A650704136786253e8685f56879A202b1C7
EVENT_REGISTRY_ADDRESS=0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA
MACHINE_NFT_ADDRESS=0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9
DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800
BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805
```
The Economics 2.0 contract addresses are not environment variables. You select a deployment record (`peaq-mainnet` or `agung-2026-08-28`) in code and the SDK verifies its addresses against `InfoDesk.peer(role)` on chain.
JS examples load the file via `import "dotenv/config"`. Python's `from_env()` reads from the shell, so export the file first with `set -a && source .env && set +a` or call `load_dotenv()`.
## Full flow
`fromEnv` reads the required variables. Throws `ValidationError` if any is missing.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const owner = PeaqosClient.fromEnv();
console.log("Owner:", owner.address);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from dotenv import load_dotenv
from peaq_os_sdk import PeaqosClient
load_dotenv()
owner = PeaqosClient.from_env()
print("Owner:", owner.address)
```
If the machine does not already have a wallet, generate one. Local operation, no chain interaction.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const keypair = PeaqosClient.generateKeypair();
console.log("Address:", keypair.address);
// Store keypair.privateKey securely. It cannot be recovered.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
address, private_key = PeaqosClient.generate_keypair()
print("Address:", address)
# Store private_key securely. It cannot be recovered.
```
If the machine already has a funded wallet, skip to step 6 and build the machine client with that key.
The Gas Station requires 2FA before it funds any address.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const FAUCET_URL = "https://depinstation.peaq.xyz";
const enrollment = await owner.setupFaucet2FA(keypair.address, FAUCET_URL);
console.log("OTPAuth URI:", enrollment.otpauthUri);
console.log("QR image:", enrollment.qrImageUrl);
// Scan the QR or paste the URI into your authenticator app. The QR URL expires after roughly 2 minutes.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
FAUCET_URL = "https://depinstation.peaq.xyz"
enrollment = owner.setup_faucet_2fa(owner_address=address, faucet_base_url=FAUCET_URL)
print("OTPAuth URI:", enrollment["otpauth_uri"])
print("QR image:", enrollment["qr_image_url"])
```
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
await owner.confirmFaucet2FA(keypair.address, FAUCET_URL, "123456"); // current TOTP code
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
owner.confirm_faucet_2fa(owner_address=address, faucet_base_url=FAUCET_URL, two_factor_code="123456")
```
Request initial gas from the Gas Station. The faucet funds the wallet or skips if the balance already suffices. The faucet covers gas, not the bond: top up the bond from any PEAQ-holding address after you have previewed the quote in step 7.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const result = await owner.fundFromGasStation(
{ ownerAddress: keypair.address, targetWalletAddress: keypair.address, chainId: "peaq", twoFactorCode: "654321" },
FAUCET_URL,
);
console.log(result.status === "success" ? result.txHash : result.currentBalance);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
result = owner.fund_from_gas_station(
owner_address=address, target_wallet_address=address, chain_id="peaq",
two_factor_code="654321", faucet_base_url=FAUCET_URL,
)
print(result["tx_hash"] if result["status"] == "success" else result["current_balance"])
```
The machine's own key signs the activation, so the machine becomes owner, payer, and controller. Passing `tokenomics20` selects the Economics 2.0 deployment.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const machine = new PeaqosClient<"tokenomics20">({
rpcUrl: owner.rpcUrl,
privateKey: keypair.privateKey,
contracts: owner.contracts,
tokenomics20: { deploymentId: "peaq-mainnet" }, // "agung-2026-08-28" for the testnet
});
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import Tokenomics20Config
machine = PeaqosClient(
rpc_url=owner.rpc_url,
private_key=private_key,
identity_registry=owner.contracts.identity_registry,
identity_staking=owner.contracts.identity_staking,
event_registry=owner.contracts.event_registry,
machine_nft=owner.contracts.machine_nft,
did_registry=owner.contracts.did_registry,
batch_precompile=owner.contracts.batch_precompile,
tokenomics20=Tokenomics20Config(deployment_id="peaq-mainnet"), # "agung-2026-08-28" for the testnet
)
```
The preview runs every check the activation runs (chain, contracts, peers, price, balance, allowance) without signing or spending. Use it to see the bond, any voucher credit, and the net PEAQ you must hold.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const params = {
controller: keypair.address,
verificationMethods: [{ id: "#key-1", methodType: "Ed25519VerificationKey2020", controller: keypair.address, publicKeyMultibase: "z6Mk..." }],
authentication: [0n],
serviceEndpoints: [
{ id: "#docs", serviceType: "Documentation", serviceEndpoint: "https://example.com/docs" },
{ id: "#data", serviceType: "DataApi", serviceEndpoint: "https://example.com/events" },
],
machineType: "Sensor",
credentialSubject: "0xdeadbeef", // your identity anchor bytes; fixes the machine ID together with machineType
manufacturer: "0x3333333333333333333333333333333333333333",
tier: 0, // 0 Entry, 1 Basic, 2 Pro
};
const preview = await machine.previewMachineActivation(params);
console.log("machine ID", preview.machineId.toString());
console.log("bond", preview.bondAmount, "voucher", preview.voucherCredit, "net", preview.netPeaqAmount);
console.log("balance", preview.balance, "approval needed", preview.approvalRequired);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import ActivateMachineParams, ServiceEndpointInput, VerificationMethodInput
params = ActivateMachineParams(
controller=address,
verification_methods=(VerificationMethodInput(id="#key-1", method_type="Ed25519VerificationKey2020", controller=address, public_key_multibase="z6Mk..."),),
authentication=(0,),
service_endpoints=(
ServiceEndpointInput(id="#docs", service_type="Documentation", service_endpoint="https://example.com/docs"),
ServiceEndpointInput(id="#data", service_type="DataApi", service_endpoint="https://example.com/events"),
),
machine_type="Sensor",
credential_subject=bytes.fromhex("deadbeef"), # your identity anchor bytes; fixes the machine ID together with machine_type
manufacturer="0x3333333333333333333333333333333333333333",
tier=0, # 0 Entry, 1 Basic, 2 Pro
)
preview = machine.preview_machine_activation(params)
print("machine ID", preview.machine_id)
print("bond", preview.bond_amount, "voucher", preview.voucher_credit, "net", preview.net_peaq_amount)
print("balance", preview.balance, "approval needed", preview.approval_required)
```
If `balance` is below `netPeaqAmount` plus gas, top up the machine wallet now.
One transaction. The SDK approves exactly the net amount to `MachineSubscription` if needed, re-quotes, simulates, submits once, then requires the three receipt events and a matching on-chain state before returning.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const result = await machine.activateMachine(params);
console.log("Activated machine", result.machineId.toString());
console.log("Paid", result.netPeaqAmount, "wei of PEAQ; bonded", result.bondAmount);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
result = machine.activate_machine(params)
print("Activated machine", result.machine_id)
print("Paid", result.net_peaq_amount, "wei of PEAQ; bonded", result.bond_amount)
```
The machine now has a peaqID (`did:peaq:`), its Machine NFT (token ID equals the machine ID), and a tier bond. Confirm with `getMachineActivationState` / `get_machine_activation_state`: `subscription.periodStart != 0` means activated (tier 0 is a valid tier, so never test the tier).
## Error handling
| Error | Cause | Resolution |
| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| `ValidationError: PEAQOS_PRIVATE_KEY is required` | Missing environment variable | Check your `.env` and shell export |
| `TokenomicsConfigError` `DEPLOYMENT_UNKNOWN` | Typo in the deployment ID | Use `peaq-mainnet` or `agung-2026-08-28` |
| `TokenomicsActivationError` `PRICE_NOT_AVAILABLE` | The oracle holds no PEAQ price for this chain | Chain-side condition; retry after the trust validator's next daily stamp |
| `TokenomicsActivationError` `INSUFFICIENT_BALANCE` | Wallet does not hold the net PEAQ plus gas | Top up the machine wallet and retry |
| `TokenomicsActivationError` `CONTRACT_REVERTED` with `MachineAlreadyExists` or similar | The same `machineType` + `credentialSubject` pair was activated before | Pick a new credential subject; the pair is permanent |
| `TokenomicsPendingTransactionError` / `PENDING_TRANSACTION` | Receipt did not arrive in time | Do not resubmit. Reconcile with `reconcile_activation_transaction` (Python) or re-check `getMachineActivationState` by machine ID |
| `RuntimeError: Invalid 2FA code` / `ApiError: Invalid 2FA code` | TOTP code expired or mistyped | Wait for a fresh code and retry |
| `RuntimeError: Faucet rate limit exceeded` / `ApiError` | Too many funding requests | Retry after a short interval |
Full code tables on [errors](/peaqos/sdk-reference/errors#tokenomics-2-0-errors).
## What the machine receives
* **peaqID** `did:peaq:` with the DID document you supplied (verification methods, authentication, service endpoints)
* **Machine NFT** in `MachineRegistry`, token ID equal to the machine ID
* **Subscription** on the chosen tier for 365 days, bonded in PEAQ; renew before the 14-day grace and 14-day runoff windows end. See [Economics 2.0](/peaqos/concepts/economics-2-0#subscription-lifecycle)
* **Home chain record** on peaq
* **Gas Station** funding (if requested)
## Machines onboarded before Economics 2.0
`registerMachine` / `register_machine`, `mintNft`, and `writeMachineDIDAttributes` still work on a client constructed **without** `tokenomics20` and still address the Tokenomics 1.0 contracts (1 PEAQ native bond, separate Machine NFT). They emit deprecation warnings and throw in Tokenomics mode. Use them only to maintain machines onboarded before 2026-09-01. See [Activate: legacy flow](/peaqos/functions/activate#legacy-flow-tokenomics-1-0).
# Submit events
Source: https://docs.peaq.xyz/peaqos/guides/submit-events
Submit revenue and activity events to the EventRegistry. Covers validation, data hashing, event types, cross-chain patterns, and operational limits.
Revenue events (type 0) and activity events (type 1) feed the [Machine Credit Rating](/peaqos/concepts/machine-credit-rating). Every event is validated client-side, hashed, and submitted on-chain to the EventRegistry contract.
**Economics 2.0 machines.** Since `@peaqos/peaq-os-sdk` 0.7.0 and `peaq-os-sdk` 0.7.1 (2026-09-11) event submission works on a client constructed with `tokenomics20`. The SDK writes to the `EventRegistry` address you configure (`EVENT_REGISTRY_ADDRESS`): use the 2.0 contract `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e` for 2.0 machines and `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` for Tokenomics 1.0 machines. Both share the `submitEvent` selector, so a write to the wrong one lands there without reverting. `machineId` is the machine's `uint256` ID (`bigint` in JS in every mode since 0.6.0). CLI 0.0.8 `peaqos qualify event` cannot address 2.0 machines yet.
## Event types
| Type | Value | Purpose | Example |
| :------- | :---- | :-------------------------------------------------- | :------------------------------------------------- |
| Revenue | `0` | Records economic value generated by the machine | A claw machine collects \$5.00 from a play session |
| Activity | `1` | Records operational activity with no direct revenue | A weather sensor reports a telemetry ping |
## Trust levels
Each event carries a trust level describing how the data was attested. See [Trust levels](/peaqos/concepts/trust-levels) for the concept overview.
| Level | Value | Meaning | Needs source tx hash? |
| :------------------ | :---- | :-------------------------------------------------- | :-------------------- |
| Self-reported | `0` | Machine self-reports. No external verification. | No |
| On-chain verifiable | `1` | Event references a verifiable on-chain transaction. | Yes |
| Hardware-signed | `2` | Event signed by tamper-resistant hardware. | No |
## Currency and value units
`currency` is a first-class parameter on `submitEvent` / `submit_event`. Revenue events take a 3-10 char uppercase alphanumeric code (`USD`, `HKD`, `JPY`, …); activity events must pass `""`. The SDK applies a smart default when omitted on single-event submits (revenue → `"USD"`, activity → `""`); `batchSubmitEvents` / `batch_submit_events` requires it explicitly.
`value` is an **ISO 4217 minor-unit integer**:
| Currency | Subunit divisor | Example |
| :--------------------------------------------------- | :-------------- | :----------------------- |
| `USD`, `HKD`, `EUR` (and other 2-decimal currencies) | `100` | `$1.23 → value: 123` |
| `JPY`, `KRW`, `VND` (no subunits) | `1` | `¥100 → value: 100` |
| `BHD`, `KWD`, `OMR` (3-decimal) | `1000` | `BD 1.234 → value: 1234` |
The MCR pipeline converts `value` to USD cents using the FX rate at `timestamp`. The converted amount surfaces on [`GET /machine/{did}`](/peaqos/api-reference/get-machine) as `usd_value` (USD cents integer) on revenue events when `data_visibility` is `onchain`. `amount_status` distinguishes `"ok"`, `"unsupported_currency"` (currency not in the FX whitelist), and `"fx_unavailable"` (degraded FX feed). Non-`"ok"` rows score conservatively and surface `mcr_degraded: true` on [`GET /mcr/{did}`](/peaqos/api-reference/get-mcr).
Activity events ignore the FX path entirely. They don't accumulate revenue.
## Validation
Call `validateSubmitEventParams` before submitting. It throws `ValidationError` on the first invalid field.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
validateSubmitEventParams,
computeDataHash,
EVENT_TYPE_REVENUE,
TRUST_SELF_REPORTED,
SUPPORTED_CHAIN_IDS,
} from "@peaqos/peaq-os-sdk";
const params = {
machineId: 1n,
eventType: EVENT_TYPE_REVENUE, // 0
value: 500, // $5.00 in cents
currency: "USD",
timestamp: Math.floor(Date.now() / 1000) - 10,
rawData: new TextEncoder().encode(JSON.stringify({ session: "abc123" })),
trustLevel: TRUST_SELF_REPORTED, // 0
sourceChainId: SUPPORTED_CHAIN_IDS.peaq, // 3338
sourceTxHash: null,
metadata: new Uint8Array([]),
};
// Throws ValidationError if any field is invalid
validateSubmitEventParams(params);
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import json
import time
from peaq_os_sdk import EVENT_TYPE_REVENUE, TRUST_SELF_REPORTED, SUPPORTED_CHAINS
from peaq_os_sdk.types.events import SubmitEventParams
from peaq_os_sdk.validation import validate_submit_event_params
params = SubmitEventParams(
machine_id=1,
event_type=EVENT_TYPE_REVENUE, # 0
value=500, # $5.00 in cents
currency="USD",
timestamp=int(time.time()),
raw_data=json.dumps({"session": "abc123"}).encode(),
trust_level=TRUST_SELF_REPORTED, # 0
source_chain_id=SUPPORTED_CHAINS["peaq"], # 3338
source_tx_hash=None,
metadata=b"",
)
# Raises ValidationError if any field is invalid
validate_submit_event_params(params)
```
### Validation rules
| Field | Constraint | Error if violated |
| :------------------------------------- | :--------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- |
| `machineId` / `machine_id` | JS: positive `bigint` (0.6.0+; `number` is rejected). Python: positive `int` | `machineId must be a positive bigint` (JS) / `machine_id must be a positive integer` (Python) |
| `eventType` / `event_type` | `0` or `1` | `eventType must be 0 or 1` |
| `value` | Non-negative integer (ISO 4217 minor units) | `value must be non-negative` |
| `currency` | Revenue: `^[A-Z0-9]{3,10}$`. Activity: must be `""`. | `currency must match ^[A-Z0-9]{3,10}$` / `activity events require an empty currency string` |
| `trustLevel` / `trust_level` | `0`, `1`, or `2` | `trustLevel must be 0, 1, or 2` |
| `sourceChainId` / `source_chain_id` | `0`, `3338`, or `8453` | `sourceChainId must be a supported chain ID (0, 3338, or 8453)` |
| `rawData` / `raw_data` | Non-empty when provided | `rawData must not be empty when provided` |
| `sourceTxHash` / `source_tx_hash` | 0x-prefixed 32-byte hex (66 chars) when provided | `sourceTxHash must be a 0x-prefixed 32-byte hex string` |
| `timestamp` | Positive integer | `timestamp must be a positive integer` |
| `sourceTxHash` when `trustLevel === 1` | Required | `sourceTxHash is required when trustLevel is 1` |
The contract additionally rejects `metadata` larger than 4096 bytes with a `MetadataTooLarge` revert. The SDK validators don't enforce this client-side, so oversized payloads surface as a transaction failure (`RuntimeError`/`RpcError` with `code: "MetadataTooLarge"`) rather than `ValidationError`.
## Computing the data hash
The EventRegistry stores a keccak256 hash of the raw data, not the data itself. Compute it with `computeDataHash` (JS) or `compute_data_hash` (Python).
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { computeDataHash } from "@peaqos/peaq-os-sdk";
const rawData = new TextEncoder().encode(
JSON.stringify({ session: "abc123", amount: 500 })
);
const hash = computeDataHash(rawData);
// hash: "0x8dd186bf57dece591311f044eaf97f4886878309ff0b546a58386d2df41978e2"
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk.utils import compute_data_hash
raw_data = b'{"session": "abc123", "amount": 500}'
data_hash = compute_data_hash(raw_data)
# data_hash is 32 bytes (keccak256)
```
The hash is passed as the `dataHash` field in the on-chain `MachineEvent` struct. Consumers who need to verify the original data compare its keccak256 against the stored hash.
## Submitting a revenue event (type 0)
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import {
PeaqosClient,
validateSubmitEventParams,
computeDataHash,
EVENT_TYPE_REVENUE,
TRUST_SELF_REPORTED,
SUPPORTED_CHAIN_IDS,
} from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
const rawData = new TextEncoder().encode(
JSON.stringify({ session: "abc123", amount: 500 })
);
const params = {
machineId: 1n,
eventType: EVENT_TYPE_REVENUE,
value: 500, // $5.00 in cents
currency: "USD",
timestamp: Math.floor(Date.now() / 1000) - 10,
rawData,
trustLevel: TRUST_SELF_REPORTED,
sourceChainId: SUPPORTED_CHAIN_IDS.peaq,
sourceTxHash: null,
metadata: new Uint8Array([]),
};
validateSubmitEventParams(params);
const { txHash, dataHash } = await client.submitEvent(params);
console.log("Submitted revenue event:", { txHash, dataHash });
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from dotenv import load_dotenv
import json
import time
from peaq_os_sdk import (
PeaqosClient,
EVENT_TYPE_REVENUE,
TRUST_SELF_REPORTED,
SUPPORTED_CHAINS,
)
from peaq_os_sdk.types.events import SubmitEventParams
from peaq_os_sdk.validation import validate_submit_event_params
load_dotenv() # load envs from .env file
client = PeaqosClient.from_env()
raw_data = json.dumps({"session": "abc123", "amount": 500}).encode()
params = SubmitEventParams(
machine_id=1,
event_type=EVENT_TYPE_REVENUE,
value=500, # $5.00 in cents
currency="USD",
timestamp=int(time.time()) - 10,
raw_data=raw_data,
trust_level=TRUST_SELF_REPORTED,
source_chain_id=SUPPORTED_CHAINS["peaq"],
source_tx_hash=None,
metadata=b"",
)
validate_submit_event_params(params)
tx_hash, data_hash = client.submit_event(
machine_id=params.machine_id,
event_type=params.event_type,
value=params.value,
currency=params.currency,
timestamp=params.timestamp,
raw_data=params.raw_data,
trust_level=params.trust_level,
source_chain_id=params.source_chain_id,
source_tx_hash=params.source_tx_hash,
metadata=params.metadata,
)
print("Submitted revenue event:", tx_hash, data_hash.hex())
```
## Submitting an activity event (type 1)
Activity events record operational telemetry with no direct revenue value.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import {
PeaqosClient,
validateSubmitEventParams,
computeDataHash,
EVENT_TYPE_ACTIVITY,
TRUST_SELF_REPORTED,
SUPPORTED_CHAIN_IDS,
} from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
const rawData = new TextEncoder().encode(
JSON.stringify({ type: "heartbeat", uptimeSeconds: 86400 })
);
const params = {
machineId: 1n,
eventType: EVENT_TYPE_ACTIVITY, // 1
value: 0, // No revenue
currency: "", // activity events must be empty
timestamp: Math.floor(Date.now() / 1000) - 10,
rawData,
trustLevel: TRUST_SELF_REPORTED,
sourceChainId: SUPPORTED_CHAIN_IDS.peaq,
sourceTxHash: null,
metadata: new Uint8Array([]),
};
validateSubmitEventParams(params);
const { txHash, dataHash } = await client.submitEvent(params);
console.log("Submitted activity event:", { txHash, dataHash });
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from dotenv import load_dotenv
import json
import time
from peaq_os_sdk import (
PeaqosClient,
EVENT_TYPE_ACTIVITY,
TRUST_SELF_REPORTED,
SUPPORTED_CHAINS,
)
from peaq_os_sdk.types.events import SubmitEventParams
from peaq_os_sdk.validation import validate_submit_event_params
load_dotenv() # load envs from .env file
client = PeaqosClient.from_env()
raw_data = json.dumps({"type": "heartbeat", "uptime_seconds": 86400}).encode()
params = SubmitEventParams(
machine_id=1,
event_type=EVENT_TYPE_ACTIVITY, # 1
value=0, # No revenue
currency="", # activity events must be empty
timestamp=int(time.time()) - 10,
raw_data=raw_data,
trust_level=TRUST_SELF_REPORTED,
source_chain_id=SUPPORTED_CHAINS["peaq"],
source_tx_hash=None,
metadata=b"",
)
validate_submit_event_params(params)
tx_hash, data_hash = client.submit_event(
machine_id=params.machine_id,
event_type=params.event_type,
value=params.value,
currency=params.currency,
timestamp=params.timestamp,
raw_data=params.raw_data,
trust_level=params.trust_level,
source_chain_id=params.source_chain_id,
source_tx_hash=params.source_tx_hash,
metadata=params.metadata,
)
print("Submitted activity event:", tx_hash, data_hash.hex())
```
## Cross-chain revenue pattern
When a machine earns revenue on another chain (e.g., Base), reference the source transaction for on-chain verifiable trust (level 1).
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
validateSubmitEventParams,
EVENT_TYPE_REVENUE,
TRUST_ON_CHAIN_VERIFIABLE,
SUPPORTED_CHAIN_IDS,
} from "@peaqos/peaq-os-sdk";
const params = {
machineId: 1n,
eventType: EVENT_TYPE_REVENUE,
value: 1200, // $12.00 in cents
currency: "USD",
timestamp: Math.floor(Date.now() / 1000) - 10,
rawData: new TextEncoder().encode(JSON.stringify({ invoice: "INV-0042" })),
trustLevel: TRUST_ON_CHAIN_VERIFIABLE, // 1
sourceChainId: SUPPORTED_CHAIN_IDS.base, // 8453
sourceTxHash: "0xa1b2c3d4e5f6789000000000000000000000000000000000000000000000a1b2",
metadata: new Uint8Array([]),
};
validateSubmitEventParams(params);
// sourceTxHash is required when trustLevel is 1.
// The MCR system can verify this transaction on Base.
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import json
import time
from peaq_os_sdk import (
EVENT_TYPE_REVENUE,
TRUST_ON_CHAIN_VERIFIABLE,
SUPPORTED_CHAINS,
)
from peaq_os_sdk.types.events import SubmitEventParams
from peaq_os_sdk.validation import validate_submit_event_params
params = SubmitEventParams(
machine_id=1,
event_type=EVENT_TYPE_REVENUE,
value=1200, # $12.00 in cents
currency="USD",
timestamp=int(time.time()),
raw_data=json.dumps({"invoice": "INV-0042"}).encode(),
trust_level=TRUST_ON_CHAIN_VERIFIABLE, # 1
source_chain_id=SUPPORTED_CHAINS["base"], # 8453
source_tx_hash="0xa1b2c3d4e5f6789000000000000000000000000000000000000000000000a1b2",
metadata=b"",
)
validate_submit_event_params(params)
# source_tx_hash is required when trust_level is 1.
# The MCR system can verify this transaction on Base.
```
### Supported chain IDs
| Chain | ID |
| :---------------------------- | :----- |
| peaq (same-chain, or use `0`) | `3338` |
| Base | `8453` |
## Operational limits
The SDK enforces per-transaction value caps and rate limits before submitting.
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { checkOperationalLimits } from "@peaqos/peaq-os-sdk";
checkOperationalLimits(
{ machineId: 1n, value: 500 },
{
maxValuePerTx: 10000,
rateLimitMaxEvents: 60,
rateLimitWindowSeconds: 3600,
},
tracker, // EventTracker from previous submissions, or null
);
// Throws ValueCapExceeded if value > maxValuePerTx
// Throws RateLimitExceeded if count >= rateLimitMaxEvents within window
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk.types.client import OperationalLimits
from peaq_os_sdk.validation import check_operational_limits
check_operational_limits(
params, # SubmitEventParams (or any object with .machine_id and .value)
OperationalLimits(
max_value_per_tx=10000,
rate_limit_max_events=60,
rate_limit_window_seconds=3600,
),
tracker, # EventTracker from previous submissions, or None
)
# Raises ValueCapExceeded if value > max_value_per_tx
# Raises RateLimitExceeded if count >= rate_limit_max_events within window
```
| Limit | Error type | Description |
| :--------------------------------------------- | :------------------ | :----------------------------------------------------- |
| `maxValuePerTx` / `max_value_per_tx` | `ValueCapExceeded` | Single event value exceeds the per-transaction cap |
| `rateLimitMaxEvents` / `rate_limit_max_events` | `RateLimitExceeded` | Too many events submitted within the rate-limit window |
Set limits to `0` to disable (the default).
## Error handling
`submitEvent` / `submit_event` raise four distinct error types. Validation and limit errors are local; `RuntimeError` (JS) / `RpcError` (Python) wraps every chain or RPC failure. JS collapses chain and HTTP errors into a single `RuntimeError`; Python keeps them separate (`RpcError` for chain, `ApiError` for HTTP).
```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
ValidationError,
ValueCapExceeded,
RateLimitExceeded,
RuntimeError,
} from "@peaqos/peaq-os-sdk";
try {
const { txHash, dataHash } = await client.submitEvent(params);
} catch (err) {
if (err instanceof ValidationError) {
// Bad params: check err.field, err.constraint
} else if (err instanceof ValueCapExceeded) {
// Per-tx value cap tripped
} else if (err instanceof RateLimitExceeded) {
// Local rate limit tripped
} else if (err instanceof RuntimeError) {
// Chain/RPC failure: err.code carries the contract revert name
// (e.g. "MetadataTooLarge", "MachineNotFound", "NotAuthorizedSubmitter")
// or "TX_REVERTED" for unrecognized reverts
} else {
throw err;
}
}
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import (
ValidationError,
ValueCapExceeded,
RateLimitExceeded,
RpcError,
)
try:
tx_hash, data_hash = client.submit_event(
machine_id=params.machine_id,
event_type=params.event_type,
value=params.value,
currency=params.currency,
timestamp=params.timestamp,
raw_data=params.raw_data,
trust_level=params.trust_level,
source_chain_id=params.source_chain_id,
source_tx_hash=params.source_tx_hash,
metadata=params.metadata,
)
except ValidationError as err:
# Bad params: inspect err.field, err.constraint
raise
except ValueCapExceeded:
# Per-tx value cap tripped
raise
except RateLimitExceeded:
# Local rate limit tripped
raise
except RpcError as err:
# Chain/RPC failure: err.code carries the contract revert name
# (e.g. "MetadataTooLarge", "MachineNotFound") or "TX_REVERTED"
raise
```
See [SDK errors reference](/peaqos/sdk-reference/errors) for the full code map and the cross-language equivalence between `RuntimeError` (JS) and `RpcError`/`ApiError` (Python).
## Next steps
* [Events concept](/peaqos/concepts/events) for deeper coverage of event types and trust levels
* [SDK reference](/peaqos/sdk-reference/sdk-js) for full method signatures
# Install
Source: https://docs.peaq.xyz/peaqos/install
Install peaqOS via npm or pip.
Pick the SDK that matches your stack. Both paths converge on the same onchain state.
## Requirements
| Requirement | Value |
| :-------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js | ≥ 22 |
| TypeScript | ≥ 5 (for the JS SDK) |
| Python | ≥ 3.12 for the Python SDK 0.6.0+ and the CLI 0.0.8+. SDK 0.4.0 to 0.5.0 and CLI 0.0.6 to 0.0.7 need ≥ 3.11. SDK 0.3.0 / CLI 0.0.5 are the last releases that run on 3.10 |
| Peer dependency | `viem >= 2.47.10` (JS only; Python pulls `web3 >= 6.0` automatically) |
| RPC access | peaq mainnet or agung testnet |
| Gas | Handled by Gas Station on fresh wallets; needs 2FA for request |
## Install paths
### Two pieces
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# 1. Install the CLI (the skill drives it under the hood); include the [ows] extra
# so `peaqos wallet ...` works inside the skill's onboarding flow.
# The skill was last aligned with CLI 0.0.6 and still issues the Tokenomics 1.0
# activate flags, so pin below 0.0.8 until the skill updates. Pin the SDK too:
# the CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, which breaks `peaqos monetize`.
# Needs Python 3.11 or newer.
pip install 'peaq-os-cli[ows]<0.0.8' 'peaq-os-sdk<0.6.0'
# 2. Add the peaqos skill to your agent (auto-detects Claude Code, Cursor, or Windsurf)
npx skills add peaqnetwork/peaq-os-skills
```
Then invoke `/peaqos` in any Claude Code session. To target a specific runtime explicitly, add `--agent claude-code | cursor | windsurf`. See the [peaqOS AI page](/peaqos/peaqos-ai) for details and the manual upload path for ChatGPT / custom GPTs.
### Works with
The `peaqos` skill ships first-class adapters for Claude Code, Cursor, and Windsurf — auto-detected, or selected with `--agent`. Hosted assistants without a local CLI (ChatGPT, Claude Projects, custom GPTs) can load `AGENT-PROMPT.md` as a system prompt manually.
The skill calls the JavaScript and Python SDKs through the CLI. Tabs below apply once you start editing code yourself.
### One command
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
pip install peaq-os-cli
# Optional: include OWS wallet commands (peaqos wallet create / import / list / …)
pip install 'peaq-os-cli[ows]'
peaqos init
peaqos activate --machine-type Sensor --credential-subject-hex 0xdeadbeef \
--manufacturer 0x3333333333333333333333333333333333333333 --tier entry \
--did-document ./did.json --dry-run # drop --dry-run to submit
```
Needs Python 3.10 or newer. `peaqos init` writes `TOKENOMICS_DEPLOYMENT_ID` (`peaq-mainnet` or `agung-2026-08-28`); `peaqos activate`, `peaqos machine`, and `peaqos monetize` all require it. The Economics 2.0 contract addresses are not in `.env`: they ship inside the SDK's deployment record.
**After running `peaqos init`, open your `.env` and verify the six Tokenomics 1.0 addresses against the [peaq mainnet contracts](#peaq-mainnet-contracts) table on this page.** A known bug in the init wizard can silently write a contract address to the wrong variable name. The SDK constructor still requires them even though activation does not read them.
Drives the same flows as the SDKs from your terminal: `peaqos init`, `peaqos whoami`, `peaqos activate`, `peaqos machine` (status, suspend, resume, subscription, transfer, DID updates), `peaqos monetize`, `peaqos wallet`, `peaqos stream`, `peaqos scale`. `peaqos qualify` and `peaqos show` read the Tokenomics 1.0 MCR API and fail when `TOKENOMICS_DEPLOYMENT_ID` is set. See [peaqOS CLI](/peaqos/cli) for the full command reference. CLI and SDK wallet helpers live on [Wallets (OWS)](/peaqos/wallets).
### Package
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
npm install @peaqos/peaq-os-sdk viem dotenv
```
OWS wallet helpers (`createWallet`, `importWallet`, `PeaqosClient.fromWallet`, …) work out of the box; `@open-wallet-standard/core` is bundled as a regular dependency of `@peaqos/peaq-os-sdk`. See [Wallets (OWS)](/peaqos/wallets) for the full surface.
`dotenv` is optional but recommended: `PeaqosClient.fromEnv()` reads from `process.env`, so `import "dotenv/config"` at the top of your entry file is the simplest way to load `.env`.
### Language + runtime
* Node.js ≥ 22
* TypeScript ≥ 5
* Package exports both ESM and CJS builds; no bundler workarounds required.
### Imports
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import "dotenv/config";
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
```
Full method reference on [SDK JS](/peaqos/sdk-reference/sdk-js). Error class hierarchy on [errors](/peaqos/sdk-reference/errors).
### Package
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
python3 -m venv .peaq-os
source .peaq-os/bin/activate
pip install "peaq-os-sdk>=0.6.0" python-dotenv
# Optional: include the OWS wallet helpers (createWallet, importWallet, …)
pip install "peaq-os-sdk[ows]>=0.6.0"
```
`python-dotenv` is optional but recommended: `PeaqosClient.from_env()` reads from dotenv, so `load_dotenv()` at the top of your entry file is the simplest way to load .env.
### Language + runtime
* Python ≥ 3.12 (0.6.0 and newer do not import on 3.10 or 3.11)
* `web3.py` pulled in automatically.
* Virtualenv strongly recommended. The commands above set one up.
### Imports
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import PeaqosClient
```
Full method reference on [SDK Python](/peaqos/sdk-reference/sdk-python). Error class hierarchy on [errors](/peaqos/sdk-reference/errors).
### Workspace
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
git clone https://github.com/peaqnetwork/peaq-robotics-ros2.git
cd peaq-robotics-ros2
```
### Build
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
source /opt/ros/jazzy/setup.bash
# Docker image: source /opt/ros/humble/setup.bash
# Native host only: python3 -m pip install -r requirements.txt
colcon build --packages-select peaq_ros2_interfaces peaq_ros2_peaqos peaq_ros2_examples
source install/setup.bash
```
### Run
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
cp peaq_ros2_examples/config/peaq_robot.example.yaml peaq_ros2_examples/config/peaq_robot.yaml
# Edit peaq_ros2_examples/config/peaq_robot.yaml and set peaq_os.enabled: true.
ros2 run peaq_ros2_peaqos peaqos_node --ros-args \
-p config.yaml_path:=peaq_ros2_examples/config/peaq_robot.yaml
```
Full method mapping on [ROS 2 SDK reference](/peaqos/sdk-reference/ros2/overview). End-to-end commands on [ROS 2 machine runtime](/peaqos/guides/ros2-machine-runtime).
## Environment variables
| Variable | Required | Default | Purpose |
| :-------------------------------- | :-------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PEAQOS_RPC_URL` | Yes | n/a | peaq chain RPC endpoint |
| `PEAQOS_PRIVATE_KEY` | Yes | n/a | Owner wallet private key (hex, `0x...`) |
| `PEAQOS_NETWORK` | CLI only | n/a | `mainnet` or `testnet`. Selects address + URL defaults for the CLI's `peaqos init`. SDK clients ignore it. |
| `PEAQOS_GAS_STATION_URL` | CLI only | n/a | Faucet base URL. Set to `https://depinstation.peaq.xyz` for the public peaq-hosted Gas Station (the older `depinstation.peaq.network` host has an expired TLS certificate). Required for the `peaqos activate` funding step unless `--skip-funding` is set or the wallet already meets the gas threshold. |
| `TOKENOMICS_DEPLOYMENT_ID` | CLI only | n/a | Economics 2.0 deployment record: `peaq-mainnet` or `agung-2026-08-28`. Written by `peaqos init`. Required by `peaqos activate`, `peaqos machine`, `peaqos monetize`, and `peaqos monetize provision run`. The SDKs do not read it: pass `tokenomics20: { deploymentId }` (JS) or `tokenomics20=Tokenomics20Config(deployment_id=...)` (Python) to the constructor instead. Contract addresses travel with the record; they are never environment variables. |
| `OWS_PASSPHRASE` | No | n/a | OWS vault passphrase used by the SDK and CLI wallet helpers (`createWallet`, `importWallet`, `exportWallet`, …). The SDK raises `PeaqosError` if neither this nor the inline `passphrase` arg is set (no interactive prompt). The CLI reads it when `PEAQOS_OWS_WALLET` is set, prompting interactively if unset. See [Wallets (OWS)](/peaqos/wallets). |
| `IDENTITY_REGISTRY_ADDRESS` | Yes | n/a | Identity Registry contract. See [peaq mainnet contracts](#peaq-mainnet-contracts) for the canonical address. |
| `IDENTITY_STAKING_ADDRESS` | Yes | n/a | Identity Staking contract |
| `EVENT_REGISTRY_ADDRESS` | Yes | n/a | Event Registry contract |
| `MACHINE_NFT_ADDRESS` | Yes | n/a | Machine NFT (ONFT) contract |
| `DID_REGISTRY_ADDRESS` | Yes | n/a | DID Registry precompile address (`0x...0800` on every peaq runtime) |
| `BATCH_PRECOMPILE_ADDRESS` | Yes | n/a | Batch precompile for multi-call bonding (`0x...0805` on every peaq runtime) |
| `MACHINE_ACCOUNT_FACTORY_ADDRESS` | No | n/a | `MachineAccountFactory`. Required only for smart-account deploy / predict. |
| `MACHINE_NFT_ADAPTER_ADDRESS` | No | n/a | `MachineNFTAdapter` (LayerZero ONFT adapter). Required only for `bridge_nft` / `bridgeNft` when `source="peaq"`. |
| `PEAQOS_OWS_WALLET` | CLI only | n/a | Active OWS vault wallet name. When set, the CLI loads the wallet via `OWS_PASSPHRASE` and skips `PEAQOS_PRIVATE_KEY`. Set by `peaqos wallet use` or `peaqos init` (`wallet` path). |
| `PEAQOS_MCR_API_URL` | Set for mainnet | `http://127.0.0.1:8000` | Tokenomics 1.0 MCR API base URL, read by `peaqos qualify` and `peaqos show`. Set to `https://mcr.peaq.xyz` to read from the public peaq-hosted MCR. Ignored by `peaqos monetize`, which resolves its endpoint from `TOKENOMICS_DEPLOYMENT_ID`. |
| `PEAQOS_MANIFEST_REPO_URL` | Monetize only | n/a | Base URL of the provisioning-manifest repo, read by `peaqos monetize provision` (or passed as `--manifest-repo`). There is no default and it is never hardcoded; get the current peaq-published base URL from the peaq team. Missing it exits `3`. |
| `PEAQOS_MACHINE_WALLET_ADDRESS` | Monetize | n/a | Machine wallet address used as the provisioning wallet context by `peaqos monetize provision`. Since CLI 0.0.8 `--machine` takes a decimal machine ID or `did:peaq:` (address DIDs are rejected), so this variable is the only way to supply the payout address. Unset it before provisioning a different machine. This value feeds the manifest's commission/payout context. |
| `PEAQOS_ORCHESTRATION_URL` | Scale only | n/a | Machine Markets orchestrator base URL. The canonical peaq-managed endpoint is `https://orchestration.peaq.xyz`. Read by the SDK (`client.orchestration`) and CLI (`peaqos scale ...`). |
| `PEAQOS_API_KEY` | SDK, optional | n/a | Platform API key sent as `x-api-key` on `client.orchestration` requests. Required only when the orchestrator runs with `PEAQOS_REQUIRE_API_AUTH=true`. |
| `PEAQOS_ORCH_API_KEY` | CLI, optional | n/a | Same platform API key as `PEAQOS_API_KEY` but under the CLI-specific name. `peaqos init` writes this; `peaqos scale ...` reads it. Set both names to the same value if you use the CLI and SDK in the same env. |
| `PEAQOS_TELEMETRY` | No | on | Anonymous SDK usage telemetry toggle. Set to `0` to disable. SDK only (JS + Python); the CLI sends none. See [Telemetry](#telemetry). |
| `DO_NOT_TRACK` | No | n/a | Cross-vendor opt-out convention: set to `1` to disable SDK telemetry (same effect as `PEAQOS_TELEMETRY=0`). |
Full reference with defaults on [SDK JS environment](/peaqos/sdk-reference/sdk-js) and [SDK Python environment](/peaqos/sdk-reference/sdk-python).
### Telemetry
The JavaScript and Python SDKs emit anonymous usage telemetry — which SDK operations run, tagged with a random install ID and with IP collection off (via PostHog, EU-hosted) — to help prioritize the roadmap. No private keys, wallet secrets, or machine data are sent. Telemetry is on by default; disable it with `PEAQOS_TELEMETRY=0` or the cross-vendor `DO_NOT_TRACK=1`. The CLI sends none.
## Public RPC endpoints
Use any of the endpoints below for `PEAQOS_RPC_URL`. QuickNode is the primary set; OnFinality and PublicNode are fallbacks. All of them accept EVM JSON-RPC calls. For private dedicated endpoints, see the [QuickNode](https://www.quicknode.com/guides/quicknode-products/how-to-use-the-quicknode-dashboard#create-a-quicknode-endpoint) and [OnFinality](https://documentation.onfinality.io/support/the-enhanced-api-service) guides. Full list on [Connecting to peaq](/peaqchain/build/getting-started/connecting-to-peaq).
```bash peaq mainnet theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
https://quicknode1.peaq.xyz
https://quicknode2.peaq.xyz
https://quicknode3.peaq.xyz
# Secondary / fallback
https://peaq.api.onfinality.io/public
https://peaq-rpc.publicnode.com
```
```bash agung testnet theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
https://peaq-agung.api.onfinality.io/public
https://wss-async-agung.peaq.xyz
```
## peaq mainnet contracts
Core contracts are UUPS upgradeable proxies; treat the addresses as the current proxy pointers. For the architecture diagram and what each contract is responsible for, see [Smart contracts](/peaqos/concepts/contracts).
### Economics 2.0
**Minimum versions: `@peaqos/peaq-os-sdk` 0.7.0 and `peaq-os-sdk` 0.7.1 (both 2026-09-11).** Earlier releases carry the `MachineBridgeAdapter` address from before the 2026-09-08 re-point and fail every `peaq-mainnet` write at preflight with `PEER_MISMATCH`. CLI 0.0.8 resolves 0.7.1 on a fresh install; upgrade an existing environment with `pip install -U peaq-os-sdk`.
Selected by `TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet` (CLI) or `deploymentId: "peaq-mainnet"` (SDKs). These are **not** environment variables: the addresses ship inside the SDK's deployment record and are verified against `InfoDesk.peer(role)` before every write. Listed here so you can check what `peaqos whoami` resolves.
| Contract | Address |
| :----------------------------- | :------------------------------------------- |
| InfoDesk | `0x6C7426ada37212D94477Ac4CFcA43A38177D6A82` |
| MachineRegistry | `0x64b93Cc29b251fAFa83BD110cDB1C24207f85536` |
| MachineStateAndSync | `0xcD1917FB2a56459AcA34FDD118e08776dE0890f2` |
| MachineSubscription | `0x9e37AD189c334C92e6B8a812Ca4c02f35Ac43895` |
| CrossChainMirror | `0x71DCB313977d6884212395505f081a2991Bfe8E5` |
| MachineBridgeAdapter | `0x791087c35484b567c53f392a624d2e4BaDcC53DF` |
| SubscriptionTokenProvisionPool | `0x7088Cf400081428a41a4287B75AC633071c0e92A` |
The full 13-contract set, including PriceOracle and TrustValidatorStaking, is on [Smart contracts](/peaqos/concepts/contracts#economics-2-0-peaq-mainnet).
### Tokenomics 1.0 (required by the SDK constructor)
| Variable | Address |
| :-------------------------- | :------------------------------------------------------------------- |
| `IDENTITY_REGISTRY_ADDRESS` | `0xb53Af985765031936311273599389b5B68aC9956` |
| `IDENTITY_STAKING_ADDRESS` | `0x11c05A650704136786253e8685f56879A202b1C7` |
| `EVENT_REGISTRY_ADDRESS` | `0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA` |
| `MACHINE_NFT_ADDRESS` | `0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9` |
| `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` (peaq DID precompile) |
| `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` (peaq batch precompile) |
### Optional
Set only if you use the corresponding SDK method.
| Variable | Address | Needed for |
| :-------------------------------- | :------------------------------------------- | :------------------------------------------------------------------ |
| `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x4A808d5A90A2c91739E92C70aF19924e0B3D527f` | `deploySmartAccount` / `getSmartAccountAddress` (ERC-4337) |
| `MACHINE_NFT_ADAPTER_ADDRESS` | `0x9AD5408702EC204441A88589B99ADfC2514AFAE6` | `bridgeNft` / `bridge_nft` when `source="peaq"` (LayerZero V2 ONFT) |
## Base mainnet contracts
Needed when bridging **into** peaq from Base. Pass `baseNftAddress` / `base_nft_address` to the bridge method.
| Contract | Address |
| :-------------------------------- | :------------------------------------------- |
| `MachineNFTBase` (LayerZero ONFT) | `0xee8A521eA434b11F956E2402beC5eBfa753Babfa` |
## Agung testnet contracts
Use these to point the SDK or CLI at agung. Precompile addresses are identical to mainnet (same fixed slots on every peaq runtime).
### Economics 2.0
Selected by `TOKENOMICS_DEPLOYMENT_ID=agung-2026-08-28` or `deploymentId: "agung-2026-08-28"`. Not environment variables.
| Contract | Address |
| :----------------------------- | :------------------------------------------- |
| InfoDesk | `0x72b66AF120c55371cA6e5Ce38D4e10dF1bA30Bb1` |
| MachineRegistry | `0x538da35489B1F0035799a8351E67835ae4ABb377` |
| MachineStateAndSync | `0xA7D33726232aae30622643a18047f0Ca3e159da9` |
| MachineSubscription | `0xaDCc5dD8CD57E3198B881c3388F25D2fD69B5aDc` |
| CrossChainMirror | `0xd5F8d5944c5d488B1cD698D642FC8156a8CD0c0c` |
| MachineBridgeAdapter | `0x0406e7bE522AEa626aee54f6258D415215e59F76` |
| SubscriptionTokenProvisionPool | `0x8A3692Ad0AcF79BB89413514Af9957d95032e19B` |
### Tokenomics 1.0 (required by the SDK constructor)
| Variable | Address |
| :-------------------------- | :------------------------------------------------------------------- |
| `IDENTITY_REGISTRY_ADDRESS` | `0x9E9463a65c7B74623b3b6Cdc39F71be7274e5971` |
| `IDENTITY_STAKING_ADDRESS` | `0x55f336714aDb0749DbFE33b057a1702405564E3d` |
| `EVENT_REGISTRY_ADDRESS` | `0x2DAD8905380993940e340C5cE6d313d5c2780040` |
| `MACHINE_NFT_ADDRESS` | `0xB41C2A4f1c19b6B06beaAce0F5CD8439e77C4b1c` |
| `DID_REGISTRY_ADDRESS` | `0x0000000000000000000000000000000000000800` (peaq DID precompile) |
| `BATCH_PRECOMPILE_ADDRESS` | `0x0000000000000000000000000000000000000805` (peaq batch precompile) |
### Optional
| Variable | Address | Needed for |
| :-------------------------------- | :------------------------------------------- | :--------------------------------------------------------- |
| `MACHINE_ACCOUNT_FACTORY_ADDRESS` | `0x65a4DfEB799dFf8CF15f13816d648a7805d6b1F9` | `deploySmartAccount` / `getSmartAccountAddress` (ERC-4337) |
| `ADMIN_FLAGS_ADDRESS` | `0x4181a2Aa34aFb247450FfcBd65be5aBD4Cbee658` | MCR API server (negative-flag + trust-override reads) |
**Bridging cannot be exercised on agung.** LayerZero deprecated the agung endpoint (EID `40299`): DVNs and executors are no longer active, so `bridgeNft` / `bridge_nft` cannot relay. No agung `MachineNFTAdapter` appears in peaq deployment records. Leave `MACHINE_NFT_ADAPTER_ADDRESS` unset on agung and test the bridge on peaq mainnet ↔ Base mainnet only.
## Troubleshooting
The OTP from your authenticator app was wrong or expired. Generate a fresh code and retry. See full [error code reference](/peaqos/sdk-reference/errors).
The faucet throttles owners after repeated bad OTPs. Wait out the lockout window and retry. See [error code reference](/peaqos/sdk-reference/errors).
The faucet enforces a per-IP and per-wallet cap. Retry after the cooldown. See [error code reference](/peaqos/sdk-reference/errors).
The same owner address has hit the daily funding cap. Wait 24 hours or contact support. See [error code reference](/peaqos/sdk-reference/errors).
Set `PEAQOS_RPC_URL` in your environment to a valid peaq chain RPC endpoint (e.g., `https://peaq.api.onfinality.io/public`). Without it, `PeaqosClient.fromEnv()` raises a `ValidationError`. See [Public RPC endpoints](#public-rpc-endpoints) for the full list. Full error taxonomy on [errors](/peaqos/sdk-reference/errors).
# peaqOS overview
Source: https://docs.peaq.xyz/peaqos/overview
The machine economy runs on peaqOS.
peaqOS is the omnichain machine layer. It gives robots and machines an on-chain identity, a credit rating, and the infrastructure to earn, transact, and become investable across chains.
## peaqOS is omnichain
peaqOS contracts hold the canonical record of identity and credit on peaq chain. Any chain can query them; any chain can consume them.
Your machine gets a peaqID and Machine NFT on peaq chain.
Revenue and activity events feed into a Machine Credit Rating.
Any chain can query identity and credit from the MCR API, including by Solana address. Machine NFTs bridge between peaq, Base, and Solana via LayerZero V2, and peaq identity and staking state is mirrored to Solana. See [Omni-chain](/peaqos/concepts/omni-chain).
## Functions
Put your machine on-chain in one transaction: peaqID, Machine NFT, and a tier bond under [Economics 2.0](/peaqos/concepts/economics-2-0). Self-owned and machine-owned, operator-controlled fleet patterns both supported.
Read the [Activate function](/peaqos/functions/activate).
Machine Credit Rating built from a machine's revenue and activity history. The MCR API exposes ratings to any chain.
Read the [Qualify function](/peaqos/functions/qualify).
Pair an AI agent to your machine, set a delegation policy (spend limits, allow/denylist), and let it discover and consume services through the Machine Markets API.
Read the [Scale function](/peaqos/functions/scale).
Sell the data your machine generates — signed, encrypted, and verifiable. Buyers pay for access to exactly what they need.
Read the [Stream function](/peaqos/functions/stream).
Put your machine to work as a compute provider and earn to its machine wallet: opt in, provision from a manifest, report presence.
Read the [Monetize function](/peaqos/functions/monetize).
Prove a machine is real via hardware attestation and trusted third parties.
Read the [Verify function](/peaqos/functions/verify).
Fractionalize your machine into an investable asset via ERC-3643.
Read the [Tokenize function](/peaqos/functions/tokenize).
## Two onboarding patterns
Owner equals operator. One machine, one wallet, one `activateMachine` call.
Each machine signs its own activation and owns its NFT; the operator is recorded as DID controller and runs lifecycle, renewals, and DID updates for the fleet.
## Keep going
TypeScript, Python, and ROS 2 signatures, parameters, returns, errors.
Run peaqOS from robot-native ROS 2 services with local key custody.
Query MCR and machine profiles from any chain.
Onboarding, fleets, event submission.
# peaqOS AI
Source: https://docs.peaq.xyz/peaqos/peaqos-ai
Use the peaqOS agent skill to onboard machines, query MCR, and run fleet workflows from any AI coding agent.
peaqOS ships one curated agent skill — `peaqos` — that turns your AI agent into a peaqOS onboarding co-pilot. It's a [skill.md](https://agentskills.io/specification)-spec skill with first-class adapters for Claude Code, Cursor, and Windsurf, plus a manual upload path for any other runtime that reads the standard. Under the hood it drives the [`peaqos` Python CLI](/peaqos/cli) — anything you can do at the terminal, the skill can do for you with the right questions asked first.
## What the skill does
Trigger it in your agent (`/peaqos` in Claude Code, or just describe what you want elsewhere) and it picks the right mode based on what you ask:
| Mode | When it kicks in |
| :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Demo** | First-time tour on agung testnet — full onboarding in \~15 minutes with explanations at every step. |
| **Real onboarding** | Asks five questions about your machine and deployment, recommends a self-managed or operator-managed setup, then runs the CLI to activate the machine and submit the first event. |
| **Fleet management** | Pulls MCR scores for an operator's fleet, surfaces machines with low or no rating, submits heartbeat events. |
| **Machine Markets (Scale)** | Onboards a machine to the orchestrator, pairs an agent with a delegation policy, runs market search, places an order, and confirms or disputes delivery. Drives `peaqos scale ...` end-to-end. |
| **Troubleshooting** | Diagnoses common failures — funding, activation, MCR lag, key mismatches — and walks you through the fix. |
The skill adapts its tone to your background: concise for developers, plain English with narrated steps for non-technical operators.
## Install
Two pieces. The CLI does the actual work; the skill is the orchestration layer your agent loads.
### 1. Install the CLI
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
python3 -m venv .peaqos-env
source .peaqos-env/bin/activate
pip install 'peaq-os-cli[ows]<0.0.8' 'peaq-os-sdk<0.6.0'
peaqos --version
```
**Pin the CLI below 0.0.8 for now.** The `peaqos` skill (`@peaqos/skills` 0.1.0) was last aligned with CLI 0.0.6 and drives the Tokenomics 1.0 `peaqos activate --doc-url ... --data-api ...` flow. CLI 0.0.8 (2026-09-04) replaced that command with the one-transaction [Economics 2.0 activation](/peaqos/functions/activate) and rejects those flags, so the skill's onboarding mode breaks against a fresh `pip install peaq-os-cli`. The pin comes off when the skill updates. Pin the SDK as well: `peaq-os-cli<0.0.8` alone resolves to CLI 0.0.7 with SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError` in `MonetizationConfig`. CLI 0.0.7 with SDK 0.5.0 needs Python 3.11 or newer; 0.0.8 needs 3.12.
### 2. Add the skill to your agent
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# Auto-detect (Claude Code, Cursor, or Windsurf)
npx skills add peaqnetwork/peaq-os-skills
# Or target a specific runtime
npx skills add peaqnetwork/peaq-os-skills --agent claude-code
npx skills add peaqnetwork/peaq-os-skills --agent cursor
npx skills add peaqnetwork/peaq-os-skills --agent windsurf
```
The installer ships adapters for Claude Code, Cursor, and Windsurf, auto-detecting the agent it finds on disk (or prompting if multiple are installed). Invoke `/peaqos` (Claude Code) or just describe what you want (e.g. *"onboard my machine to peaqOS"*) in Cursor or Windsurf.
For ChatGPT, Claude Projects, custom GPTs, or anywhere you can't run a local CLI, clone the repo and upload `AGENT-PROMPT.md` as the system prompt with `knowledge/` and `GUIDE.md` as knowledge sources:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
git clone https://github.com/peaqnetwork/peaq-os-skills
```
## LLM context files
Separate from the curated skill, Mintlify auto-generates machine-readable bundles of these docs. Point any agent at one of:
| File | URL | Use when |
| :-------------- | :------------------------------------------------------------------------- | :----------------------------------------------------------------------- |
| `llms.txt` | [https://docs.peaq.xyz/llms.txt](https://docs.peaq.xyz/llms.txt) | Model has a modest context window or you want a compact index with links |
| `llms-full.txt` | [https://docs.peaq.xyz/llms-full.txt](https://docs.peaq.xyz/llms-full.txt) | Model has a large context window and you want the full docs inline |
| `skill.md` | [https://docs.peaq.xyz/skill.md](https://docs.peaq.xyz/skill.md) | Agent needs a doc-derived capabilities spec rather than prose docs |
These are doc context, not a substitute for the curated `peaqos` skill — that one knows the onboarding flow, decision tree, and recovery paths; the auto-generated bundle just knows what's on the page.
## Editor setup (docs context)
Wire the peaq docs into your editor of choice. Independent of the `peaqos` skill — useful any time you want the agent to ground answers in current docs.
Open **Cursor Settings → Features → Docs**, click **Add new doc**, and paste:
```
https://docs.peaq.xyz/llms-full.txt
```
Reference peaq in chat with `@docs` → peaq.
Windsurf has no persistent docs store. Paste into Cascade (`Cmd+L`) per chat:
```
@docs:https://docs.peaq.xyz/llms-full.txt
```
Add the Mintlify-hosted MCP server to `.mcp.json`:
```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
"mcpServers": {
"peaq-docs": { "url": "https://docs.peaq.xyz/mcp" }
}
}
```
Or one-shot install with the Mintlify CLI:
```bash theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
npx @mintlify/mcp add docs.peaq.xyz
```
Same MCP URL: `https://docs.peaq.xyz/mcp`. For custom GPTs or Claude Projects, upload `llms-full.txt` as a knowledge source.
## See also
Chain-level prompting tips, Cursor Projects, peaq SDK prompt patterns.
The ground truth the skill calls into via the CLI.
# Errors
Source: https://docs.peaq.xyz/peaqos/sdk-reference/errors
Error class hierarchy, Tokenomics 2.0 error families, 20 faucet codes, and on-chain revert names for the peaqOS SDK.
Every error raised by the SDK extends a common base. Faucet-specific codes are surfaced on the error instance (`error.code`) so callers can branch without string matching.
## Class hierarchy
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
PeaqosError
├── ValidationError
├── RuntimeError
├── ValueCapExceeded
└── RateLimitExceeded
```
These four extend `PeaqosError` directly. Each function area adds its own family alongside them, also extending `PeaqosError`: `MonetizationError` (plus `MonetizationCompatibilityError` since 0.6.0), `ProvisioningError`, `StreamError`, `OrchestrationError`, and since 0.6.0 the Economics 2.0 family `TokenomicsConfigError`, `TokenomicsActivationError`, `TokenomicsUnsupportedError`, `TokenomicsIntegrationUnavailableError` (see [Tokenomics 2.0 errors](#tokenomics-2-0-errors)). So `catch (err instanceof PeaqosError)` covers every SDK error; catching `RuntimeError` alone will miss `ValueCapExceeded` and `RateLimitExceeded`.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
PeaqosError, // base
ValidationError, // bad input
RuntimeError, // chain or HTTP failure; carries optional `code`
ValueCapExceeded, // operational cap hit (extends PeaqosError)
RateLimitExceeded, // operational cap hit (extends PeaqosError)
} from "@peaqos/peaq-os-sdk";
```
```text theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
PeaqosError
├── ValidationError
├── RpcError
├── ApiError
├── ValueCapExceeded
└── RateLimitExceeded
```
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import (
PeaqosError,
ValidationError,
RpcError, # chain / web3 errors
ApiError, # HTTP API errors (faucet, MCR API)
ValueCapExceeded,
RateLimitExceeded,
)
```
Python separates transport-layer failures into `RpcError` (chain) and `ApiError` (faucet + MCR API). JavaScript collapses both into `RuntimeError` with a `code` field. Since 0.6.0 the root also exports `TokenomicsConfigError`, `TokenomicsActivationError`, `TokenomicsPendingTransactionError` (a subclass of the activation error), `TokenomicsUnsupportedError`, and `TokenomicsIntegrationUnavailableError`; `peaq_os_sdk.monetization` adds `MonetizationCompatibilityError`, `MonetizationNetworkError`, `MonetizationResponseError`, `MonetizationTimeoutError`, and `MonetizationCancelledError`. All extend `PeaqosError`.
***
## Tokenomics 2.0 errors
Raised by the Economics 2.0 surface (SDK 0.6.0+): activation, machine management, and the 2.0 monetization client. Codes are string literals on `err.code` in both SDKs; the contract a revert came from travels on `err.contract`, and reverts are decoded by 4-byte selector scoped to that contract's ABI. An unrecognised selector still surfaces as `CONTRACT_REVERTED` with the raw `revertData` / `revert_data`.
| Class | Codes | Notes |
| :--------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenomicsConfigError` | `DEPLOYMENT_UNKNOWN`, `DEPLOYMENT_UNAVAILABLE`, `DEPLOYMENT_NOT_CONFIGURED`, `ADDRESS_OVERRIDE_MISMATCH` (the CLI reports `DEPLOYMENT_NOT_CONFIGURED` as `TOKENOMICS_NOT_CONFIGURED`) | Deployment selection and the never-merged address overrides. Agung monetization raises `DEPLOYMENT_UNAVAILABLE`. |
| `TokenomicsActivationError` | Preflight: `CHAIN_MISMATCH`, `CONTRACT_NOT_DEPLOYED`, `PEER_MISMATCH`, `SIGNER_UNAVAILABLE`, `NOT_FULL_MODE`, `INVALID_TIER`, `PRICE_NOT_AVAILABLE`. Quote and funds: `MACHINE_ID_MISMATCH`, `MAX_NET_PEAQ_EXCEEDED`, `MAX_USDT_EXCEEDED`, `INSUFFICIENT_BALANCE`, `APPROVAL_FAILED`, `ALLOWANCE_INSUFFICIENT`, `SLIPPAGE_EXCEEDED`. Reads: `MACHINE_NOT_FOUND`, `READ_FAILED`. Management: `NOT_OWNER_OR_CONTROLLER`, `NOT_MACHINE_OWNER`, `AUTHENTICATION_REWRITE_REQUIRED`, `MACHINE_RELOCATING`, `ERC721_INCORRECT_OWNER`, `ERC721_INSUFFICIENT_APPROVAL`, `ERC721_INVALID_APPROVER`, `ERC721_INVALID_OPERATOR`. Submission: `CONTRACT_REVERTED`, `EVENT_MISMATCH`, `STATE_MISMATCH`, `ABORTED`, and a pending result when the receipt does not arrive: JS `RECEIPT_UNAVAILABLE` (activation) or `TRANSACTION_PENDING` (management writes), Python `PENDING_TRANSACTION` (as `TokenomicsPendingTransactionError` with `.submitted`) | Attributes `code`, `contract`, `operation`, `solidityError` / `solidity_error`, `revertData` / `revert_data`, `transactionHash` / `transaction_hash`. Never resubmit after a pending or receipt-unavailable result; reconcile the hash. `PEER_MISMATCH` on `machineBridgeAdapter` against `peaq-mainnet` means an SDK older than `@peaqos/peaq-os-sdk` 0.7.0 / `peaq-os-sdk` 0.7.1 (the adapter was re-pointed on 2026-09-08); upgrade. |
| `TokenomicsUnsupportedError` | `LEGACY_REGISTRATION_UNSUPPORTED` (`registerMachine`, `mintNft`, `tokenIdOf`), `SPONSORED_ACTIVATION_UNSUPPORTED` (`registerFor`, no replacement), `MACHINE_RELOCATION_UNAVAILABLE` (`bridgeNft`) | Deprecated entry points called in Tokenomics mode. Carries `operation` and `replacement`. |
| `TokenomicsIntegrationUnavailableError` | `TOKENOMICS_INTEGRATION_UNAVAILABLE` | Carries `integration` (`legacy DID helpers`, `orchestration identity binding`; `events` and `MCR/query` were removed in JS 0.7.0 / Python 0.7.1, which enable both in Tokenomics mode) and `owningTicket` / `owning_ticket`. Raised before any HTTP, RPC, or signing. |
| `MonetizationCompatibilityError` | `MONETIZATION_API_INCOMPATIBLE` | The 2.0 MCR's `/.well-known/peaq-monetization` signal is missing or does not match the deployment. `mcr-20.peaq.xyz` publishes the signal since 2026-09-05; you still see this against a self-hosted MCR that has not been upgraded, or when `TOKENOMICS_DEPLOYMENT_ID` names a different chain than the host serves. |
| `MonetizationApiError` (JS) / `MonetizationError` (Python) | Adds `MACHINE_UNAVAILABLE` (503, the only retryable code) and `CHAIN_UNAVAILABLE` to the 1.0 table | Branch on `code`, never on the message. Timeouts and cancellations are separate classes: Python `MonetizationTimeoutError` (`MONETIZATION_TIMEOUT`) and `MonetizationCancelledError` (`MONETIZATION_CANCELLED`); JS `MonetizationTimeoutError` and `MonetizationCancellationError`, which carry no `code`. |
Error messages pass through credential redaction (URL userinfo, secret query parameters, 32-byte hex). Revert data and transaction hashes are kept unredacted on the attributes.
***
## Faucet error codes
All 20 codes the Gas Station can return from `POST /faucet/fund`, `POST /2fa/setup`, and `POST /2fa/confirm`. Each endpoint returns a subset. For example, `INVALID_OWNER_ADDRESS` and `QR_GENERATION_FAILED` only come from `/2fa/setup`. Codes surface as `RuntimeError.code` (JS) or `ApiError.code` (Python).
| Code | Description | Retry |
| :------------------- | :--------------------------------------------------- | :--------------------------------------------- |
| `INVALID_2FA` | OTP rejected by the faucet | Yes: submit a fresh 6-digit code |
| `2FA_NOT_CONFIGURED` | Owner has not completed `setup_faucet_2fa` | No: re-run setup |
| `2FA_NOT_ACTIVE` | 2FA enrolled but not confirmed | No: call `confirm_faucet_2fa` with a valid OTP |
| `2FA_LOCKED` | Too many invalid attempts; owner temporarily blocked | After lockout window |
| Code | Description | Retry |
| :-------------------------- | :---------------------------------- | :-------------------------------------------------------- |
| `DUPLICATE_REQUEST` | Same `request_id` already in flight | No: use a different `request_id` or wait for prior result |
| `REQUEST_ALREADY_PROCESSED` | Same `request_id` already resolved | No: read the prior result |
| Code | Description | Retry |
| :-------------------- | :---------------------------------- | :------------------------------------- |
| `RATE_LIMITED` | Per-IP or per-wallet throttle hit | After cooldown |
| `CAP_EXCEEDED_OWNER` | Daily per-owner funding cap reached | Next day |
| `CAP_EXCEEDED_WALLET` | Daily per-target-wallet cap reached | Next day, or target a different wallet |
| Code | Description | Retry |
| :----------------------- | :-------------------------------------- | :------------------ |
| `INVALID_PAYLOAD` | Request body malformed | No: fix the payload |
| `INVALID_OWNER_ADDRESS` | `owner_address` not a recognized format | No |
| `INVALID_TARGET_ADDRESS` | `target_wallet_address` not recognized | No |
| `INVALID_CHAIN_ID` | `chain_id` not configured on the faucet | No |
| `INVALID_REQUEST_ID` | `request_id` not a UUID | No |
| Code | Description | Retry |
| :---------------- | :------------------------------------ | :---- |
| `TRANSFER_FAILED` | On-chain transfer reverted or stalled | Yes |
| `CHAIN_RPC_ERROR` | Faucet's RPC call failed | Yes |
| `INTERNAL_ERROR` | Faucet internal failure | Yes |
| Code | Description | Retry |
| :--------------------- | :--------------------------------------- | :--------------- |
| `QR_NOT_FOUND` | QR image expired or never created | No: re-run setup |
| `QR_EXPIRED` | QR retrieval attempted after \~2 min TTL | No: re-run setup |
| `QR_GENERATION_FAILED` | Faucet could not generate the image | Yes |
Full flow reference: [Gas Station concept](/peaqos/concepts/gas-station), [setupFaucet2FA](/peaqos/sdk-reference/sdk-js#setupfaucet2fa), [fund\_from\_gas\_station](/peaqos/sdk-reference/sdk-python#fund_from_gas_station).
***
## On-chain revert names (Tokenomics 1.0)
These are the `IdentityRegistry`, `IdentityStaking`, `MachineNFT`, and `EventRegistry` reverts. Economics 2.0 reverts are decoded per contract into the `TokenomicsActivationError` codes above. Revert names the SDK translates to a friendly message and surfaces as `RuntimeError.code` (JS) or `RpcError.code` (Python). The contracts define more custom errors than this table; anything not listed surfaces as `code: "TX_REVERTED"` (Python) or `code: ""` with a generic `Transaction reverted: …` message (JS, when viem decodes the selector).
| Revert | Raised by | Cause |
| :---------------------------- | :----------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AlreadyRegistered` | `registerMachine` / `registerFor` / `register_machine` | Address already has a machine ID |
| `InvalidMachineAddress` | `registerFor` | `machineAddress` is the zero address |
| `InvalidAddress` | Staking / NFT flows | Caller passed the zero address where a real address is required |
| `AmountZero` | Staking / bond flow | Bond amount is zero |
| `AlreadyStaked` | Staking flow | Machine is already bonded/staked |
| `MachineNotBonded` | `mintNft` / `mint_nft` | Caller's machine is not bonded on the IdentityRegistry |
| `AlreadyMinted` | `mintNft` / `mint_nft` | An NFT is already minted for the machine |
| `NotMachineOwner` | `mintNft` / `mint_nft` | Caller is not the owner of the machine |
| `RecipientMustBeMachineOwner` | `mint_nft` (Python) | Operator-mint guard: recipient must be the registered machine owner. Python-only — the JS SDK surfaces this revert as `code: ""` via the viem decode path. |
| `MachineNotFound` | `submitEvent` / `tokenIdOf` | machineId has no Identity record on the IdentityRegistry |
| `MachineDeactivated` | `submitEvent` | The machine has been deactivated by the IdentityRegistry |
| `NotAuthorizedSubmitter` | `submitEvent` | Caller is not authorized to submit events for this machine |
| `InvalidEventType` | `submitEvent` | `eventType` is not `0` (revenue) or `1` (activity) |
| `InvalidTrustLevel` | `submitEvent` | `trustLevel` is not `0`, `1`, or `2` |
## MCR API error codes
Returned by `queryMcr` / `query_mcr`, `queryMachine` / `query_machine`, and `queryOperatorMachines` / `query_operator_machines` against the MCR API: `mcr.peaq.xyz` in legacy mode, the deployment's 2.0 server (`mcr-20.peaq.xyz`) in Tokenomics mode since JS 0.7.0 / Python 0.7.1. Surfaced as `RuntimeError.code` (JS) or `ApiError.code` (Python). In Tokenomics mode a malformed or non-canonical `machine_id`, or a response about a different machine or operator than requested, is `BAD_RESPONSE`.
| Code | Cause | Retry |
| :-------------------- | :--------------------------------------------------------------------- | :---------------------------------------- |
| `NOT_FOUND` | The DID, machine, or token ID was not found by the MCR API (HTTP 404) | No: check the input |
| `BAD_RESPONSE` | The MCR returned an unexpected payload shape | No: file an issue if it persists |
| `HTTP_ERROR` | The MCR returned an unhandled non-2xx status | Maybe: surface the status code and decide |
| `SERVER_ERROR` | The MCR returned 5xx | Yes, with backoff |
| `SERVICE_UNAVAILABLE` | The MCR returned 503 (`Service not initialised` / `Chain unavailable`) | Yes, with backoff |
| `TIMEOUT` | The HTTP request exceeded `timeoutMs` | Yes |
| `NETWORK_ERROR` | Transport-level failure (DNS, TCP, TLS) | Yes |
| `ABORTED` | The caller aborted the request via `AbortSignal` (JS only) | Caller's choice |
## OWS signing error codes
Raised when transaction signing routes through an OWS vault wallet (`PeaqosClient.fromWallet` / `from_wallet` with `owsSigning=true`). The SDK normalises the upstream OWS error code into a typed SDK exception — only `INVALID_INPUT` becomes `ValidationError`; the other four become `PeaqosError` (or `RuntimeError` in JS) with the original OWS error preserved as `.cause`.
| Code | Cause | Surfaces as |
| :-------------------- | :--------------------------------------------- | :--------------------------------------- |
| `WALLET_NOT_FOUND` | Vault wallet name / UUID does not exist | `PeaqosError` (Py) / `RuntimeError` (JS) |
| `INVALID_PASSPHRASE` | Wrong vault passphrase | `PeaqosError` (Py) / `RuntimeError` (JS) |
| `INVALID_INPUT` | Malformed transaction or sign-hash payload | `ValidationError(field="transaction")` |
| `POLICY_DENIED` | Signing blocked by an OWS policy rule | `PeaqosError` (Py) / `RuntimeError` (JS) |
| `CHAIN_NOT_SUPPORTED` | Transaction `chainId` is not configured in OWS | `PeaqosError` (Py) / `RuntimeError` (JS) |
Constants exported from both SDKs as `OWS_ERROR_WALLET_NOT_FOUND`, `OWS_ERROR_INVALID_PASSPHRASE`, `OWS_ERROR_INVALID_INPUT`, `OWS_ERROR_POLICY_DENIED`, `OWS_ERROR_CHAIN_NOT_SUPPORTED`. JS additionally exports the `OwsSigningErrorCode` union type.
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { OWS_ERROR_INVALID_PASSPHRASE, OWS_ERROR_CHAIN_NOT_SUPPORTED } from "@peaqos/peaq-os-sdk";
try {
await client.bridgeNft({ tokenId, destination: "base" });
} catch (err) {
if (err.cause?.code === OWS_ERROR_INVALID_PASSPHRASE) { /* prompt re-auth */ }
if (err.cause?.code === OWS_ERROR_CHAIN_NOT_SUPPORTED) { /* OWS chain config bug */ }
throw err;
}
```
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import (
OWS_ERROR_INVALID_PASSPHRASE,
OWS_ERROR_CHAIN_NOT_SUPPORTED,
PeaqosError,
)
try:
client.bridge_nft(token_id=..., destination="base")
except PeaqosError as err:
code = getattr(err.__cause__, "code", None)
if code == OWS_ERROR_INVALID_PASSPHRASE:
... # prompt re-auth
if code == OWS_ERROR_CHAIN_NOT_SUPPORTED:
... # OWS chain config bug
raise
```
## SDK transaction sentinels
Raised by the SDK's transaction helper around any contract call. Surfaced as `RuntimeError.code` (JS) or `RpcError.code` (Python).
| Code | Cause |
| :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `WALLET_NOT_CONFIGURED` | Method requires a signer but none was configured. (JS only; the Python `PeaqosClient` constructor requires a `private_key` and surfaces this as `ValidationError`.) |
| `TX_REVERTED` | The transaction reverted on-chain. The revert reason is in the message; revert names from the table above are decoded into `code` when the SDK recognizes them. |
| `RECEIPT_AWAIT_FAILED` | Transaction was submitted but the SDK could not retrieve a receipt. The tx may still have landed; re-query by hash before retrying. |
| `REGISTERED_EVENT_MISSING` | Receipt has no `Registered` event log. Indicates a contract/SDK ABI mismatch. |
| `REGISTERED_EVENT_MALFORMED` | `Registered` event log was decoded but had unexpected fields. Same root cause as above. |
| `INVALID_FEE_RESULT` | `quoteSend` on the LayerZero ONFT adapter returned a malformed `MessagingFee`. Raised inside `bridge_nft` (Python) before submission. |
## Faucet / HTTP envelope sentinels
Raised by the SDK when a faucet or MCR response is reachable but unparseable. Surfaced as `ApiError.code` (Python). The JS SDK collapses these into the generic `RuntimeError` envelope path.
| Code | Cause |
| :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_RESPONSE` | Faucet returned a non-JSON body. |
| `UNEXPECTED_RESPONSE` | Faucet returned JSON but the `status` / `code` / `data` envelope did not match the expected shape for the endpoint. |
| `NETWORK_ERROR` | Transport-level failure during a faucet call (DNS, connection refused, TLS, timeout via `requests.RequestException`). Same sentinel as the MCR API table above. |
## Client-side error codes
Raised by the SDK itself (not by a chain revert). Surfaced as `RuntimeError.code` (JS) or `ValidationError`/`RpcError` attributes (Python).
| Code | Raised by | Cause |
| :------------------------ | :------------------------------------------ | :------------------------------------------------------------------------------------- |
| `MIN_BOND_INVALID_RESULT` | `registerMachine` / `registerFor` (JS only) | `IdentityRegistry.minBond()` returned a non-`bigint` value before submitting the bond. |
***
## Error handling patterns
```typescript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
PeaqosError,
ValidationError,
RuntimeError,
RateLimitExceeded,
} from "@peaqos/peaq-os-sdk";
try {
await client.fundFromGasStation(params, faucetUrl);
} catch (err) {
if (err instanceof ValidationError) {
// fix caller input
} else if (err instanceof RateLimitExceeded) {
// back off
} else if (err instanceof RuntimeError) {
switch (err.code) {
case "INVALID_2FA":
// prompt for a fresh TOTP
break;
case "CAP_EXCEEDED_OWNER":
case "CAP_EXCEEDED_WALLET":
// surface cap messaging
break;
default:
throw err;
}
} else {
throw err;
}
}
```
```python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk import PeaqosError, ValidationError, ApiError, RpcError
try:
client.fund_from_gas_station(
owner_address=owner,
target_wallet_address=target,
chain_id="peaq",
two_factor_code=totp,
faucet_base_url=faucet_url,
)
except ValidationError:
# fix caller input
raise
except ApiError as err:
if err.code == "INVALID_2FA":
# prompt for a fresh TOTP
pass
elif err.code in ("CAP_EXCEEDED_OWNER", "CAP_EXCEEDED_WALLET"):
# surface cap messaging
pass
else:
raise
except RpcError:
# retry with backoff
raise
```
***
## Related
* [SDK JS](/peaqos/sdk-reference/sdk-js)
* [SDK Python](/peaqos/sdk-reference/sdk-python)
* [Gas Station concept](/peaqos/concepts/gas-station)
* [Install page troubleshooting](/peaqos/install)
# Monetize: Presence heartbeat
Source: https://docs.peaq.xyz/peaqos/sdk-reference/heartbeat
The machine-side heartbeat client: sign and push a presence heartbeat at a configurable interval, and query any machine's presence.
The heartbeat client ships in `@peaqos/peaq-os-sdk` 0.5.0+ (JS/TS) and `peaq-os-sdk` 0.5.0+ (Python) under the `monetization` namespace. It talks to the dedicated peaqOS heartbeat service; the peaq-hosted instance is `https://heartbeat.peaq.xyz`, and the base URL stays configuration, never a constant.
A machine that has [opted into monetization](/peaqos/sdk-reference/monetization-opt-in) reports that it is online by pushing a signed heartbeat to the heartbeat service at a regular interval. A valid heartbeat keeps the machine **online**; when heartbeats stop, the server marks it offline. The client does not enforce the opt-in state itself: start it only on an opted-in machine (check with `getMonetization` / `get_monetization`), and stop it when the machine opts out.
## Push, not poll
The machine is a *client* of the heartbeat service: it pushes a signed heartbeat out. It does not host an endpoint that others poll, and it never serves its own availability.
```mermaid theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
sequenceDiagram
participant M as Machine (SDK client)
participant H as Heartbeat service
loop every intervalMs
M->>H: POST /api/v1/heartbeats {machineId, publicKey, sentAt, signature}
H->>H: verify signature, store presence (online for the TTL)
H-->>M: {accepted, online, lastHeartbeatAt}
end
```
## The 180-second TTL and the interval bound
A valid heartbeat marks the machine online for a fixed presence TTL of **180 seconds**. Miss that window and the server flips the machine to offline. To stay online, the machine must heartbeat comfortably more often than the TTL, so the client caps the configured interval:
| Constant | Value | Meaning |
| :--------------- | :---- | :---------------------------------------------------------------------------------------------------------------- |
| Presence TTL | 180 s | Server presence window (`PRESENCE_TTL_MS` in JS, `PRESENCE_TTL_SECONDS` in Python). |
| Heartbeat margin | 60 s | Safety margin absorbing request latency plus one retry (`HEARTBEAT_MARGIN_MS` / `HEARTBEAT_MARGIN_SECONDS`). |
| Max interval | 120 s | The maximum accepted interval, TTL minus margin (`MAX_HEARTBEAT_INTERVAL_MS` / `MAX_HEARTBEAT_INTERVAL_SECONDS`). |
`start` rejects an interval that is zero, negative, or above the maximum. The exact interval within that bound is caller-supplied configuration.
**Offline is server-derived.** The client never self-reports offline. Stopping the client simply stops heartbeating; the server observes the absence and lets presence lapse. Operator-initiated maintenance is expressed the same way: stop the client.
## Request-signing auth
There is no bearer credential. The server authenticates each heartbeat by verifying the `signature` against the `publicKey` in the payload.
* The machine identity **private key** is held only by your signer implementation. The client passes it a canonical message and receives a hex signature; the client never handles or logs the raw key.
* `publicKey`, `signature`, `machineId`, and `sentAt` are not secret and are safe to log.
The signature covers exactly this canonical message, byte for byte (two lines, LF-joined, no trailing newline):
```
machineId: {machineId}
sentAt: {sentAt}
```
Build it with `buildCanonicalMessage` / `build_canonical_message`, the single place this format is defined.
## Quick start
```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { HeartbeatClient } from "@peaqos/peaq-os-sdk";
// or: const heartbeat = client.monetization.createHeartbeatClient();
const heartbeat = new HeartbeatClient();
heartbeat.start({
baseUrl: "https://heartbeat.peaq.xyz",
intervalMs: 30_000, // 0 < intervalMs <= 120_000
machineId: "machine-abc-123",
publicKey: "0x04…",
signer, // holds the machine identity private key
onResult: (res) => {
// res: { accepted, online, lastHeartbeatAt }
console.log("online:", res.online);
},
});
// on shutdown or maintenance:
heartbeat.stop();
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk.monetization import HeartbeatClient, HeartbeatConfig
heartbeat = HeartbeatClient()
heartbeat.start(HeartbeatConfig(
base_url="https://heartbeat.peaq.xyz",
interval_seconds=30, # 0 < interval_seconds <= 120
machine_id="machine-abc-123",
public_key="0x04…",
signer=signer, # holds the machine identity private key
on_result=lambda res: print("online:", res.online),
))
# on shutdown or maintenance:
heartbeat.stop()
```
Each tick sends exactly `{ machineId, publicKey, sentAt, signature }` and parses `{ accepted, online, lastHeartbeatAt }`, surfaced through the optional result callback.
## Fire-and-forget
A failed push, whether a network error or an `accepted: false` response, is logged and retried on the next tick. It never throws out of the timer and never blocks the machine's other operations. Every parsed response reaches the result callback, including rejections.
A hung push cannot stall the loop either. In JS/TS the whole tick (signing plus push) is time-bounded to 30 seconds, or the interval if shorter. In Python the bound is per request: the transport's 10-second HTTP timeout, with signing cancelled cooperatively rather than forced. A server that accepts the connection but never responds is timed out, logged, and retried on the next tick like any other failure.
`stop()` is the one exception: it cancels the timer, aborts any in-flight request (including one still signing), and is treated as expected cancellation, not a failure. No error log, no result callback, no retry.
## Presence query
`checkPresence` / `check_presence` is a standalone one-shot query with no scheduler and no signing. Pass exactly one of `machineId` or `publicKey`:
```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import { checkPresence } from "@peaqos/peaq-os-sdk";
const presence = await checkPresence(
{ machineId: "machine-abc-123" },
{ baseUrl: "https://heartbeat.peaq.xyz" },
);
// { online, machineId, publicKey, lastHeartbeatAt }
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk.monetization import PresenceCheckRequest, check_presence
presence = check_presence(
PresenceCheckRequest(machine_id="machine-abc-123"),
base_url="https://heartbeat.peaq.xyz",
)
# presence.online, .machine_id, .public_key, .last_heartbeat_at
```
When the machine is offline or unknown, `online` is `false` and `publicKey` / `lastHeartbeatAt` are `null`.
## Presence only
The heartbeat payload is a pure presence signal: it carries no status field and no queue depth. Presence is binary under this API: online (a valid heartbeat within 180 seconds) or offline (TTL expired). Utilisation states such as `busy` are not represented, and maintenance is expressed by stopping the client. Do not add extra fields to the payload.
## API summary
| Symbol | Role |
| :-------------------------------------------------------------------- | :------------------------------------------------------- |
| `HeartbeatClient` | `start(config)` / `stop()` presence scheduler. |
| `HeartbeatSigner` | `sign(message)` returning hex; holds the private key. |
| `buildCanonicalMessage` | Byte-for-byte canonical message builder. |
| `HeartbeatTransport` | Wire and HTTP seam; swap it for tests or a custom stack. |
| `checkPresence` | One-shot presence query. |
| `PRESENCE_TTL_MS`, `HEARTBEAT_MARGIN_MS`, `MAX_HEARTBEAT_INTERVAL_MS` | Interval bounds. |
## Related
* [Monetize function](/peaqos/functions/monetize): where the heartbeat fits in the flow
* [Provisioning SDK reference](/peaqos/sdk-reference/provisioning): provision the provider node first
* [Monetization opt-in API](/peaqos/api-reference/put-machine-monetization)
# Monetize: Opt-in
Source: https://docs.peaq.xyz/peaqos/sdk-reference/monetization-opt-in
Toggle a machine's monetization on or off from the SDK, and read its state: the client for the MCR API's signed opt-in endpoint.
**Economics 2.0 monetization is live on `peaq-mainnet` since 2026-09-05.** `@peaqos/peaq-os-sdk` 0.6.0 and `peaq-os-sdk` 0.6.0 talk to the 2.0 MCR at `https://mcr-20.peaq.xyz`, which now publishes the `/.well-known/peaq-monetization` signal (`tokenomics-2.0-monetization-v1`, chain `3338`, the `MachineRegistry` address). Reads were verified against the mirrored 2.0 machines on 2026-09-05 (`getMonetization` returns `PENDING` for a machine that never opted in); writes were not exercised. `agung-2026-08-28` still has no paired MCR (`DEPLOYMENT_UNAVAILABLE`). Tokenomics 1.0 machines stay on `mcr.peaq.xyz` through `@peaqos/peaq-os-sdk@0.5.0` / `peaq-os-sdk==0.5.0` (Python 3.11 or newer) with the [1.0 configuration](#tokenomics-1-0-sdk-0-5-0).
An authorized signer turns a machine's monetization on or off with a signed EIP-191 toggle, and anyone can read the current state. The SDK builds the byte-exact canonical message, signs it, and handles the coded error envelope, so you never assemble the message by hand. State is stored off-chain in the MCR server; no on-chain transaction is made.
Opting **in** requires the machine to be bonded and not deactivated (server-enforced). Opting **out** is always allowed. The read is public and doubles as the pre-provisioning check.
## Economics 2.0 (SDK 0.6.0+)
The configuration is a deployment ID. The SDK resolves the MCR URL, chain ID, `MachineRegistry` address, and protocol version from the same deployment record that activation uses; callers cannot supply or override any of them.
```ts JavaScript theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import {
optIn,
optOut,
getMonetization,
eip191SignerFromPrivateKey,
type MonetizationConfig,
} from "@peaqos/peaq-os-sdk";
const config: MonetizationConfig = {
deploymentId: "peaq-mainnet",
signer: eip191SignerFromPrivateKey(privateKey), // the machine's current owner or DID controller key
};
const machineId = 57896044618658097711785492504343953926634992332820282019728792003956564819975n;
// Opt in (machine must be bonded and not deactivated).
const state = await optIn(config, machineId);
console.log(state.status); // "OPTED_IN"
// Opt out (always allowed).
await optOut(config, machineId);
// Public read, no signer needed: the pre-provisioning check.
const current = await getMonetization(config, machineId);
if (current.status !== "OPTED_IN") {
throw new Error("Opt in before provisioning");
}
```
```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
from peaq_os_sdk.monetization import (
Eip191AccountSigner,
MonetizationConfig,
get_monetization,
opt_in,
opt_out,
)
config = MonetizationConfig(
deployment_id="peaq-mainnet",
signer=Eip191AccountSigner.from_key(private_key), # the machine's current owner or DID controller key
)
machine_id = 57896044618658097711785492504343953926634992332820282019728792003956564819975
# Opt in (machine must be bonded and not deactivated).
state = opt_in(config, machine_id)
print(state.status) # "OPTED_IN"
# Opt out (always allowed).
opt_out(config, machine_id)
# Public read, no signer needed: the pre-provisioning check.
current = get_monetization(config, machine_id)
if current.status != "OPTED_IN":
raise RuntimeError("Opt in before provisioning")
```
### Machine key
Every call takes the machine as a full-width `uint256` (`bigint` in JS, `int` in Python) or as `did:peaq:`. Address-form DIDs (`did:peaq:0x…`), decimal strings in JS, leading zeros, signs, and exponents are rejected locally before any HTTP. There is no lookup from a 1.0 address DID to a 2.0 machine ID.
### Who may sign
The machine's **current `MachineRegistry` owner or DID controller**. A legacy machine-wallet key or a 1.0 EventRegistry operator key is not authorized (`403 UNAUTHORIZED_SIGNER`). The canonical message binds `registry: ` and `chain_id: 3338`.
### Configuration
| Field | Notes |
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------- |
| `deploymentId` / `deployment_id` | `"peaq-mainnet"` only. `"agung-2026-08-28"` has no paired MCR and raises `TokenomicsConfigError` `DEPLOYMENT_UNAVAILABLE`. |
| `signer` | Owner or controller key. Writes only; reads are unsigned. |
| `fetch` (JS) / `transport` (Python) | Optional low-level HTTP adapter. It receives only SDK-built, deployment-bound URLs and cannot change the base URL. |
The resolved values are readable (`api_base`, `registry`, `chain_id`, `api_version` in Python) but not settable. `apiBase`, `registry`, and `chainId` as constructor fields are gone; passing them is a `TypeError` in Python and a type error in TypeScript.
### Compatibility check
Before every read and write the SDK fetches `GET {apiBase}/.well-known/peaq-monetization` and requires the exact protocol version (`tokenomics-2.0-monetization-v1`, exported as `TOKENOMICS_MONETIZATION_API_VERSION`), the canonical decimal chain ID, and the lowercased `MachineRegistry` address from the selected deployment. A write runs this check **before** invoking the signer. Missing, malformed, or mismatched signals raise `MonetizationCompatibilityError` (code `MONETIZATION_API_INCOMPATIBLE`). Not cached, cannot be bypassed.
### Retries, deadline, cancellation
| Option | Default | Behaviour |
| :------------------------------------------------------------ | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `maxUnavailableRetries` / `max_unavailable_retries` | `2` | Retries a write after `503 MACHINE_UNAVAILABLE`, the only retryable code. Each attempt re-signs with a fresh timestamp. Reads are not retried. |
| `unavailableRetryDelayMs` / `unavailable_retry_delay_seconds` | `1000` ms / `1.0` s | Pause before each retry. |
| `timeoutMs` / `timeout_seconds` | `30000` ms / `30.0` s | One end-to-end deadline covering validation, the compatibility check, signing, retries, and response parsing. Retries do not reset it. Raises `MonetizationTimeoutError`. |
| `signal` / `cancel` | none | Caller abort. Raises `MonetizationCancellationError` (JS) / `MonetizationCancelledError` (Python). |
Python passes these through `GetMonetizationOptions` and `SetMonetizationOptions`.
### The response
| Field | Notes |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `machineId` / `machine_id` | The machine ID, parsed losslessly. On the wire it is a canonical decimal **string**; a JSON number is rejected. |
| `status` | `"PENDING"`, `"OPTED_IN"`, or `"OPTED_OUT"`. `PENDING` means never opted in or out. 2.0 machines start at `PENDING`; 1.0 decisions are not imported. |
| `signer` | Address that authorized the current state; `null`/`None` only when `PENDING`. |
| `updatedAt` / `updated_at` | Unix seconds of the last change; `null`/`None` only when `PENDING`. |
### Errors
| Class (JS / Python) | Code | Meaning |
| :-------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TokenomicsConfigError` | `DEPLOYMENT_UNKNOWN`, `DEPLOYMENT_UNAVAILABLE`, `DEPLOYMENT_NOT_CONFIGURED` | Bad or missing deployment; agung has no monetization |
| `MonetizationCompatibilityError` | `MONETIZATION_API_INCOMPATIBLE` | The `/.well-known/peaq-monetization` signal is missing or does not match the deployment |
| `MonetizationApiError` / `MonetizationError` | `UNAUTHORIZED_SIGNER`, `MACHINE_NOT_BONDED`, `MACHINE_DEACTIVATED`, `STALE_SIGNATURE`, `MACHINE_UNAVAILABLE` (503, retryable), `INVALID_REQUEST`, … | Server-side coded envelope; branch on the code, never the message. Full table on the [PUT endpoint page](/peaqos/api-reference/put-machine-monetization#error-responses) |
| `MonetizationNetworkError`, `MonetizationResponseError`, `MonetizationTimeoutError`, `MonetizationCancellationError` / `MonetizationCancelledError` | | Transport, malformed response, deadline, caller abort |
| `MonetizationValidationError` | | Rejected machine key or option locally |
Re-sending the identical signed request surfaces `STALE_SIGNATURE`; re-signing the same state with a newer timestamp succeeds, so the toggle is idempotent to retry.
## Tokenomics 1.0 (SDK 0.5.0)
The previous client targets `mcr.peaq.xyz` and serves machines registered through `IdentityRegistry`. Available by pinning `@peaqos/peaq-os-sdk@0.5.0` / `peaq-os-sdk==0.5.0` (Python 3.11 or newer), or from `peaq-os-cli<0.0.8` pinned together with `peaq-os-sdk<0.6.0`. The CLI pin alone resolves to CLI 0.0.7 with SDK 0.7.0, and `peaqos monetize` then crashes with a `TypeError`.
```ts JavaScript (0.5.0) theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
const config: MonetizationConfig = {
apiBase: "https://mcr.peaq.xyz",
registry: IDENTITY_REGISTRY_ADDRESS,
chainId: 3338n,
signer: eip191SignerFromPrivateKey(privateKey), // machine wallet, owner, or on-chain operator key
};
const state = await optIn(config, 42n);
```
```python Python (0.5.0) theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
config = MonetizationConfig(
api_base="https://mcr.peaq.xyz",
registry=IDENTITY_REGISTRY_ADDRESS,
chain_id=3338,
signer=Eip191AccountSigner.from_key(private_key), # machine wallet, owner, or on-chain operator key
)
state = opt_in(config, 42)
```
In 1.0 the signed message binds the `IdentityRegistry` address, the authorized signers are the machine wallet, the owner, or the on-chain operator, `getMonetization` also accepts `did:peaq:0x`, and machine IDs fit a JS `number` range but the SDK still uses `bigint`. `registry` plus `chainId` are the domain binding: a signature made against the wrong pair recovers a non-authorized address and the server rejects it with `403 UNAUTHORIZED_SIGNER`; `verifyLocalRecovery` / `verify_local_recovery` recovers the signer locally so you catch a wrong key before a write.
## From the terminal
The CLI wraps this surface as [`peaqos monetize opt-in | opt-out | status`](/peaqos/cli#peaqos-monetize); all signing and HTTP stay in the SDK.
## Related
* [Monetize function](/peaqos/functions/monetize): where opt-in fits in the flow
* [Provisioning SDK reference](/peaqos/sdk-reference/provisioning): the step after opting in
* [PUT /machine/\{key}/monetization](/peaqos/api-reference/put-machine-monetization): the 1.0 wire contract, canonical message, and error codes
* [Economics 2.0](/peaqos/concepts/economics-2-0): what changed and what is live
# Orchestration (JavaScript)
Source: https://docs.peaq.xyz/peaqos/sdk-reference/orchestration-js
client.orchestration.* namespace on the JS/TS SDK — machines, agent pairings, machine agents, runtime endpoints, skills, market services, market search.
The JS/TS SDK exposes the [Machine Markets API](/peaqos/api-reference/machine-markets-overview) as a typed namespace on the existing `PeaqosClient`. Same client, additive surface. The flat method layout matches the HTTP API one-to-one.
## Setup
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
import 'dotenv/config';
import { PeaqosClient } from "@peaqos/peaq-os-sdk";
const client = PeaqosClient.fromEnv();
const { items } = await client.orchestration.listMachines({ limit: 20 });
```
Touching `client.orchestration` without `orchestrationUrl` configured throws `OrchestrationConfigError`. The namespace lazy-initialises and caches per client.
## Configuration
| Field | Env var (via `fromEnv()`) | Notes |
| :----------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------- |
| `orchestrationUrl` | `PEAQOS_ORCHESTRATION_URL` | Required to use the namespace. Must parse as `http:` or `https:`. Non-loopback `http://` triggers a warning. |
| `apiKey` | `PEAQOS_API_KEY` | Required for platform-auth calls. Sent as `x-api-key`. |
Constructor form when not using env:
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
new PeaqosClient({
rpcUrl,
privateKey,
contracts,
orchestrationUrl: "https://orchestration.peaq.xyz",
apiKey: "...",
});
```
Transport constants:
* `API_BASE_PATH = "/api/v1"`, prepended to every path.
* Timeouts: 30 s GET, 60 s POST/PATCH/PUT/DELETE.
* A `peaq-os-sdk-js/…` `User-Agent` header.
No retry, no exponential backoff. Six auto-paginated iterators are available; see [Pagination](#pagination) below.
## Common envelopes
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
type ListResponse = {
readonly items: readonly T[];
readonly nextCursor?: string; // omitted when there is no next page
};
type ItemResponse = {
readonly item: Readonly;
};
type ListQuery = {
readonly limit?: number; // 1..500, default 100
readonly cursor?: string; // opaque, omit on first page
};
```
`204 No Content` resolves to `void`. `validateListQuery` rejects `limit < 1` or `limit > 500` with `OrchestrationValidationError` before the HTTP call.
## Machines
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.orchestration.listMachines(options?: {
cursor?: string;
limit?: number;
}): Promise>
client.orchestration.createMachine(params: CreateMachineRequest)
: Promise>
client.orchestration.getMachine(machineId: string)
: Promise>
client.orchestration.updateMachine(machineId: string, params: UpdateMachineRequest)
: Promise>
client.orchestration.archiveMachine(machineId: string): Promise
```
`Machine` shape:
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
type Machine = {
readonly id: string;
readonly displayName: string;
readonly status: "draft" | "active" | "degraded" | "blocked" | "archived";
readonly ownerId: string;
readonly identityRef?: string | null;
readonly identityProof?: {
readonly method: "eip191";
readonly identityRef: string;
readonly signerAddress: string;
readonly challengeId: string;
readonly verifiedAt: string;
readonly challengeExpiresAt: string;
readonly controllerAddresses: readonly string[];
readonly resolutionSource: "peaqos-mcr";
} | null;
readonly machineType: string;
readonly runtimeProfile: string;
readonly capabilities: readonly string[];
readonly labels: Readonly>;
readonly policyIds: readonly string[];
readonly skillKeys: readonly string[];
readonly createdAt: string;
readonly updatedAt: string;
};
```
## Machine identity challenges
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.orchestration.createMachineIdentityChallenge(
params: CreateMachineIdentityChallengeRequest,
): Promise>
```
Requests a server-issued challenge tied to a `did:peaq:0x...` or `peaqos:machine:` identity reference. The DID controller signs `item.message` with EIP-191 `personal_sign` and submits the resulting `{ challengeId, signature }` as `identityProof` when creating or updating the machine record.
## Agent pairings
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.orchestration.listAgentPairings(machineId: string, options?: ListAgentPairingsOptions)
: Promise>
client.orchestration.createAgentPairingChallenge(
machineId: string,
params: CreateAgentPairingChallengeRequest,
): Promise>
client.orchestration.createAgentPairing(
machineId: string,
params: CreateAgentPairingRequest,
): Promise>
client.orchestration.createAgentPairingSession(
machineId: string,
pairingId: string,
params: CreateAgentPairingSessionRequest,
): Promise>
client.orchestration.updateAgentPairing(
machineId: string,
pairingId: string,
params: UpdateAgentPairingRequest,
): Promise>
client.orchestration.revokeAgentPairing(machineId: string, pairingId: string)
: Promise
```
Pairing is challenge-based end to end:
1. `createAgentPairingChallenge` returns a server-issued challenge keyed to `agentAddress`, `agentProvider`, `agentRole`, and optional `agentDid`.
2. The Machine Agent signs `item.message` (EIP-191) with the wallet key behind `agentAddress`.
3. `createAgentPairing` accepts the proof in `params.agentProof` and returns the pairing with a signed HS256 session JWT in `pairingToken`. The token is returned once at create.
4. `createAgentPairingSession` rotates the session token before expiry with a fresh proof. Required after any `updateAgentPairing` to the delegation policy, since policy changes invalidate the current token's `delegationPolicyHash`.
`createAgentPairing` and `createAgentPairingSession` return the `pairingToken` exactly once each. `toJSON` redacts the token to `"[REDACTED]"` so it does not leak through `JSON.stringify`.
`CreateAgentPairingRequest` now requires `agentProof: { challengeId, signature }` and accepts optional `agentDid`. `delegationPolicy.allowedServiceIds` and `delegationPolicy.deniedServiceIds` join the existing skill-level allow/deny lists.
## Machine agents
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.orchestration.listMachineAgents(machineId: string)
: Promise>
client.orchestration.enrollMachineAgent(
machineId: string,
params?: EnrollMachineAgentRequest,
): Promise>
client.orchestration.revokeMachineAgent(machineId: string, agentId: string)
: Promise
client.orchestration.machineAgentHeartbeat(
params: MachineAgentHeartbeatRequest,
): Promise
```
`enrollMachineAgent` POSTs `/machines/:machineId/agents/enrollment` and returns a one-time `provisioningToken`. `machineAgentHeartbeat` uses no auth header — the `agentToken` rides in the request body.
## Runtime endpoints
```ts theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
client.orchestration.listRuntimeEndpoints(machineId: string, options?: ListRuntimeEndpointsOptions)
: Promise>
client.orchestration.getRuntimeEndpoint(machineId: string, providerKey: string)
: Promise