Metadata-Version: 2.4
Name: peaq-os-sdk
Version: 0.11.2
Summary: Python SDK for peaqOS, the operating system for the machine economy — on-chain identity, credit rating, and omnichain infrastructure for robots and machines.
Author-email: peaqOS <info@peaq.xyz>
License: Apache-2.0
Project-URL: Homepage, https://www.peaq.xyz
Project-URL: Documentation, https://docs.peaq.xyz
Project-URL: Repository, https://github.com/peaqnetwork/peaq-os-sdk-py
Project-URL: Issues, https://github.com/peaqnetwork/peaq-os-sdk-py/issues
Keywords: peaq,peaqos,peaq-os,machine-economy,depin,robotics,ai-agents,blockchain,omnichain,peaqid,did,machine-nft,machine-tokenization,machine-credit-rating,mcr,sdk,python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-Infineon
Requires-Dist: cryptography==50.0.0
Requires-Dist: rfc8785==0.1.4
Requires-Dist: pycryptodome>=3.20.0
Requires-Dist: PyNaCl>=1.5.0
Requires-Dist: web3>=6.0
Requires-Dist: aiohttp<3.15,>=3.14.3
Requires-Dist: eth-account
Requires-Dist: requests<2.34,>=2.33.1
Requires-Dist: urllib3<2.9,>=2.8.0
Requires-Dist: idna<4,>=3.15
Requires-Dist: posthog>=3.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: google-re2>=1.1
Provides-Extra: ows
Requires-Dist: open-wallet-standard>=1.3.2; extra == "ows"
Provides-Extra: solana
Requires-Dist: anchorpy<0.22,>=0.21.0; extra == "solana"
Requires-Dist: solders>=0.21.0; extra == "solana"
Requires-Dist: solana>=0.36.0; extra == "solana"
Provides-Extra: p2p
Requires-Dist: libp2p<1.0,>=0.6.0; extra == "p2p"
Provides-Extra: x402
Requires-Dist: x402>=2.14.0; extra == "x402"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests<2.34,>=2.33; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: setuptools>=61.0; extra == "dev"
Requires-Dist: wheel; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: python-dotenv; extra == "dev"
Requires-Dist: anchorpy<0.22,>=0.21.0; extra == "dev"
Requires-Dist: solders>=0.21.0; extra == "dev"
Requires-Dist: solana>=0.36.0; extra == "dev"
Requires-Dist: x402>=2.14.0; extra == "dev"
Requires-Dist: open-wallet-standard>=1.3.2; extra == "dev"
Dynamic: license-file

# peaq-os-sdk

Python SDK for the peaqOS protocol. Provides a typed wrapper around the peaq on-chain capabilities so integrators can onboard machines, submit events, mint NFTs, and query credit ratings without writing raw `web3.py` boilerplate.

---

## Tokenomics 2.0 machine-ID compatibility

Tokenomics machine IDs are non-boolean Python `int` values in the full range `0 <= machine_id < 2**256`. At JSON, URL, log, or other text boundaries they must be canonical base-10 strings. ID size never selects a mode: a non-`None` `client.tokenomics20` configuration explicitly selects Tokenomics mode.

```python
from peaq_os_sdk import (
    parse_tokenomics_machine_id,
    serialize_tokenomics_machine_id,
    validate_tokenomics_machine_id,
)

machine_id = parse_tokenomics_machine_id("9223372036854775808")
validate_tokenomics_machine_id(machine_id)
assert serialize_tokenomics_machine_id(machine_id) == "9223372036854775808"
```

| Surface | Tokenomics 2.0 status |
|---|---|
| Machine-ID validator and decimal parser/serializer | Migrated and lossless through `2**256 - 1` |
| Event submission | **Enabled**: routes through the existing `EventRegistry` address and ABI, whose `submitEvent` signature matches the Tokenomics 2.0 contract's. That contract authorizes via `MachineRegistry` `ownerOf`/`controllerOf`, so the signer must be the machine's owner or DID controller |
| Legacy machine/proxy DID writers | Disabled locally before batch transaction work |
| MCR queries | **Enabled** against the deployment-selected MCR-20 endpoint: decimal machine DIDs in, canonical decimal wire strings parsed to lossless `int`, requests correlated to responses. `api_url` / `PEAQOS_MCR_API_URL` are not consulted |
| Orchestration identity-dependent machine reads/writes, pairing challenge, and market search | Disabled locally before HTTP because these endpoints resolve, return, or require unverified MCR identity state |
| Other Machine Markets records, purchases, delivery, and P2P | Supported as opaque strings such as `mach_...` when the endpoint does not resolve or verify MCR identity; never convert these IDs to integers or pass them to contracts |
| Registration, NFT, and bridge wrappers | Legacy-only; replacement and local disablement require coordinated Tokenomics activation work |
| Monetisation toggle/read | Migrated for approved paired deployments: full-width decimal IDs, `MachineRegistry` owner/controller signatures, live MCR compatibility checks, and independent Tokenomics state |

Still-disabled surfaces raise `TokenomicsIntegrationUnavailableError` with code `TOKENOMICS_INTEGRATION_UNAVAILABLE`, plus `.integration` and `.owning_ticket`, at their first local boundary. Select the mode with `Tokenomics20Config(deployment_id=...)`, or `TOKENOMICS_DEPLOYMENT_ID` for `PeaqosClient.from_env()`. See [MCR queries](docs/03_QUERIES.md#tokenomics-20-mode), [events](docs/03_EVENTS.md), and the [machine-ID inventory](docs/17_MACHINE_ID_INVENTORY.md) for every audited field and owner.

---

## Tokenomics 2.0 machine activation

On peaq, one transaction mints the machine NFT, stores its DID document, activates its subscription, and registers its home chain — replacing the deprecated `register_machine()` + `mint_nft()` pair.

Activation is opt-in. Supplying `tokenomics20` selects a deployment by ID from the SDK's approved snapshot; contract addresses are never passed by callers. A client without it keeps its existing behaviour unchanged.

```python
from peaq_os_sdk import PeaqosClient, Tokenomics20Config

client = PeaqosClient(
    rpc_url="https://peaq-agung.api.onfinality.io/public",
    private_key=os.environ["PRIVATE_KEY"],
    # ... existing contract addresses ...
    tokenomics20=Tokenomics20Config(deployment_id="agung-2026-08-28"),
)

preview = client.preview_machine_activation(params)   # no signing, no writes
result = client.activate_machine(params)              # one atomic transaction
print(result.machine_id, result.net_peaq_amount)
```

Selecting a deployment also **disables** the deprecated registration, mint, and bridge entry points, which then raise `TokenomicsUnsupportedError` locally — before any RPC, approval, or signature — rather than quietly doing the old thing. Outside Tokenomics mode they keep working and emit a `DeprecationWarning`.

| Deprecated | Code | Replacement |
|---|---|---|
| `register_machine()` | `LEGACY_REGISTRATION_UNSUPPORTED` | `activate_machine()` |
| `register_for()` | `SPONSORED_ACTIVATION_UNSUPPORTED` | none — operator-sponsored onboarding has no equivalent |
| `mint_nft()` | `LEGACY_REGISTRATION_UNSUPPORTED` | minting happens inside `activate_machine()` |
| `token_id_of()` | `LEGACY_REGISTRATION_UNSUPPORTED` | `get_machine_owner()` — the ID *is* the token ID |
| `bridge_nft()` | `MACHINE_RELOCATION_UNAVAILABLE` | none in this release |

See [docs/18_TOKENOMICS_ACTIVATION.md](docs/18_TOKENOMICS_ACTIVATION.md) for configuration, the preview/activate flow, the bond and voucher arithmetic, the full error taxonomy, reconciling an unconfirmed transaction, the machine-signed/operator-controlled migration path, and the release-day procedure for adding a network.

---

## Tokenomics 2.0 machine management

After onboarding, the same deployment-selected client exposes aggregate state and availability reads, suspend/resume, PEAQ and USDT subscriptions, ERC-721 ownership, DID updates, side-effect-free action previews, and read-only reconciliation for a known transaction hash. Owner, controller, and payer authority remain distinct, and every write correlates contract-scoped events before reconciling post-state.

See [docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md](docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md) for the authority matrix, bounded payment flows, migration notes, relocation-read semantics, and remaining release gates.

---

## Solana onboarding and reads

The existing activation APIs onboard a Solana machine terminal-first (CORE-801): the wallet is registered on peaq, the activation is requested, credited, finalised and bonded on Solana, the machine is minted against that bonded request and then linked to peaq. Keyless previews and status, staged submission, a durable journal and recovery cover every phase. Existing EVM activation behavior is unchanged.

The link push travels one of two ways, chosen by the deployment, not the caller. Both mainnet sets run a CORE-781 adapter (`adapter_generation="core_781"`; `peaq-mainnet` since 2026-10-06): the SDK reads `info_desk`'s route and, as production is configured, queues the push for peaq's Trust Validator node, which delivered it to peaq in 44 to 91 seconds on the production runs: no LayerZero fee, `max_link_push_fee_lamports` not needed. A missing `TvOutbox` (`TV_OUTBOX_MISSING`) or peaq refusing node deliveries (`TV_INBOUND_REFUSED`) is refused before signing. LayerZero is the fallback a deployment may select on the route; only then is the fee cap required.

[`scripts/onboard_solana.py`](scripts/onboard_solana.py) is the end-to-end example: it ran the production onboardings of 2026-10-06 in PEAQ and in USDC from one command each (TICKET-ADD-SVM-023). See the [Solana onboarding guide](docs/21_SOLANA_ONBOARDING.md) for configuration, signer selection, examples, status and recovery, and migration guidance. See [Solana reads](docs/20_SOLANA_READS.md) for home resolution and native account observations.

### peaq RPC: retries, rate limits and call counts

`PeaqosClient` talks to peaq through `PeaqHTTPProvider` (TICKET-TKNA-007):

- **Chain id.** It is asked once per client.
- **Retries, reads only.** Rate limits (HTTP 429, `-32029`) back off from 0.5 s, at most five attempts, honouring `Retry-After`. A transient failure is retried once after 1 s. Submissions are sent exactly once.
- **Time budget.** Solana onboarding operations bound every retry pause by their `timeout_seconds`.
- **One block per read.** A read operation pins one finalized peaq block.
- **Log windows.** History reads split `eth_getLogs` into windows of at most 5,000 blocks, so an endpoint that caps the range (QuickNode: 10,000) keeps working as machines age.
- **Errors.** A failed read names the request: `TokenomicsActivationError.rpc_method` and `.rpc_host` (host only, never the path or query where an API key lives). An exhausted rate limit is `RPC_RATE_LIMITED`.

To see how many calls an operation makes, enable `DEBUG` on the `peaq_os_sdk.rpc` logger (one `rpc calls for <operation>: total=N …` line per operation), or run the counting proxy:

```bash
python scripts/count_rpc_calls.py --upstream-env PEAQOS_RPC_URL --port 18545 --log rpc-calls.log
```

Point the client at `http://127.0.0.1:18545`, run the operation, then press Ctrl-C for per-method totals. The proxy never prints the upstream URL.

### Solana machine management

Writes for Solana-homed machines go through the **same entry points** as their peaq equivalents — `transfer_machine`, `set_machine_controller`, the three DID setters, and `suspend_machine`/`resume_machine` take an optional `solana=SolanaMachineWriteParams(...)`. A caller who passes none sees exactly the shipped EVM behaviour; no machine-keyed method gained a chain argument. `set_machine_evm_operator` is the one addition with no peaq sibling, because on peaq the owner already *is* the EVM address.

For read-only management, `PeaqosClient.preview_machine_action(SolanaMachineActionParams(...))` returns `SolanaMachineActionPreview`, while `reconcile_machine_management_transaction(native_attempt, native_context)` takes `NativeManagementTransaction` and `SolanaManagementReconciliationContext` and returns `SolanaManagementReconciliation`. Both client methods preserve the corresponding EVM overloads and precise return types; reconciliation requires the attempt and context to belong to the same variant. Native previews accept a public signer identity and require no signing account.

The signer is an argument rather than client configuration: Solana's key space and the EVM account's are unrelated, and the action is authorized as that signer's public key. Three ERC-721 operations — `approve_machine`, `set_machine_approval_for_all`, `safe_transfer_machine` — are refused by name with `SOLANA_OPERATION_UNSUPPORTED` before any RPC or signature, rather than rejected as unknown.

A transaction that landed and then disagreed comes back as a **result carrying its conflicts**, not as an exception, because a caller told only "mismatch" cannot tell that their own action succeeded. Confirmation requires the transaction, the action's **own** event from the expected program for this machine, *and* the complete expected post-state including the fields the action was not supposed to change. `pending` means "not yet known", never "failed".

Confirming locally never implies peaq has applied anything: `set_machine_evm_operator` returns the two in two fields, and synchronization is established only by peaq's own `lastAppliedMirrorPushSeq` watermark keyed by Solana's **protocol chain ID (5)** — not its LayerZero endpoint ID and not peaq's EVM chain ID. Matching operators, `NothingToPush`, a skipped send and a source sequence are each true of a push that was dropped in transit, so none of them counts. The SDK sends no cross-chain message and changes no bridge configuration.

See [docs/21_SOLANA_MACHINE_MANAGEMENT.md](docs/21_SOLANA_MACHINE_MANAGEMENT.md) for the authority table, the explicit clearing rules, DID rent direction, and how `expired` is the only reconciliation state that licenses a fresh attempt.

### Two subscription accounts, selected by deployment record

Two generations of the Solana programs are live. Before CORE-778 they read a machine's subscription from the adapter-owned `SubscriptionMirror`; since CORE-778 they read the terminal-owned `SubscriptionTerminal`, which carries a **status** as well as a tier. The deployment record says which, through `subscription_reader` (`"mirror"` or `"terminal"`) and `machine_subscription_terminal`. Only that field decides — not the packaged IDL's account name, not `metadata.version` (`0.1.0` in both), and not whether either account exists. A `"terminal"` record with no terminal program ID fails at resolution with `DEPLOYMENT_UNAVAILABLE`, before any RPC, rather than falling back to an account the deployed programs no longer read.

The eligibility rules differ with it, and neither reads the tier: `onboard_machine` needs `last_seq != 0` **and** a status of Active, Grace or Runoff (`can_renew`, since CORE-801); `submit_event` needs Active or Grace alone. **Tier `0` is Entry, a real purchasable tier** — never read it as "no subscription". An absent terminal account is ineligible for both, not tier zero.

Status reads report **both** accounts and name the authoritative one in `subscription_source`; nothing merges them into a single tier. `subscription_terminal is None` means the account was not read, never that it is absent. The two `last_seq` values count different streams — the adapter's tier push and peaq's status signal — and are never comparable.

The shipped `peaq-mainnet` record reads `"terminal"` and names the deployed terminal program `4EFBHr9AyazPA6cxZRYYU5eK6uguogG5YhPQX6cs6A9d`: the official Solana set has run the CORE-778 readers since the production upgrade of 2026-09-16. The mirror generation stays implemented for a record whose programs still run pre-CORE-778 builds; none is shipped. See [docs/20_SOLANA_READS.md](docs/20_SOLANA_READS.md#which-account-holds-the-subscription) for the full field list, the readiness table, what the shipped record uses, and the migration notes for callers that construct deployment records.

---

## Solana event submission

`submit_event` and `batch_submit_events` resolve each machine and write to the chain it is homed on. Callers never name a chain, and an EVM-only caller sees no change. The native path needs `pip install peaq-os-sdk[solana]`.

```python
from peaq_os_sdk import SubmitEventParams

# A Solana-homed machine: the same call, routed natively.
signature, data_hash = client.submit_event(
    machine_id=machine_id,          # full uint256, never narrowed
    event_type=0,                   # 0 revenue, 1 activity
    value=1_23,                     # subunit integer: US$1.23
    currency="USD",                 # omit for the same default
    timestamp=observed_at,          # checked against the cluster clock
    raw_data=payload,               # keccak256(payload); None means 32 zero bytes
    trust_level=0,
    source_chain_id=3338,           # provenance, not routing
    source_tx_hash=None,
    metadata=b"",                   # at most 256 bytes, refused not truncated
)
```

A batch is one transaction, which is what makes it atomic:

```python
signatures = client.batch_submit_events([
    {**event_for(machine_a), "currency": "USD"},
    {**event_for(machine_a), "currency": "USD"},   # same log, consecutive indices
    {**event_for(machine_b), "currency": ""},      # activity event
])
```

Mixing peaq-homed and Solana-homed machines in one batch is refused before either chain is written. An oversized batch is refused rather than split, because a split batch would no longer be atomic.

The first event for a machine creates its log and the submitter pays the rent shortfall, once per machine however many events target it. Each intentional submission carries a random 128-bit ID in one SPL Memo instruction, which is what keeps two identical calls against one blockhash from collapsing into a single transaction. Retries reuse that ID and rebroadcast the same signed bytes; a timeout means unresolved, not failed.

Local confirmation proves the event was recorded on Solana. It does not prove that any downstream rating service has ingested it, that a score changed, or that peaq has synchronized. On chain the log keeps only its count and the most recent event, so there is no event enumeration and this SDK adds no history API.

See [docs/03_EVENTS.md](docs/03_EVENTS.md) for the full gate list, the field differences from EVM, and what completion does and does not establish.

---

## Install

```bash
pip install peaq-os-sdk
```

Dependencies installed automatically: `web3`, `eth-account`, `requests`, `posthog`.

For OWS wallet management (optional):

```bash
pip install peaq-os-sdk[ows]
```

For Solana transaction signing via OWS (optional):

```bash
pip install peaq-os-sdk[ows,solana]
```

For Stream buyer SPL token payments (optional):

```bash
pip install peaq-os-sdk[solana]
```

The `solana` extra installs `solders` (native SOL transfers and signing) and `solana` (provides `spl.token` for SPL transfers such as USDC).

For P2P stream delivery (optional):

```bash
pip install peaq-os-sdk[p2p]
```

The `p2p` extra enables machine-to-machine P2P delivery via the `peaqos-p2p` transport (`P2PDeliveryChannel` seller, `P2PDeliveryReceiver` buyer).

### Solana signing from an OWS wallet

When `open-wallet-standard` and `solders` are installed, construct a standalone
Solana signer from a vault wallet that has a Solana account:

```python
from solders.hash import Hash
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction

from peaq_os_sdk import PeaqosClient, SOLANA_MAINNET_CHAIN_ID

signer = PeaqosClient.solana_signer_from_wallet("my-wallet", "s3cret")
print(signer.address, signer.chain_id)  # chain_id == SOLANA_MAINNET_CHAIN_ID

from_pubkey = Pubkey.from_string(signer.address)
instruction = transfer(
    TransferParams(
        from_pubkey=from_pubkey,
        to_pubkey=Keypair().pubkey(),
        lamports=1_000,
    ),
)
message = Message.new_with_blockhash([instruction], from_pubkey, Hash.default())
tx = Transaction.new_unsigned(message)

signed = signer.sign_transaction(tx)
# broadcast with your Solana RPC client using signed.serialize()
```

By default (`ows_signing=True`), the ed25519 key is decrypted only per sign via OWS.
Pass `ows_signing=False` to decrypt at construction and sign locally with
`solders`. See [`docs/06_WALLET.md`](docs/06_WALLET.md) for details.

### Requirements

- Python `>= 3.10`
- A peaq RPC endpoint
- A funded wallet for the proxy operator (or for the machine itself, in self-managed mode)

---

## Quick start

```python
from peaq_os_sdk import PeaqosClient

client = PeaqosClient(
    rpc_url="https://peaq.api.onfinality.io/public",
    private_key="0xYOUR_PRIVATE_KEY",
    identity_registry="0x...",
    identity_staking="0x...",
    event_registry="0x...",
    machine_nft="0x...",
    did_registry="0x...",
    batch_precompile="0x...",
)

print("signer address:", client.address)
```

`PeaqosClient` is the only class consumers instantiate. All feature methods hang off it. The constructor performs synchronous validation and wires up the underlying `Web3` provider + signing account — no network I/O is issued at construction time.

[**Read More here >>**](docs/01_QUICK_START.md)

---

## MCR queries

Use `PeaqosQueryClient(deployment_id="peaq-mainnet")` for HTTP-only MCR reads without RPC, contracts, keys or wallet configuration. The SDK selects the paired endpoint. Legacy callers explicitly select `PeaqosQueryClient(legacy_api_url=...)`. All three methods reuse the existing typed validation, response correlation and pooled HTTP transport; full `PeaqosClient` query calls remain supported. See the [query client guide](docs/03_QUERIES.md#http-only-client) for examples and mode-specific DID formats.

| Method | HTTP endpoint | Returns |
|---|---|---|
| `client.query_mcr(did)` | `GET /mcr/{did}` | `MCRResponse` TypedDict |
| `client.query_machine(did)` | `GET /machine/{did}` | `MachineProfileResponse` (NFT metadata JSON v1.0 as `dict[str, Any]`) |
| `client.query_operator_machines(did)` | `GET /operator/{did}/machines` | `OperatorMachinesResponse` |

`MCRResponse`:

```python
{
    "did": str,
    "machine_id": int,
    "mcr_score": int | None,    # 0–100, None if Provisioned
    "mcr": str,                 # "AAA" | "AA" | "A" | "BBB" | "BB" | "B" | "NR" | "Provisioned"
    "bond_status": str,         # "bonded" | "unbonded"
    "negative_flag": bool,      # active negative event flag
    "event_count": int,
    "revenue_event_count": int,
    "activity_event_count": int,
    "revenue_trend": str,       # "up" | "stable" | "down" | "insufficient"
    "total_revenue": float,
    "average_revenue_per_event": float,
    "last_updated": int | None,
}
```

`OperatorMachinesResponse` carries `operator_did`, a `machines` list of `{did, machine_id, mcr_score, mcr, negative_flag}` entries, and a `pagination` object. `MachineProfileResponse` returns structured NFT metadata with a validated `peaqos` sub-object. Every failure path is an `ApiError` with a stable `.code` (`NOT_FOUND`, `SERVICE_UNAVAILABLE`, `SERVER_ERROR`, `HTTP_ERROR`, `BAD_RESPONSE`, `TIMEOUT`, `NETWORK_ERROR`). See [docs/03_QUERIES.md](docs/03_QUERIES.md) for the full endpoint and error reference.

---

## Smart account deployment

ERC-4337 smart accounts are provisioned via the `MachineAccountFactory` contract using CREATE2 — the deployed address is deterministic from `(owner, machine, daily_limit, salt)`, so the same tuple always resolves to the same address.

| Method | On-chain | Returns |
|---|---|---|
| `client.get_smart_account_address(owner, machine, daily_limit, salt)` | view call — no gas | predicted address |
| `client.deploy_smart_account(owner, machine, daily_limit, salt)` | `createAccount` tx | deployed address |

Because CREATE2 is deterministic, `predicted == deployed` for the same inputs — callers can preview the address, pre-fund it, or display it in a UI before paying gas. Configure the factory address via the optional `machine_account_factory` kwarg on `PeaqosClient` or the `MACHINE_ACCOUNT_FACTORY_ADDRESS` env var. See [docs/04_SMART_ACCOUNTS.md](docs/04_SMART_ACCOUNTS.md) for parameter rules and receipt-decoding error codes.

---

## Cross-chain NFT bridging

Machine NFTs move between peaq and Base over LayerZero v2. Two chains are recognised today; direction is inferred from the `source` / `destination` arguments.

| Chain | `SUPPORTED_CHAINS` id | `LAYERZERO_EIDS` |
|---|---|---|
| `"peaq"` | 3338 | **30302** |
| `"base"` | 8453 | **30184** |

| Direction | Source contract | Effect | `dstEid` |
|---|---|---|---|
| peaq → base | `MachineNFTAdapter` on peaq | NFT locked on peaq, minted on Base | `30184` |
| base → peaq | `MachineNFTBase` on Base | NFT burned on Base, unlocked on peaq | `30302` |

`client.bridge_nft(...)` estimates the LayerZero messaging fee via `quoteSend` **before** broadcasting and attaches that fee as `msg.value` on the actual `send` transaction so the message is correctly paid for. `PeaqosClient.wait_for_bridge_arrival(...)` is a static method (no client instance needed) that polls `MachineNFT.ownerOf(token_id)` on the destination every 10 seconds and returns `True` on arrival or `False` at timeout (default 300 s). See [docs/05_BRIDGE.md](docs/05_BRIDGE.md) for the full walkthrough including `options` handling, Base-source setup, and the complete error-code table.

> **Deprecated in Tokenomics mode.** `bridge_nft` raises `TokenomicsUnsupportedError` (`MACHINE_RELOCATION_UNAVAILABLE`) when the client selected a Tokenomics deployment — before the adapter approval it would otherwise submit. Tokenomics 2.0 relocates the whole machine record rather than sending an ONFT, and relocation is not exposed in this release. `wait_for_bridge_arrival` takes no client and so cannot detect the mode; it is deprecated but not gated. See [docs/18_TOKENOMICS_ACTIVATION.md](docs/18_TOKENOMICS_ACTIVATION.md#relocation).

---

## Provider-node provisioning

The `peaq_os_sdk.provisioning` namespace is a **generic, provider-agnostic runner** that turns a schema-driven `ProviderProvisioningManifest` (`node-provider.peaq.network/v1alpha1`) into an ordered, auditable install. All provider-specific complexity lives in the manifest — the runner understands only the schema. It runs entirely on the machine and calls no external API.

```python
from peaq_os_sdk import provisioning

manifest = provisioning.fetch_manifest("akash", "latest", repo_base_url="https://manifests.example")
inputs = provisioning.resolve_inputs(manifest, {"providerDomain": "node.example.com", ...})
executor = provisioning.LocalShellExecutor()

preflight = provisioning.run_preflight(manifest, executor)
preflight.raise_if_blocked()                       # gate: no phase starts while blocked

result = provisioning.provision(
    manifest, inputs, mode="manual", executor=executor,
    context={"machine_wallet_address": "0x..."},   # never the operator's
    on_owner_action=lambda e: input(e.instructions) or "confirmed",
)

verification = provisioning.verify_provisioning(
    manifest, executor, inputs=inputs.values, captures=result.captures
)
verification.raise_if_failed()                     # live only when all probes pass
```

- **Manual vs auto modes** are declared per step (`manual` / `auto` / `both`). Eligibility is asymmetric: a manual-only step still runs (confirmed) in an auto run, while an **auto-only step is skipped in a manual run** and left un-recorded for a later auto pass — so `completed` means *this selected-mode pass* finished. Auto mode requires a `SudoGrant` scoped to the manifest's `allowedCommands`; a `sudo: required` command outside the grant **fails closed**, and unmarked commands are never elevated.
- **Owner-action handoffs** (funding, DNS, SSH, signing) are real pause points — `on_owner_action` must return the step's `expectedConfirmation`; they are never auto-confirmed.
- **Secrets never leak — and are never persisted.** Secret inputs and secret captures are redacted before any output is emitted; a secret capture with no `pattern` withholds its step's raw stream entirely. Errors and resume state carry no secret material, and secret captures are never written to a resume state (not even encrypted).
- **Idempotency & durable resume.** A step's idempotency `check` short-circuits already-provisioned work. Every run emits a typed, JSON-serializable `ResumeState` (with a `schema_version`, completed `phase/step` ids, and resolved non-secret inputs) at each step boundary via `on_state_change` — and as `ProvisionResult.resume_state` / `StepExecutionError.resume_state` — which you pass back as `resume_state=` to skip completed steps and restore non-secret captures. A resumed step needing a non-persisted secret re-runs its producer when `rerun: safe`, else the caller can re-provide it via `resume_secrets` (manifest-declared secret keys only), else `ResumeNotPossibleError`.
- **Deterministic manifest pinning.** `fetch_manifest(..., include_source=True)` returns a `FetchedManifest` exposing the raw source bytes, so a caller can pin a stable `sha256:<hex>` digest.
- **Verification, not exit codes, defines success** — `verify_provisioning` succeeds only when every `success.all_of` probe passes (the caller passes `inputs=inputs.values` and a redaction registry seeded from `inputs.secret_values()`).

The runner is standalone; a CLI (or any other consumer) calls these functions. CLI-surface decisions (headless vs prompts) and provider manifest content (commission/API-key fields) are the consumer's and the manifest's concern, not the runner's. Remote execution is out of scope — only the default local-shell executor and a swappable local agent/session executor are supported.

---

## Telemetry

The SDK collects anonymous, aggregate usage telemetry via PostHog to measure install counts, onboarding success rates, and MCR query volume. **No PII is collected** — GeoIP is disabled, IP addresses are nulled, and all UUIDs are randomly generated.

Telemetry is enabled by default and can be disabled via environment variables:

| Env var | Effect |
|---|---|
| `DO_NOT_TRACK=1` | Disables telemetry unconditionally (industry standard) |
| `PEAQOS_TELEMETRY=0` | Disables telemetry (project-specific toggle) |

`DO_NOT_TRACK=1` always takes precedence. When telemetry is disabled, no PostHog client is created and no events are sent.

Analytics events are captured automatically from existing SDK methods — no additional API calls are needed:

```python
client = PeaqosClient(...)                         # peaqos_sdk_initialize (first install only)
addr, key = PeaqosClient.generate_keypair()        # peaqos_sdk_generate_keypair (via active instance)
machine_id = client.register_machine()             # peaqos_sdk_generate_wallet
tx = client.write_machine_did_attributes(...)      # peaqos_sdk_generate_did
tx = client.mint_nft(machine_id, addr)             # peaqos_sdk_generate_nft
mcr = client.query_mcr("did:peaq:0x...")           # peaqos_sdk_mcr_request
client.close()                                     # flush queued events
```

All analytics calls are fire-and-forget — they never block or raise. See [docs/10_ANALYTICS.md](docs/10_ANALYTICS.md) for the full event reference and privacy details.

---

## Documentation

Per-feature deep-dives live under [`docs/`](docs/):

- [**Quick Start**](docs/01_QUICK_START.md) — client initialization, configuration, environment variables, types, exception classes, validation, utilities, and constants.
- [**Machine identity & registration**](docs/02_REGISTRATION.md) — faucet 2FA enrollment, gas-station funding, self-managed and proxy-managed registration. Includes the full faucet error reference and the on-chain revert mapping.
- [**MCR queries**](docs/03_QUERIES.md) — `query_mcr`, `query_machine`, `query_operator_machines`, response shapes, rating tiers, and the full HTTP error-code table.
- [**Smart account deployment**](docs/04_SMART_ACCOUNTS.md) — `deploy_smart_account` and `get_smart_account_address`, CREATE2 determinism, parameter rules, and receipt-decoding error codes.
- [**Cross-chain NFT bridging**](docs/05_BRIDGE.md) — `bridge_nft` direction handling (peaq ↔ Base via LayerZero v2), supported chain IDs and LayerZero EIDs, LayerZero fee estimation, and `wait_for_bridge_arrival` polling semantics.
- [**Provider-node provisioning**](docs/15_PROVISIONING.md) — the generic, manifest-driven `peaq_os_sdk.provisioning` runner: fetch/validate a `ProviderProvisioningManifest`, resolve inputs, gate on pre-flight, execute phases/steps in manual/auto modes with scoped sudo and owner-action handoffs, redact secrets before emission, thread captures, verify success by probes, and resume interrupted runs. Covers the `CommandExecutor` protocol, machine-wallet context, idempotency, the error hierarchy, and the consumer/runner boundary.
- [**Monetisation opt-in**](docs/16_MONETIZATION.md) — the Tokenomics 2.0 signed on/off toggle and public state read: deployment-selected MCR configuration, full `uint256` IDs and decimal DIDs, owner/controller authorization, live compatibility checks, bounded retries, one end-to-end deadline, strict response correlation, typed failures, and migration from independent legacy state.
- [**Tokenomics 2.0 machine activation**](docs/18_TOKENOMICS_ACTIVATION.md) — atomic PEAQ activation: deployment selection, `preview_machine_activation` / `activate_machine`, the bond/voucher arithmetic and why only the net amount is approved, receipt event correlation and post-state reconciliation, `reconcile_activation_transaction` for a submitted-but-unconfirmed hash, the typed error taxonomy, migrating off register-then-mint, and the release-day diff for adding a network.
- [**Tokenomics 2.0 machine management**](docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md) — post-onboarding reads and previews, lifecycle, bounded PEAQ/USDT subscriptions, ERC-721 ownership, DID updates, relocation evidence, authority rules, and known-hash reconciliation.
- [**Solana machine management**](docs/21_SOLANA_MACHINE_MANAGEMENT.md) — native writes through the existing entry points: the authority table, why the signer is an argument, results that report conflicts instead of raising, keeping local confirmation apart from peaq application, explicit clearing, DID rent direction, and reconciling after a crash.
- [**Solana onboarding and recovery**](docs/21_SOLANA_ONBOARDING.md) — the terminal-first phases (registration, request, credit, finalise, bond, mint, link), keyless previews/status, signer selection, the journal and recovery.
- [**Solana reads**](docs/20_SOLANA_READS.md) — home resolution, native identity and subscription observations, which of the two subscription accounts a deployment reads and the two eligibility rules, cluster verification and packaged bindings.
- [**MCR API compatibility**](docs/22_MCR_API_COMPATIBILITY.md) — Tokenomics 2.0 and Solana-homed machines: the three breaking type changes, the additive `home_chain` / `rating_unavailable` / `owner` / `controller` fields, the distinct `400` / `401` / `409` / `503` codes on `ApiError`, and the planned owner-signed Solana monetization path (implemented, gated off until a compatible server release is recorded).
- [**Event submission**](docs/03_EVENTS.md) — single and batch event submission, pipeline details, limits, hashing, metadata mode, and rate-limiting behavior.
- [**NFT minting & DID attributes**](docs/04_NFT_AND_DID.md) — minting machine NFTs, querying token IDs, writing machine and proxy DID attributes atomically via the Batch precompile. Covers attribute key reference, data visibility options, and the atomic batch guarantee.
- [**OWS wallet lifecycle**](docs/06_WALLET.md) — create, import, list, get, export, and delete encrypted wallets via the Open Wallet Standard. Multi-chain accounts, passphrase management, vault storage, OWS-native EVM signing via `from_wallet()`, and Solana signing via `solana_signer_from_wallet()`.
- [**Orchestration service**](docs/07_ORCHESTRATION.md) *(experimental)* — `client.orchestration`, policies, observability, market lifecycle, pagination, challenge-sign workflows, planned type stubs, and live integration tests.
- [**Stream data signing & encryption**](docs/08_STREAM.md) *(experimental)* — local field-level privacy rules, EIP-191 data package signing, and verification (Phase 2 Step 2; no network/distribution).
- [**Stream data chunking**](docs/09_CHUNKING.md) *(experimental)* — per-chunk XChaCha20-Poly1305 encryption, inline Ed25519 signatures, owner/operator/machine key wrapping, and separate buyer access docs; `build_chunk_chain` / `verify_chunk_chain` / `decrypt_chunk` / `create_buyer_access_entry` / `build_buyer_access_files`.
- [**Analytics & telemetry**](docs/10_ANALYTICS.md) — anonymous usage telemetry, event inventory, opt-out, privacy guarantees, and onboarding flow integration.
- [**Stream distribution**](docs/11_DISTRIBUTION.md) *(experimental)* — seller-side payment confirmation and delivery (`distribute_data`, `PollingConfirmationProvider`, `S3DeliveryChannel`) plus buyer-side token payment (`transfer_token`, `submit_payment_proof`, `pay_and_submit_proof`) on peaq, Base, and Solana.
- [**P2P delivery**](docs/14_P2P_DELIVERY.md) *(experimental)* — machine-to-machine delivery via the `peaqos-p2p` transport: `P2PDeliveryChannel` (seller) and `P2PDeliveryReceiver` (buyer) with verify-before-decrypt chunk reception, session routing, and replay protection.
- [**Stream delivery setup**](docs/12_STREAM_DELIVERY_SETUP.md) *(in development)* — pre-purchase payment rails discovery, delivery transports discovery, delivery capability registration, and stream listing purchase field updates.
- [**Stream purchases**](docs/13_STREAM_PURCHASES.md) *(in development)* — full purchase lifecycle: creation with buyer identity, payment intent, payment proof, delivery retrieval, and purchase events.
- [**Verify reads and chip preflight**](docs/17_VERIFY.md) *(experimental)* — `peaq_os_sdk.verify` provides `READ_PREFLIGHT_ONLY + API_ONLY`. `VerifyReadClient.get_machine_verification_across_chains(machine_id)` is the sole read method for EVM and Solana homes over one HTTPS route, with distinct home/controller/operator roles and independent KYB/chip states. Solana genesis hashes and role keys must be nonzero. The API is authoritative. SDK-owned and caller-owned sessions have explicit cleanup. EVM-only CA306 preflight produces non-authoritative evidence using an exact decimal machine ID/DID pair. The [eight-stage application flow](docs/17_VERIFY.md#application-flow) separates platform chip access, wallet signing, application-owned evidence handoff, backend processing, and the later read. The undeployed EVM-only reader is absent; there is no stable root-client integration, RPC, SDK hardware access, evidence upload, or persistence.

---

## Development

```bash
git clone https://github.com/peaqnetwork/peaq-os-sdk-py.git
cd peaq-os-sdk-py
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

### Activate `.env` for Testing
```
set -a
source .env
set +a
```

### Quality gates

```bash
ruff check src tests       # lint — zero warnings
black --check src tests    # formatting
mypy src                   # strict type check, zero errors
mypy src tests/unit/verify # also type-checks the Verify type fixtures
pytest -q                  # all green
```

The [Verify guide](docs/17_VERIFY.md#read-machine-verification) covers API-only reads, independent lifecycle examples, controlled errors, cancellation, and beta compatibility. Its conformance sections describe the pinned backend and SDK-local [read corpora](docs/17_VERIFY.md#read-conformance-and-compatibility), the real [CA306 capture](docs/17_VERIFY.md#physical-chip-conformance), and pending acceptance checks. Run the unit and offline corpus checks from a repository checkout:

```bash
pytest tests/unit/verify tests/conformance/verify -q
mypy --strict tests/unit/verify tests/conformance/verify
```

The Verify read/chip fixtures and public test-account recipe are excluded from release packages. The backend read bytes and revision are pinned. The corpus documentation records the SDK error-message expectation and the difference between injected headers and Fetch normalization.

The unit tests are hermetic — they mock `Web3` and `requests.Session` with handwritten stubs and never touch the network.

The Tokenomics activation surface additionally carries a branch-coverage floor:

```bash
pytest -q -m "not integration" \
  --cov=src/peaq_os_sdk/tokenomics --cov-branch --cov-fail-under=90
```

Coverage is measured from the hermetic suite alone, so the floor is meaningful without spending PEAQ.

### Optional integration tests

The repo ships an opt-in suite that talks to a real peaq devnet. It is skipped silently in normal `pytest -q` and only runs when the required environment variables are supplied.

**Registration integration suite** ([`tests/integration/registration/test_registration_integration.py`](tests/integration/registration/test_registration_integration.py)) — three end-to-end tests for the self-managed flow, proxy-managed flow, and double-registration revert. Gated behind the `integration` marker plus seven environment variables:

```bash
PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public
PEAQOS_PRIVATE_KEY=0xYOUR_FUNDED_TREASURY_KEY
IDENTITY_REGISTRY_ADDRESS=0xYOUR_DEPLOYED_REGISTRY_ADDRESS
IDENTITY_STAKING_ADDRESS=0xYOUR_DEPLOYED_STAKING_ADDRESS
PEAQOS_OWNER_ADDRESS=5GrwvaEF...
PEAQOS_FAUCET_URL=https://depinstation.peaq.xyz
PEAQOS_2FA_CODE=123456
```

```bash
pytest -m integration tests/integration/
```

See [docs/02_REGISTRATION.md → Integration tests](docs/02_REGISTRATION.md#integration-tests) for the full breakdown of what each test verifies and operational warnings.

**Tokenomics 2.0 query & event suites** ([`tests/integration/tokenomics/`](tests/integration/tokenomics/)) — MCR-20 coverage for the three enabled queries (lossless `int` machine IDs, identifier correlation, per-entry DID/ID agreement) and for Tokenomics event submission. Requires the eight base contract env vars plus:

```bash
PEAQOS_TOKENOMICS_TEST_MACHINE_ID=9007199254740992   # canonical decimal, registered on MachineRegistry
PEAQOS_TOKENOMICS_TEST_OPERATOR_DID=did:peaq:0x...   # queries only
PEAQOS_TOKENOMICS_EVENT_WRITES=1                     # events only — submits real transactions, spends gas
```

```bash
pytest -m integration tests/integration/tokenomics/
```

The query suite pins routing by building its client with a deliberately invalid `api_url`, so any fallback to the legacy endpoint fails loudly. agung is explicitly skipped with a reason — it has no approved paired MCR to test against.

**NFT & DID integration suite** ([`tests/integration/nft_did/test_nft_and_did_integration.py`](tests/integration/nft_did/test_nft_and_did_integration.py)) — end-to-end tests for the full NFT lifecycle, atomic DID writes, and atomicity guarantee verification (all-or-nothing batch semantics). Includes 7 tests across minting, token queries, machine DID attributes, proxy DID attributes, and atomic revert scenarios. Requires the same env vars as the registration suite plus:

```bash
EVENT_REGISTRY_ADDRESS=0x...
MACHINE_NFT_ADDRESS=0x...
DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800
BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805
```

See [docs/04_NFT_AND_DID.md](docs/04_NFT_AND_DID.md) for the full breakdown.

**OWS wallet integration suite** ([`tests/integration/wallet/test_wallet_integration.py`](tests/integration/wallet/test_wallet_integration.py)) — six end-to-end tests for wallet lifecycle (create, list, get, delete), create-export-reimport round-trip, private key import, empty vault, and error paths. Gated behind `PEAQOS_INTEGRATION=1` and requires the `open-wallet-standard` package:

```bash
pip install peaq-os-sdk[ows]
PEAQOS_INTEGRATION=1 pytest -m integration tests/integration/wallet/
```

See [docs/06_WALLET.md](docs/06_WALLET.md) for the full wallet API reference.

**Orchestration live API suite** ([`tests/integration/orchestration/test_orchestration_integration.py`](tests/integration/orchestration/test_orchestration_integration.py)) — health, readiness, machine lifecycle, market lifecycle, policy CRUD, and audit-event listing against a real orchestration deployment. Gated behind `PEAQOS_ORCHESTRATION_INTEGRATION=1`:

```bash
PEAQOS_ORCHESTRATION_INTEGRATION=1
PEAQOS_ORCHESTRATION_URL=https://markets.peaq.xyz
PEAQOS_API_KEY=your-platform-api-key
pytest -m integration tests/integration/orchestration/test_orchestration_integration.py
```

Optional overrides: `PEAQOS_ORCHESTRATION_PROVIDER_KEY`, `PEAQOS_ORCHESTRATION_ENDPOINT_URL`, `PEAQOS_ORCHESTRATION_MARKET_SERVICE_TYPE`, `PEAQOS_ORCHESTRATION_MARKET_OPERATION`, `PEAQOS_ORCHESTRATION_MARKET_ORDER_INPUT` (JSON object), `PEAQOS_ORCHESTRATION_MARKET_PAYMENT_RAIL`. See [docs/07_ORCHESTRATION.md → Integration tests](docs/07_ORCHESTRATION.md#integration-tests).

A separate **mocked** market lifecycle test lives in [`tests/integration/orchestration/test_market_lifecycle_integration.py`](tests/integration/orchestration/test_market_lifecycle_integration.py) and runs without orchestration env vars.

**Tokenomics integration suite** ([`tests/integration/tokenomics/test_activation_integration.py`](tests/integration/tokenomics/test_activation_integration.py)) — live Agung coverage for snapshot and `InfoDesk.peer` drift, pricing, deterministic IDs, genuine revert decoding, write-free previews, activation guards, atomic activations, management reads, reversible suspend/resume, submission reporting, and known-hash reconciliation.

> **This suite spends real PEAQ and cannot be undone.** Activation mints a permanent machine identity and bonds tokens against it — there is no deactivate and no refund. A full run costs roughly **0.8 PEAQ of bond plus activation and management gas** at the tier-0 price stamped on Agung. It is gated behind a dedicated variable on top of the `integration` marker.

```bash
PEAQOS_TOKENOMICS_INTEGRATION=1 \
PEAQOS_RPC_URL=https://peaq-agung.api.onfinality.io/public \
PEAQOS_PRIVATE_KEY=0xYOUR_FUNDED_KEY \
pytest -m integration tests/integration/tokenomics/
```

Contract addresses are deliberately **not** environment variables — resolving them from the SDK's approved snapshot is the behaviour under test. See the [activation](docs/18_TOKENOMICS_ACTIVATION.md) and [management](docs/19_TOKENOMICS_MACHINE_MANAGEMENT.md) guides for the API reference and live-suite limitations.

`PEAQOS_PRIVATE_KEY` must match `^0x[0-9a-fA-F]{64}$`. **Never commit a real key. Never run the suite against mainnet.**

### Build

```bash
python -m build
```

Produces `dist/peaq_os_sdk-<version>-py3-none-any.whl` and `dist/peaq_os_sdk-<version>.tar.gz` for downstream testing.

## License

See [LICENSE](./LICENSE) for details.
