Metadata-Version: 2.4
Name: peaq-os-cli
Version: 0.0.15
Summary: Python CLI 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-cli-py
Project-URL: Issues, https://github.com/peaqnetwork/peaq-os-cli-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,cli,python
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: peaq_os_sdk[x402]<0.12,>=0.11.0
Requires-Dist: click>=8.1
Requires-Dist: python-dotenv>=1.0
Requires-Dist: cryptography>=41.0
Requires-Dist: Pillow>=10.0
Requires-Dist: eth-account<1.0,>=0.10
Requires-Dist: PyYAML>=6.0
Provides-Extra: ows
Requires-Dist: open-wallet-standard>=1.3.2; extra == "ows"
Provides-Extra: s3
Requires-Dist: boto3>=1.28; extra == "s3"
Provides-Extra: solana
Requires-Dist: peaq_os_sdk[solana]<0.12,>=0.11.0; extra == "solana"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"

# peaq-os-cli

Python CLI for peaqOS. Describes install and the console entry point.

## Install

```bash
python3 -m venv .peaq-os-cli
source .peaq-os-cli/bin/activate
pip install -e ".[dev]"
```

The declared SDK dependency is `peaq_os_sdk[x402]>=0.11.0,<0.12`; the CLI `solana` extra uses the same range. `0.11.0` is the first release that carries what this CLI's Solana onboarding needs: the terminal-first phases (registration, request, finalise, the mint against the bonded request, the link push), the link push over the Trust Validator node, and the onboarding history's owner deltas that `peaqos machine history` shows. `0.10.0` still runs the removed reservation flow, so it cannot be used; no release below `0.10.0` ships the `peaq_os_sdk.verify` package that `peaqos verify` requires. CI/CD creates the release version. Optional Solana dependencies can be installed by changing the CLI extra above to `.[dev,solana]`; that extra alone does not supply the onboarding APIs or a Solana RPC endpoint.

## Run Tests
```
pytest
```

The opt-in mainnet onboarding test is `tests/integration/test_solana_onboarding_e2e.py`; its module docstring provides the configuration example and wallet setup. Run it with `PEAQOS_CLI_SVM_ONBOARDING_RUN=1` and `PEAQOS_CLI_SVM_ONBOARDING_CONFIG=/absolute/path/run.json`. Recovery/rejection cases are in `tests/integration/test_solana_onboarding_recovery_e2e.py`, with a separate prepared-state manifest and opt-in flags documented there. These tests can spend mainnet funds when enabled. The normal suite does not execute them. Preserve the run directory and original journal between phases; external operators provide mirror and link delivery.

For installed-package checks, build a wheel and sdist with `python -m build`, install the wheel and chosen SDK in fresh virtual environments, then run each environment's Python with `-I tests/packaging/check_install.py --native-extra absent` (or `present` after installing the SDK's Solana extra). The check reports installed versions, packaged IDL hashes, synchronous API availability and console-help results without RPC or wallet operations. A passing local development-wheel check does not establish a compatible published release or successful live onboarding.

## Quality Gates
```
ruff check src tests
black --check src tests
python -m mypy src
pytest -q --cov=src/peaq_os_cli --cov-fail-under=90
flake8 src tests
```

The strict mypy gate covers `src/`, as specified by the CLI guardrails. Run tests with pytest; `python -m mypy src tests` additionally checks the entire test suite, which is not yet fully typed. Use the same virtual environment for dependency installation and quality gates so Click/SDK typing changes are checked consistently.

### Running integration tests

| Gate | Scope | Cost |
| --- | --- | --- |
| `PEAQOS_CLI_INTEGRATION=1` | CLI suites, local mock faucet | No chain, no funds |
| `PEAQOS_TOKENOMICS_INTEGRATION=1` | Live agung activation and machine management | **Activation spends real PEAQ irreversibly** |

The second matches the SDK's own gate name deliberately: enabling the CLI's integration tests must not also start spending.

Under that gate, the machine-management suite **submits nothing by default**. A write invoked without `--yes` against non-TTY stdin renders its full preview — real preflight, real chain reads, real SDK-supplied contract and method — and only then refuses for want of consent, so the read half of every write is covered for free. Three further opt-ins widen it:

| Variable | Unlocks | Cost |
| --- | --- | --- |
| `PEAQOS_TKN2_MACHINE_ID` | Anything machine-scoped | None |
| `PEAQOS_TKN2_LIFECYCLE_WRITES=1` | `suspend` → rerun → `resume` | Gas only, no bond |
| `PEAQOS_TKN2_DESTINATION_RPC_URL` + `PEAQOS_TKN2_DESTINATION_DEPLOYMENT_ID` | `relocation status` across deployments | None |

Never aim the destination at peaq mainnet. These suites are testnet-only, and a cross-deployment read is still a read against whatever it targets.

**Known limits, deliberately visible.** Seven integration paths cannot run yet for want of fixtures neither this repository nor the SDK provides: existing-machine subscription activation, USDT subscription on either rail, controller-funded subscription, ERC-721 transfer, DID array replacement, a renewal reconciled after its allowance is consumed to zero, and a tier change at renewal (no recorded deployment carries CORE-784 yet). Each is a **skipped test with its reason attached** rather than an absent one, so `pytest -m integration -rs` lists them.

Agung's PriceOracle also has no committed PEAQ price, so every subscription quote reverts `PriceNotAvailable()` today. The suite asserts the CLI presents that as a stable `error_code` rather than a traceback, and starts skipping with "a price is now committed" once that changes.

## Console usage

The `peaqos` script invokes the Click root group in `peaq_os_cli.main`.

```bash
peaqos --help
peaqos --version
```

Authoritative option text for any subcommand is always available via built-in help:

```bash
peaqos <command> -h
peaqos qualify event -h
peaqos qualify mcr -h
peaqos monetize provision run -h
peaqos stream grant -h
peaqos stream consume -h
peaqos stream distribute -h
peaqos stream pay -h
peaqos stream payproof -h
```

## Commands

### `peaqos verify` (experimental)

`peaqos verify status MACHINE_ID` reads independent KYB and chip states from the Verify API. `MACHINE_ID` must be an ASCII decimal integer in `1..2^256-1`, without signs, whitespace or leading zeros. The API is authoritative; this command does not independently verify chain state or submit evidence.

The dedicated Verify configuration uses the root `--verify-api-url` option before `PEAQOS_VERIFY_API_URL` (including `.env`). It requires an HTTPS origin validated by the SDK; an empty explicit option is invalid. There is no default origin or fallback to another service. Help requires no Verify configuration or client.

```bash
peaqos verify
peaqos verify --help
peaqos --verify-api-url https://verify.example verify status 123
peaqos --verify-api-url https://verify.example verify status 123 --json
```

Human output shows the decimal machine DID, the home chain, effective DID controller, economic operator, separate KYB/chip states, and `Source: Verify API`. A Solana home reads `Solana`. An EVM home is named from its chain ID — `peaq (EVM)`, `Agung testnet (EVM)`, `Base (EVM)` — so the reader can always tell which chain the claim covers. An EVM chain the CLI has no name for reads `EVM chain <id>` rather than being labelled as some other network. The Solana genesis hash, and the numeric chain ID of a named EVM chain, appear only under `--json`. Each state is `unverified`, `verified`, `expired` or `revoked`. An existing unverified machine is successful data; an unknown machine is an error. No aggregate verification status is inferred.

`--json` emits one object with `machineId` (a decimal string), `machineDid`, `homeChain`, tagged `didController` and `operator` objects, and `verification.kyb.status` / `verification.chip.status`. Root `--quiet` preserves data output; `--verbose` adds no record or endpoint diagnostics. The command needs no wallet, private key, RPC, Tokenomics deployment or MCR configuration.

For EVM, `homeChain` contains `kind: "evm"` and a decimal `chainId`; for Solana it contains `kind: "solana"` and a full base58 `genesisHash`. Both identity objects contain the matching `kind` and full `address`. These values come from the API; no chain selector or lookup is added. This replaces the earlier experimental flat JSON shape.

Example JSON for a synthetic, existing EVM machine with neither component verified:

```json
{
  "machineId": "123",
  "machineDid": "did:peaq:123",
  "homeChain": {
    "kind": "evm",
    "chainId": "3338"
  },
  "didController": {
    "kind": "evm",
    "address": "0x8617E340B3D01FA5F11F306F4090FD50E238070D"
  },
  "operator": {
    "kind": "evm",
    "address": "0x52908400098527886E0F7030069857D2E4169EE7"
  },
  "verification": {
    "kyb": {
      "status": "unverified"
    },
    "chip": {
      "status": "unverified"
    }
  }
}
```

The equivalent synthetic Solana projection keeps native identities separate:

```json
{
  "machineId": "123",
  "machineDid": "did:peaq:123",
  "homeChain": {
    "kind": "solana",
    "genesisHash": "CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8"
  },
  "didController": {
    "kind": "solana",
    "address": "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi"
  },
  "operator": {
    "kind": "solana",
    "address": "8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR"
  },
  "verification": {
    "kyb": {
      "status": "unverified"
    },
    "chip": {
      "status": "unverified"
    }
  }
}
```

The read is public and unauthenticated. Each invocation makes one SDK read; it does not follow redirects, retry automatically, cache results or poll. Rate-limit errors may include a bounded delay for a later manual retry. Invalid responses produce an error rather than a verification state.

Exit codes are `0` for success, `1` for invalid machine-ID syntax/range, `2` for API/transport/protocol failures, and `3` for missing/invalid Verify configuration or incompatible SDK support. This resolves from PyPI on a clean install; MCR-approved cross-chain fixture validation is still pending.

`peaqos verify status` is the only Verify command that reaches the network, and it does so only through the SDK. It reads state; it never submits anything.

#### `peaqos verify chip` (experimental)

`peaqos verify chip` builds the local evidence an onboarding workflow needs before a machine's chip can be verified. Three stages run in order, each reading bounded files and writing one new artifact.

All options are file paths: no certificate, nonce, signature, evidence or key is ever passed as an argument value, so nothing sensitive reaches argv, the environment or shell history.

**The challenge context.** The onboarding service issues `context.json`. It is strict UTF-8 JSON of at most 4096 bytes with exactly these six string fields and no others:

```json
{
  "chainId": "3338",
  "didController": "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
  "expiresAt": "1789999999",
  "machineDid": "did:peaq:123",
  "machineId": "123",
  "nonce": "0x9e21fa871fae1ee7bbb829f178a675aa076a7e6241ce4653b208453377da3b2e"
}
```

`chainId`, `machineId` and `expiresAt` are canonical unsigned decimals with no sign, prefix, separator or leading zero. `machineId` is nonzero and `machineDid` must equal `did:peaq:` followed by exactly that decimal. `nonce` is `0x` plus 64 lowercase hex characters. Protocol, profile, trust-bundle and fingerprint values are not accepted here — the SDK fixes or derives all of them.

**Stage 1 — `prepare`.** Validates the context, its freshness and the leaf certificate, then writes the digest the chip must sign.

```bash
peaqos verify chip prepare \
  --context context.json --certificate leaf.der --out prehash.bin
```

```text
Chip preflight artifact created.

  Stage:        prepare
  Artifact:     chip-prehash
  Byte Length:  32
  Authority:    local preflight only
```

**Stage 2 — `controller-request`.** Rebuilds stage 1, verifies the chip signature, then writes the message the DID controller signs.

```bash
peaqos verify chip controller-request \
  --context context.json --certificate leaf.der \
  --chip-signature chip-signature.bin --out controller-message.bin
```

```text
Chip preflight artifact created.

  Stage:        controller-request
  Artifact:     controller-message
  Byte Length:  279
  Authority:    local preflight only
```

**Stage 3 — `finalize`.** Rebuilds both earlier stages, verifies the controller signature, then writes the canonical evidence document and reports the fixed protocol identifiers.

```bash
peaqos verify chip finalize \
  --context context.json --certificate leaf.der \
  --chip-signature chip-signature.bin \
  --controller-signature controller-signature.bin --out evidence.json
```

```text
Chip preflight artifact created.

  Stage:        finalize
  Artifact:     evidence
  Byte Length:  2411
  Authority:    local preflight only

  Protocol:           peaq.verify.chip-proof/1
  Profile:            infineon-optiga-trust-m-express-ca306/1
  Evidence Schema:    peaq.verify.chip-evidence/1
  Trust Bundle:       infineon-optiga-trust-m-express-ca306-roots/1
  Revocation Status:  not_evaluated
```

Byte lengths vary with the certificate and identity supplied; the values above come from the shared conformance corpus.

**`--json` metadata.** Every stage accepts `--json`, which replaces the rows with one object on stdout and nothing else. `prepare` and `controller-request` omit the five result fields rather than reporting empty ones:

```json
{
  "artifact": "chip-prehash",
  "authority": "local_preflight_only",
  "byteLength": 32,
  "stage": "prepare"
}
```

```json
{
  "artifact": "evidence",
  "authority": "local_preflight_only",
  "byteLength": 2411,
  "evidenceSchema": "peaq.verify.chip-evidence/1",
  "profile": "infineon-optiga-trust-m-express-ca306/1",
  "protocol": "peaq.verify.chip-proof/1",
  "revocationStatus": "not_evaluated",
  "stage": "finalize",
  "trustBundle": "infineon-optiga-trust-m-express-ca306-roots/1"
}
```

The metadata never contains artifact bytes, the output path, a DID, an address, the nonce or the expiry. Failures print one fixed line to stderr naming the flag and the constraint, for example:

```text
Error: --out must be a path that does not exist yet and can be created as a new file.
Error: Chip controller-request failed because the supplied chip signature does not match this attempt.
Error: Chip prepare failed because the challenge is outside its freshness window. Request a new challenge and retry.
```


**These commands do not talk to the chip, to a wallet or to any service.** The CLI holds no key and signs nothing. Two signatures come from outside it, and the surrounding workflow owns both:

1. The onboarding service issues a challenge and gives you `context.json` (`chainId`, `didController`, `expiresAt`, `machineDid`, `machineId`, `nonce`) and the leaf certificate read from the chip's `0xE0E0` slot.
2. `prepare` writes a 32-byte prehash. Your platform adapter passes it to the chip's `0xE0F0` ECDSA-without-hashing operation. **Do not hash it again** — it is already the digest the chip signs.
3. `controller-request` verifies that chip signature and writes the controller's message. The machine's current DID controller signs it as an EIP-191 personal message **exactly once**, with no extra framing or prefix.
4. `finalize` revalidates both proofs and writes the canonical `peaq.verify.chip-evidence/1` document. Submit that file through the onboarding service.

**Success is not verification.** The artifacts are local preflight evidence only. The backend independently revalidates the evidence and consumes the challenge before any machine reaches a verified state — read that state with `peaqos verify status`. Output says `local preflight only` and never reports a machine as verified, accepted, submitted or anchored. `revocationStatus: not_evaluated` records that revocation was not checked locally; it is not a claim that the certificate is unrevoked.

The challenge is short-lived and its freshness is rechecked on every stage against the SDK's own clock. There is no `--now`, grace period or bypass, and no state is carried between commands: each stage rebuilds every earlier one from the same files, so there is no cache, journal or resume.

Finalize prints the protocol, profile, evidence schema, trust bundle and revocation status. It prints no evidence hash and no chip ref: the SDK exposes no public read path for either value. The chip ref is carried as a field inside the written evidence document, and the evidence hash is `keccak256` over the exact artifact bytes.

Artifacts are written to a new file that must not already exist, created with owner-only `0600` permissions and never overwritten, appended to or followed through a symlink. There is no `--force`. A stage that fails writes nothing and removes any file it had begun. Treat the artifacts as sensitive and delete them once the onboarding workflow is finished with them.

Exit codes match the rest of the Verify surface: `1` for invalid input, a rejected proof, an expired challenge or an artifact-write failure; `2` for an unexpected internal failure; `3` for an installed SDK without the preflight surface. Error messages name the flag and the constraint only — no path, identifier, certificate, signature or evidence content is ever printed or logged.

Chip preflight is EVM-only in v1. Solana-homed machines are readable through `peaqos verify status`, but the chip-preflight protocol does not extend to them.

### `peaqos init`

Interactive wizard that writes a `.env` file in the current working directory.

```bash
peaqos init                    # interactive (network, key, URLs, contract addresses)
peaqos init --non-interactive  # use environment variables and network defaults
peaqos init --force            # overwrite existing .env without confirmation
```

The Event Registry prompt defaults to `EVENT_REGISTRY_ADDRESS` from the shell when set, otherwise to the Tokenomics 2.0 registry mapped to the deployment ID entered just before it, otherwise to the selected network's default. Press Enter to accept it or type a replacement. On mainnet the deployment ID defaults to `peaq-mainnet`, so the prompt offers the 2.0 EventRegistry `0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e`: 2.0 machines submit events there. Clearing the deployment ID is legacy mode and falls back to `deployed_contracts.peaq.event_registry` in the canonical contract configuration, which publishes the 1.0 address. Testnet maps to no 2.0 registry and still has no network default. An empty default requires an address and blank input re-prompts.

The Tokenomics 2.0 deployment ID is asked before the MCR API URL and the Event Registry because it selects both of their defaults. `peaq-mainnet` resolves to the production MCR at `https://mcr.peaq.xyz`; `peaq-mainnet-michael` is a staging deployment and resolves to `https://mcr-staging.peaq.xyz`, so it never inherits the production host. Exporting `PEAQOS_MCR_API_URL` or `EVENT_REGISTRY_ADDRESS` still overrides either default. The written `.env` keeps its previous layout.

In non-interactive mode the same precedence applies: `EVENT_REGISTRY_ADDRESS`, then the 2.0 registry for the resolved `TOKENOMICS_DEPLOYMENT_ID`, then the network default. If the resolved value is empty (including an explicitly empty shell variable), `init` exits with code `3`, names `EVENT_REGISTRY_ADDRESS`, and does not create or overwrite `.env`, even with `--force`.

**Example (interactive):**

```
Network (mainnet, testnet) [mainnet]: mainnet
Private key source (paste, generate, wallet): generate
  Address:  0xAbCd...1234
  Key:      0xdeadbeef...
IMPORTANT: Save this private key securely. It will not be shown again.
RPC URL [https://peaq.api.onfinality.io/public]:
Tokenomics 2.0 deployment ID [peaq-mainnet]:
MCR API URL [https://mcr.peaq.xyz]:
Gas Station URL [https://depinstation.peaq.xyz]:
Event Registry address [0xA1e7F1d7B24dAb55Dc92491e6d9B89F6E925Ad1e]:
  Config: .env written to /home/operator/project/.env

  Running whoami to verify...
  Address :  0xAbCd...1234
  Network :  mainnet
  RPC URL :  https://peaq.api.onfinality.io/public
  Chain ID:  3338
  MCR API :  https://mcr.peaq.xyz

  Contracts:
    IdentityRegistry:  0x9075...0B6A
    IdentityStaking :  0x7d39...9B8E
    EventRegistry   :  0xA1e7...Ad1e
    MachineNFT      :  0xaF13...Bd61
    DID Registry    :  0x0000...0800
    Batch Precompile:  0x0000...0805
```

### `peaqos whoami`

Show the active wallet address, network, chain ID, and all contract addresses.

When either Solana setting is configured, `whoami` also shows the requested cluster, a redacted RPC origin, and available SDK deployment/artifact metadata. Solana RPC credentials, paths, query strings and fragments are hidden. Packaged IDL integrity is checked through the SDK; missing metadata is labeled unavailable. These local checks do not verify the live cluster, deployed binaries or machine-read capability. The added diagnostics load no additional signer and make no Solana RPC calls; `whoami` retains its existing wallet identity and peaq chain-ID lookup.

```bash
peaqos whoami
```

**Example:**

```
  Address :  0xAbCd...1234
  Network :  mainnet
  RPC URL :  https://peaq.api.onfinality.io/public
  Chain ID:  3338
  MCR API :  https://mcr.peaq.xyz

  Contracts:
    IdentityRegistry:  0x9075...0B6A
    IdentityStaking :  0x7d39...9B8E
    EventRegistry   :  0xA1e7...Ad1e
    MachineNFT      :  0xaF13...Bd61
    DID Registry    :  0x0000...0800
    Batch Precompile:  0x0000...0805
```

### `peaqos wallet`

Manage OWS (Open Wallet Standard) wallets in the local encrypted vault (`~/.ows/`). Requires the optional OWS dependency: `pip install peaq-os-cli[ows]`.

```
peaqos wallet create <name> [--words 12|24] [--json]
peaqos wallet import <name> (--mnemonic | --private-key-file <path>) [--index <n>] [--json]
peaqos wallet list [--json]
peaqos wallet show <name-or-id> [--json]
peaqos wallet export <name-or-id>
peaqos wallet delete <name-or-id>
peaqos wallet use <name-or-id>
```

**`wallet create`** — Generate a new mnemonic-backed wallet. Displays addresses for all supported chains (peaq, Base, Ethereum, Solana, Bitcoin, Cosmos, etc.). The mnemonic is never shown; use `wallet export` to retrieve it.

```bash
peaqos wallet create my-machine
peaqos wallet create my-machine --words 24   # 24-word mnemonic
peaqos wallet create my-machine --json       # JSON output
```

**`wallet import`** — Import an existing wallet from a BIP-39 mnemonic (hidden prompt) or a private key file.

```bash
peaqos wallet import recovered --mnemonic
peaqos wallet import from-file --private-key-file ./operator.key
```

**`wallet list`** — List all wallets in the vault. Shows Name, ID, peaq Address, Key Type, and Created date.

```bash
peaqos wallet list
peaqos wallet list --json   # full WalletInfo array
```

**`wallet show`** — Display full wallet details with addresses for every supported chain family.

```bash
peaqos wallet show operator
peaqos wallet show operator --json
```

**`wallet export`** — Export the recovery phrase or private key (requires confirmation).

```bash
peaqos wallet export my-machine
```

**`wallet delete`** — Securely delete a wallet from the vault (requires confirmation).

```bash
peaqos wallet delete my-machine
```

**`wallet use`** — Set a wallet as the active default by writing `PEAQOS_OWS_WALLET` to `.env`. Subsequent commands use this wallet via `load_client()`.

```bash
peaqos wallet use operator
```

**Migration from raw keys:**

```bash
peaqos wallet import my-operator --private-key-file ./operator.key
peaqos wallet use my-operator
peaqos whoami   # address matches the original key
```

### `peaqos activate`

For a complete mainnet walkthrough, see [Solana onboarding](docs/SOLANA_ONBOARDING.md): installation, the two wallets and what to fund on Solana, the DID input, the seven stages on both rails as production printed them on 2026-10-06 (TICKET-ADD-SVM-023), the waits and resume commands, keyless status, recovery and test execution.

EVM activation onboards a machine in **one atomic transaction**. `MachineStateAndSync.activateMachine` mints the ERC-721, stores the DID document, activates the subscription and registers the home chain in a single call — replacing the old register → mint → DID-write sequence.

**Solana onboarding (`--chain solana`) is terminal-first** (CORE-801): the Solana owner pays the bond on Solana, in PEAQ (the OFT, 9 decimals) or USDC, and the run takes seven stages:

```text
[1/7] Operator registration on peaq        registration   (reconciled, not sent, when already bound)
[2/7] Activation request on Solana         request
[3/7] Credit from peaq                     credit         (wait; the Trust Validator node)
[4/7] Settlement on Solana                 finalise       (the point of no return)
[5/7] Bond on peaq                         bond           (wait; node → peaq → node)
[6/7] Native onboarding on Solana          native_onboarding
[7/7] Link push Solana → peaq              linkage        (wait for peaq's watermark)
```

Without `--phase`, a write runs every stage the onboarding still needs in one process (`--operator-wallet`, `--owner-wallet`, `--max-in`, `--yes --wait-minutes 60`; about ten minutes on production), resumes by rerunning the same command at the stage the request's state names, and ends with `phases` and, once complete, `spent`: the escrow out and back, the rents out and back, the registration's gas, all read from receipts. `--dry-run` reads the plan, the registration, the activation quote (with the suggested `--max-in` to copy) and, on USDC, the finalise lookup table, keylessly. Every write result names its `resume_command`, its own transaction as `reference` and every transaction the run touched as `references`, the RPC hosts it used as `endpoints`, and an `audit_trail` (the registration, the terminal request, the transactions, and the link push's LayerZero message with its nonce and GUID or, on a Trust Validator route, its relay with the outbox nonce). `--dry-run --audit` prints the same trail keylessly, from `peaqos.log` and the original inputs. See [Solana onboarding](docs/SOLANA_ONBOARDING.md) for the waits, the audit trail, exit codes and the `journal_claim` / `chain_verified` evidence labels.

| Solana option | Meaning |
| --- | --- |
| `--phase` | `registration`, `request`, `finalise`, `native_onboarding`, `linkage` and `cancel_request` each run one stage subject to SDK checks. Omitted on a write, the command runs every phase the onboarding still needs in one process (the default run; see [Solana onboarding](docs/SOLANA_ONBOARDING.md)). The waits (`credit`, `bond`) resume through the default run. Preview accepts any phase, or omission for the full plan; `--phase linkage --dry-run` also quotes the link push, `--phase finalise --dry-run` the settlement. |
| `--operator-wallet` / `--owner-wallet` | OWS wallets of the peaq operator, which signs only `registration` on peaq (gas only, about 0.01 PEAQ, once per wallet), and of the Solana owner, which signs `request`, `finalise`, `native_onboarding`, `linkage` and `cancel_request` and **pays the bond**. Each defaults to `PEAQOS_OWS_WALLET` and is unlocked once per run. |
| `--pay-in` | `PEAQ` (default) or `USDC`: the token the owner pays the bond in on Solana. |
| `--max-in` | **Required** to open a request (the default run and `--phase request`): the escrow cap in base units of the `--pay-in` token. On PEAQ it must cover the bond (`ESCROW_BELOW_BOND` otherwise, with both figures); anything above the bond comes back at settlement. `--dry-run` prints the quote's suggestion. |
| `--slippage-bps` | `0`..`10000`, default `100`, both rails: on USDC the cushion on the swap's simulated cost, on PEAQ on the price stamp moving between the quote and the landing. |
| `--tier` | `entry`, `basic`, `pro`, or a program tier number `0`–`7` (`basic` is program tier 1); the quote refuses an unpriced one. |
| `--nonce` | `--phase cancel_request` only: the request to cancel; default the machine's latest. |
| `--solana-owner` | Required owner's base58 public key: it pays the bond and signs every Solana write. |
| `--solana-controller` | Optional native controller; omission uses the SDK's owner-only default. |
| `--evm-operator` | Optional peaq assertion: must equal the operator wallet's peaq address, which registration binds and the mint inherits. Refused before `[1/7]` with `EVM_OPERATOR_MISMATCH` and both addresses. |
| `--max-native-fee-lamports` | Required network/priority fee ceiling of each Solana write, excluding rent. |
| `--max-native-rent-lamports` | Required rent ceiling of each Solana write: the request (three accounts on a machine's first request), the mint (two), the link push (one). |
| `--max-link-push-fee-lamports` | `--phase linkage` and the default run: ceiling on the LayerZero messaging fee for the link push, needed only when the deployment routes the push over LayerZero (about 962,352 lamports at the 2026-09-24 quote, with the push's `lzReceive` gas at 1.5M; read it from `--dry-run --phase linkage`, which also names the route). The Trust Validator route (CORE-781) has no fee and ignores it. Refused with any other phase. |
| `--compute-unit-limit` | Optional: the mint's compute budget (default 600,000). The terminal writes and the link push use the SDK's per-instruction limits. |
| `--compute-unit-price-micro-lamports` | Optional priority price per compute unit for every Solana write (default `10000`: 9,700 lamports in priority fees across a PEAQ-rail onboarding, 12,700 on USDC, so the largest write, the mint, pays 11,000 with its base fee and fits a `--max-native-fee-lamports` of 50,000). `0` sends without priority, which Solana's public endpoint can drop under load; a dropped write is reconciled from the journal, never resent. |
| `--payment`, `--max-net-peaq-amount`, `--max-usdt-amount` | The removed reservation flow's options: refused on `--chain solana` with `OPTION_NOT_FOR_CHAIN` (exit `1`), naming `--pay-in` / `--max-in`. `--payment` keeps its default for `--chain peaq`; only a command-line value is refused. |
| `--from-block` | No longer read: since SDK 0.11 nothing scans a peaq log window. Accepted, with a note, so existing commands keep parsing. |
| `--timeout-seconds` | Optional time budget for each attempt of a write: recovery reads, the SDK's checks, the send and the receipt wait (default `300`). On a slow public RPC the budget can run out before signing (`CANCELLED_BEFORE_SUBMIT`) or before the receipt is seen (`PENDING` with the reference retained); raise it, or add `--wait-minutes`, rather than resubmitting. Never a spending limit. |
| `--wait-minutes` | Optional, 0 to 60 (default `0`): one budget for the run's three waits (the node's credit, at most 3 min; the bond, at most 6 min; the delivery of the link push, by the Trust Validator node or LayerZero) and every receipt reconcile (progress event `receipt_reconciled`), which never resends. Reads only. A leg still unresolved when the budget runs out exits `2` (`PENDING`); Ctrl-C exits `1` (`CANCELLED`). Refused with `--dry-run`. |
| `--audit` | `--dry-run` only: also read the onboarding's `audit_trail` keylessly, from `peaqos.log` in the working directory and the original inputs (the SDK needs both). Run it where the onboarding ran. Without a journal it prints a note; a failed read goes to `audit_errors` and the preview still prints. Nothing is signed or sent. |

All ceilings are explicit; zero is a strict value, not unlimited. For Solana, `--for` and `--machine-key` are rejected. Reuse the identity/DID flags with base58 manufacturer and verification-method controllers. Configure the paired Tokenomics deployment and Solana RPC using the shared configuration flags/environment. For example, with public identity and caller-selected budget variables already set:

```bash
peaqos activate --chain solana --dry-run --json \
  --solana-owner "$OWNER" \
  --machine-type Sensor --credential-subject-hex 0x00ff --manufacturer "$MANUFACTURER" \
  --tier basic --did-document ./native-did.json --pay-in PEAQ \
  --max-native-fee-lamports "$FEE_CAP" --max-native-rent-lamports "$RENT_CAP"
```

Then run it, copying the dry run's suggested `--max-in`:

```bash
peaqos activate --chain solana <the same inputs> --max-in "$MAX_IN" \
  --operator-wallet fleet-operator --owner-wallet fleet-owner \
  ${LINK_FEE_CAP:+--max-link-push-fee-lamports "$LINK_FEE_CAP"} --yes --wait-minutes 30
```

Leave `LINK_FEE_CAP` unset on production, where the Trust Validator node delivers the push with no messaging fee (both mainnet sets; `peaq-mainnet` since 2026-10-06); set it only on a deployment that routes the push over LayerZero. The dry run's `link_push.transport` says which.

Previews do not prove completed transactions. Before the mint JSON reports `did: null`. Consent covers only the stage it is shown for and the displayed caps; the SDK re-reads and re-checks before it signs. The settlement's terms open with "This cannot be cancelled afterwards." Interactive calls prompt on stderr, including `--json`; non-interactive calls require `--yes`. JSON stdout contains one result or error object.

Keep `peaqos.log` in the working directory and provide the same original inputs on every rerun. The journal holds every signed row before broadcast (the registration, the terminal request, the settlement or cancel, the mint, the link push), and every rerun passes them back to the SDK, which reconciles a recorded write instead of sending it again. A stage the chain already shows done is reported keyless: no wallet is unlocked. Missing, failed or delayed receipts never authorize automatic replacement. A persistence failure retains the reference in output: restore the journal before further action.

The result distinguishes new attempts, recorded attempts and independently satisfied state. Exit 0 means the selected stage succeeded with no reported errors, not that onboarding is complete; onboarding is complete when the SDK's aggregate stage is `complete`, after peaq applies the link push. Pending, refused or failed outcomes exit 2, input or consent errors 1, and typed configuration errors 3. A settled bond is not rolled back by stopping later stages. For ID-only current observations, use `machine status MACHINE_ID` with the Solana settings below.

The bond is always **quoted in PEAQ**. `--payment` chooses what settles it.

The transaction sender becomes the machine's **owner and bond payer**. There is no recipient parameter, and no operator-sponsored variant.

**Two modes:**

* **Self-owned** — no `--for`. Your configured signer owns the machine, pays the bond, and is its DID controller.
* **Machine-owned / operator-controlled** — `--for <machine-address> --machine-key <path>` together. The **machine** signs, owns the NFT, and pays gas and the bond; your operator address is recorded as DID controller. Both flags are required together; supplying one alone exits `1`.

Before any spend the CLI shows the bond, the voucher credit applied, and the net PEAQ, then asks. In machine-key mode it also states the changed rights:

| Who | Can | Cannot |
| --- | --- | --- |
| Machine wallet (owner) | Transfer the NFT; rotate or clear the controller | — |
| Operator (controller) | Lifecycle, subscription, relocation and DID actions | Transfer the NFT; rotate or clear the controller |

```bash
# Self-owned
peaqos activate \
  --machine-type Sensor \
  --credential-subject-hex 0xdeadbeef \
  --manufacturer 0x3333333333333333333333333333333333333333 \
  --tier entry \
  --did-document ./did.json

# Machine-owned, operator-controlled
peaqos activate ... --for 0xMachine --machine-key ./machine.key

# Preview the cost without submitting anything
peaqos activate ... --dry-run

# Scripted: --yes is the only non-interactive consent
peaqos activate ... --json --yes

# Settle the PEAQ bond in USDT, accepting 0.5% conversion movement
peaqos activate ... --payment usdt --slippage-bps 50
```

**Flags:**

| Flag | Purpose |
| --- | --- |
| `--machine-type` | Identity domain. Half of what fixes the permanent machine ID. |
| `--credential-subject-hex` | `0x`-prefixed identity-anchor bytes. The other half. |
| `--manufacturer` | Manufacturer address. **Recorded on-chain, never verified by the contract.** |
| `--tier` | `entry` \| `basic` \| `pro`. |
| `--did-document` | Path to a UTF-8 JSON DID document (schema below). |
| `--for` | Machine EOA address. Switches to machine-owned mode; requires `--machine-key`. |
| `--machine-key` | File holding the machine's `0x`-prefixed hex private key. |
| `--payment` | `peaq` (default) \| `usdt`. Which asset settles the PEAQ-quoted bond. |
| `--slippage-bps` | Accepted conversion movement, `0`..`10000`. **Required for `usdt`, rejected for `peaq`.** |
| `--skip-funding` | Skip balance check, 2FA and gas-station funding. |
| `--dry-run` | Preview and stop. Submits nothing. |
| `--json` | One JSON object on stdout. **Never implies consent.** |
| `--yes` / `-y` | Accept the displayed terms. The only non-interactive consent. |

`--machine-type` and `--credential-subject-hex` alone determine the machine ID (`uint256(keccak256(abi.encode(machineType, credentialSubject)))`). Neither can be changed after activation, and the same pair can never be onboarded twice.

**Paying in USDT.** The bond, the voucher credit and the net amount stay in PEAQ; only settlement differs. The CLI shows the USDT token, the quote, the accepted slippage and the resulting **maximum USDT**, then submits that exact maximum — it never widens a bound you approved. The allowance goes to `SubscriptionTokenProvisionPool`, which performs the conversion, rather than to `MachineSubscription`.

On this rail no PEAQ leaves your wallet, so the funding check accounts for gas alone. A bond covered entirely by voucher credit converts nothing, transfers nothing, emits no `PaidWithUsdt`, and reports `0` paid — which is what happened, not a missing value.

Private keys **must** come from a file. Inline key flags are intentionally unsupported — a file keeps the key out of shell history and `ps` output.

**Removed flags.** `--doc-url`, `--data-api` and `--visibility` wrote DID attributes *after* registration. Tokenomics 2.0 records the DID document atomically during activation, so there is no post-registration write to target. Passing any of them exits `1` with migration guidance; move documentation and API URLs into the `serviceEndpoints` array of `--did-document`.

**DID document schema.** Exactly three root fields, all required. Unknown fields, duplicate JSON keys, and a root `id` or `controller` are rejected — `id` is computed on-chain, and `controller` is set by the CLI from the mode.

```json
{
  "verificationMethods": [
    {
      "id": "#key-1",
      "methodType": "Ed25519VerificationKey2020",
      "controller": "0x1111111111111111111111111111111111111111",
      "publicKeyMultibase": "z6MkiExamplePublicKey"
    }
  ],
  "authentication": [0],
  "serviceEndpoints": [
    {
      "id": "#telemetry",
      "serviceType": "TelemetryService",
      "serviceEndpoint": "https://machine.example/telemetry"
    }
  ]
}
```

Each `authentication` entry is an index into `verificationMethods`. The CLI bounds-checks them because **nothing on-chain does**: `MachineRegistry` stores them unchecked at mint, so an out-of-range index would be recorded permanently.

**Environment:**

| Variable | Purpose |
| --- | --- |
| `PEAQOS_PRIVATE_KEY` | Operator key. In machine-key mode this signs nothing; it supplies the controller address. |
| `PEAQOS_RPC_URL` | RPC endpoint. Must match the deployment's chain. |
| `PEAQOS_NETWORK` | `mainnet` or `testnet`. |
| `PEAQOS_GAS_STATION_URL` | Gas-station base URL for the funding steps. |
| `TOKENOMICS_DEPLOYMENT_ID` | Deployment record, e.g. `agung-2026-08-28`. |

**No Tokenomics contract addresses are configured.** `TOKENOMICS_DEPLOYMENT_ID` selects a record from the SDK's approved snapshot; the five addresses travel with it and are verified against `InfoDesk.peer(role)` on-chain before any write. `peaqos whoami` shows which ones resolved.

The six legacy `*_ADDRESS` variables are still required, because the SDK client still takes them. They are unused by activation.

**Exit codes and `error_code`.** Four exit codes, per the CLI-wide convention. The specific condition rides on `error_code`, which is stable, appears in `--json`, and is bracketed in human output.

| Exit | Meaning | `error_code` |
| --- | --- | --- |
| `0` | Activated, already active, or a confirmed management write | `ACTIVATED`, `ALREADY_ACTIVE`, `CONFIRMED`, `ALREADY_CONFIRMED` |
| `1` | Bad input, declined, or non-interactive without `--yes` | `INVALID_INPUT`, `DEPRECATED_FLAG`, `MACHINE_KEY_MISMATCH`, `DECLINED`, `NOT_INTERACTIVE`, `MACHINE_ID_MISMATCH`, `CANCELLED_BEFORE_SUBMIT`, `SPONSORED_UNSUPPORTED`, `INVALID_TIER`, `LEGACY_REGISTRATION_UNSUPPORTED`, `RELOCATION_UNAVAILABLE`, `AUTHENTICATION_REWRITE_REQUIRED`, `SOLANA_OPERATION_UNSUPPORTED`, `ONBOARDING_FLOW_UNSUPPORTED`, `DRY_RUN_UNSUPPORTED`, `SUBSCRIPTION_NOT_ELIGIBLE`, `ZERO_ESCROW_AMOUNT`, `ESCROW_BELOW_BOND`, `NOT_ACTIVATION_PAYER`, `UNSUPPORTED_TOKEN_PROGRAM`, `FROM_BLOCK_AHEAD_OF_FINALIZED`, `CANCELLED`, `OPTION_NOT_FOR_CHAIN`, `EVM_OPERATOR_MISMATCH` |
| `2` | RPC / on-chain failure, or a submitted transaction still pending | `ORACLE_UNPRICED`, `NOT_FULL_MODE`, `INSUFFICIENT_PEAQ`, `QUOTE_MOVED`, `APPROVAL_FAILED`, `ALLOWANCE_INSUFFICIENT`, `ALREADY_ACTIVATED_RACE`, `TX_REVERTED`, `EVENT_MISMATCH`, `STATE_MISMATCH`, `STATE_CONFLICT`, `MACHINE_NOT_FOUND`, `MACHINE_HOMED_ELSEWHERE`, `RPC_FAILED`, `RPC_RATE_LIMITED`, `PENDING`, `ALREADY_ACTIVATED`, `ALREADY_TERMINATED`, `NOT_ACTIVATED`, `NOT_ACTIVE`, `NOT_SUSPENDED`, `TECHNICALLY_PAUSED`, `MACHINE_RELOCATING`, `NOT_MACHINE_OWNER`, `NOT_OWNER_OR_CONTROLLER`, `ERC721_INCORRECT_OWNER`, `ERC721_INSUFFICIENT_APPROVAL`, `ERC721_INVALID_APPROVER`, `ERC721_INVALID_OPERATOR`, `PROVISION_POOL_NOT_CONFIGURED`, `SLIPPAGE_EXCEEDED`, `MAX_USDT_EXCEEDED`, `INSUFFICIENT_RESERVE`, `INSUFFICIENT_USDT_RECEIVED`, `TIER_CHANGE_WHILE_ACTIVE`, `MACHINE_NOT_ONBOARDED`, `EVM_OPERATOR_UNSET`, `LINK_CONFIG_UNAVAILABLE`, `BRIDGING_DISABLED`, `TV_OUTBOX_MISSING`, `TV_INBOUND_REFUSED`, `LZ_SEND_LIBRARY_UNSUPPORTED`, `FEE_LIMIT_EXCEEDED`, `RENT_LIMIT_EXCEEDED`, `INSUFFICIENT_SOL`, `LINK_PUSH_TOO_LARGE`, `NONCE_IN_USE`, `TERMINAL_PAUSED`, `OPERATOR_NOT_REGISTERED`, `HOME_CHAIN_MISMATCH`, `NONCE_NOT_MONOTONIC`, `NONCE_JUMP_TOO_LARGE`, `REQUEST_ALREADY_OPEN`, `MACHINE_ALREADY_ACTIVE`, `ROUTE_NOT_WHITELISTED`, `PRICE_TIMESTAMP_IN_FUTURE`, `PRICE_TOO_OLD`, `TIER_NOT_PRICED`, `BOND_UNSIZEABLE`, `BOND_TOO_LARGE`, `ESCROW_BALANCE_MISMATCH`, `MACHINE_ID_TAKEN_ON_PEAQ`, `OPERATOR_BOUND_ELSEWHERE`, `REQUEST_EXPIRED`, `COMMIT_EXPIRED`, `REQUEST_NOT_COMMITTED`, `REQUEST_NOT_CANCELLABLE`, `REQUEST_NOT_YET_EXPIRED`, `ACTIVATION_REFUNDED`, `FINALISE_PATH_MISMATCH`, `SWAP_ROUTE_MISCONFIGURED`, `TICK_ACCOUNTS_INVALID`, `ACTIVATION_NOT_BONDED`, `ACTIVATION_REQUEST_MISMATCH`, `INSUFFICIENT_BALANCE`, `WAIT_READ_FAILED` |
| `3` | Missing or invalid configuration | `ADDRESSES_UNSET`, `DEPLOYMENT_UNKNOWN`, `TOKENOMICS_NOT_CONFIGURED`, `ADDRESS_OVERRIDE_MISMATCH`, `CHAIN_MISMATCH`, `CONTRACT_NOT_DEPLOYED`, `PEER_MISMATCH`, `SIGNER_UNAVAILABLE`, `SVM_DISPATCH_UNAVAILABLE`, `NOT_ECONOMIC_AUTHORITY`, `RENEWAL_TIER_UNSUPPORTED`, `PEER_UNSET`, `CHAIN_NOT_REGISTERED`, `FINALISE_TABLE_MISSING`, `FINALISE_TABLE_STALE`, `LINK_PUSH_UNSUPPORTED` |

`ONBOARDING_FLOW_UNSUPPORTED` is declared by the SDK for the removed reservation-first flow; nothing raises it since terminal-first onboarding (SDK 0.11). `TECHNICALLY_PAUSED` covers both chains: a technical pause on peaq or CORE-774's suite upgrade lock on Solana; nothing was signed, so rerun once it is released (the link push's `--dry-run` shows `upgrade lock: …`). The terminal-first codes (`OPERATOR_NOT_REGISTERED` through `MACHINE_ID_TAKEN_ON_PEAQ`) are the Solana terminal's refusals of a request, raised by the SDK's quote before anything is signed; `REQUEST_EXPIRED` through `UNSUPPORTED_TOKEN_PROGRAM` the terminal phases' and their waits'; `ACTIVATION_NOT_BONDED`, `ACTIVATION_REQUEST_MISMATCH` and `EVM_OPERATOR_MISMATCH` the mint's against the bonded request. On `--chain solana`, `INSUFFICIENT_BALANCE` names the owner's pay-in token account on Solana (the registration's gas shortfall stays `INSUFFICIENT_PEAQ`), and `INVALID_TIER` offers the program's tiers. `OPTION_NOT_FOR_CHAIN` and a local `EVM_OPERATOR_MISMATCH` / `ESCROW_BELOW_BOND` are refused before any read.

Two are worth telling apart, because they look alike and resolve differently:

* `ADDRESSES_UNSET` — the network is supported but its Tokenomics contracts are not deployed yet. Nothing is wrong with your configuration.
* `ORACLE_UNPRICED` — the contracts *are* deployed and reachable, but no PEAQ price is committed, so the bond cannot be quoted. A chain-side condition; nothing local will fix it.

**Pending transactions.** A submitted transaction whose receipt does not arrive is reported as `PENDING` at exit `2` with its hash — **not** as a failure. It may still mine, and nothing local can recall it. Re-run the same command: the CLI reconciles the recorded hash before considering another write, and a hash with no receipt blocks resubmission regardless of its age. Never submit a second activation for the same machine.

**Example — `--json` on success:**

```json
{
  "status": "activated",
  "mode": "self-owned",
  "deployment_id": "agung-2026-08-28",
  "chain_id": "9990",
  "machine_id": "57896044618658097711785492504343953926634992332820282019728792003956564819975",
  "tier": "entry",
  "owner": "0xDC5b20847F43d67928F49Cd4f85D696b5A7617B5",
  "controller": "0xDC5b20847F43d67928F49Cd4f85D696b5A7617B5",
  "bond_amount": "400000000000000000",
  "voucher_credit": "0",
  "net_peaq_amount": "400000000000000000",
  "transaction_hash": "0xabab...",
  "is_homed_locally": true
}
```

Machine IDs and every unbounded chain integer are **decimal strings**, never JSON numbers: a `uint256` exceeds what most parsers hold as a number. Progress, the preview and prompts go to stderr, so stdout carries only this object. For a Solana onboarding under `--json --yes`, stderr is JSON lines only: each stage's terms are a `consent` event (signer, wallet, action, every amount in base units against its cap, and `pay_in`, `escrow`, `bond`, `remainder_returns`, `point_of_no_return`), the run's plan block is a `plan` event, and environment warnings are `warning` events.

**`peaqos.log`.** Every submitted transaction is appended as one JSONL line — flushed *before* the receipt wait, which is what makes recovery possible — with mode `0600`. Never delete it while a transaction is outstanding; the hash is the only way to reconcile it.

```jsonl
{"step":"activate_machine","phase":"activation","status":"pending","tx_hash":"0xabab...","machine_id":"578960...","sender_nonce":7,"deployment_id":"agung-2026-08-28","chain_id":9990,"ts":"..."}
```


### `peaqos machine`

Everything after onboarding: lifecycle, subscription payments, ERC-721 ownership, DID updates, and cross-deployment relocation evidence.

```text
peaqos machine status MACHINE_ID
peaqos machine history MACHINE_ID --chain solana
peaqos machine suspend MACHINE_ID
peaqos machine resume MACHINE_ID

peaqos machine subscription activate MACHINE_ID --tier TIER --payment RAIL [--slippage-bps N]
peaqos machine subscription renew MACHINE_ID --payment RAIL [--slippage-bps N] [--tier TIER]

peaqos machine approve MACHINE_ID OPERATOR
peaqos machine approve-all OPERATOR --allow|--revoke
peaqos machine transfer MACHINE_ID TO [--unsafe] [--data-hex HEX]

peaqos machine did set-controller MACHINE_ID CONTROLLER
peaqos machine did clear-controller MACHINE_ID
peaqos machine did set-verification-methods MACHINE_ID --file PATH
peaqos machine did set-authentication MACHINE_ID [--index N]... | --clear
peaqos machine did set-services MACHINE_ID --file PATH

peaqos machine set-evm-operator MACHINE_ID EVM_OPERATOR --chain solana

peaqos machine relocation status MACHINE_ID \
  --destination-rpc-url URL --destination-deployment-id ID
```

Every write accepts `--yes` and `--json`. Every read accepts `--json`. Every
management write also accepts `--dry-run`, which previews and reconciles and
then stops without signing anything.

#### Solana-homed machines

The commands above reach a Solana-homed machine with `--chain solana`, which
selects the native SDK path. It is a routing choice and not a claim about where
the machine lives — the SDK still resolves that from the machine ID and refuses
a request for the wrong chain.

```text
peaqos machine status MACHINE_ID --chain solana
peaqos machine transfer MACHINE_ID TO --unsafe --chain solana
peaqos machine did set-controller MACHINE_ID CONTROLLER --chain solana
peaqos machine suspend MACHINE_ID --chain solana
peaqos machine set-evm-operator MACHINE_ID 0xOperator --chain solana
```

Native support is an optional install: `pip install 'peaq-os-cli[solana]'`.

Four things differ from the peaq path, and each is refused rather than
approximated:

- **Safe transfer does not exist.** A native machine is a program-owned record
  with no ERC-721 receiver hook, so `--unsafe` is required and `--data-hex` is
  refused, including `0x`.
- **ERC-721 approval does not exist.** `approve` and `approve-all` are refused
  when scoped to Solana. There are no transfer rights to delegate.
- **`set-evm-operator` is Solana-only.** It maintains the subscription link and
  has no peaq counterpart.
- **Addresses are base58**, and are never converted to or from EVM addresses.

`machine status --chain solana` keeps native and peaq facts in separate
sections: the native owner and controller are Solana keys, the EVM operator is
the peaq address whose subscription covers the machine, and the mirrored tier is
a copy rather than the authoritative subscription. Operator synchronization is
reported as `unavailable` with its reason — this SDK establishes it only while
performing the write, so no keyless read can confirm it.

It also reports the finalized peaq block every peaq value was read at
(`peaq.block`, `peaq.block_hash`; "Observed block N (finalized)"), the RPC hosts
read from (`endpoints`), and the `link` block: whether peaq applied the
machine's link push, read from the Solana push account and peaq's watermark by
ID alone. Human output prints it as one line, for example `Link: seq 1 on
Solana, applied on peaq (current; read at finalized block N)`, or `not pushed
yet`, `pushed (seq S), not yet applied on peaq`, `unavailable (<reason>)`.
`linked` is not "onboarding complete": completion needs the onboarding push's
own sequence, which only the journal names, so it stays with `activate`.

Solana status and onboarding evidence report `subscription_source` as `mirror`
or `terminal`, identifying which account the deployed programs use according to
the SDK. Both `SubscriptionMirror` and `SubscriptionTerminal` remain visible
when read, with separate values from the authoritative peaq subscription state.
Status JSON includes terminal presence, tier, status and last sequence; onboarding
evidence also includes its period end. These numeric fields are decimal strings.
An unread terminal has null fields, distinct from an account reported as `absent`;
human status labels it `not read` with `--chain solana`, or `not supplied by SDK`
on the default status path. Tier `0` is Entry and does not mean no subscription.

#### What an onboarding did: `machine history --chain solana`

`peaqos machine history MACHINE_ID --chain solana` lists every transaction of a
machine's terminal-first onboarding, on both chains, in time order: what it did
(`registration`, `request`, `credit`, `finalise`, `bond`, `native_onboarding`,
`linkage`, `link_delivery` …), who sent it (`owner`, `operator`, `node`,
`layerzero`), its slot or block (finalized) and what it cost its payer, then the
totals per payer. It is keyless and runs from any directory: the SDK rebuilds it
from the machine ID (`get_solana_onboarding_history`), with no `peaqos.log`, no
original inputs and no wallet. `-v` adds full references and explorer links
(Solscan, peaq Subscan, LayerZero Scan, or the Trust Validator outbox for a push the node carried; a convenience, not evidence); `--json`
carries every amount as a decimal string. A source that could not be read is
named and the command exits `2`; the rows shown are still exact.

Every `peaqos activate --chain solana` write also leaves `onboarding-<machine-id>.json`
beside `peaqos.log`: one entry per invocation (start, end, exit code, stages,
error codes), the latest references, `spent` and audit trail, and, once the
onboarding is complete, the same history. It is for people; reruns resume from
`peaqos.log` only.

`machine status --json` emits one stdout object on success or any handled failure after command syntax is accepted. Errors contain `status: "error"`, `error_code`, `message`, and `guidance`, plus available sanitized SDK context. Exit codes remain `1` for invalid input/unsupported actions, `2` for read failures, and `3` for missing configuration or dependencies. Human errors use stderr.

`machine status` and `machine relocation status` require no configured private key or wallet unlock. Read clients use a fresh temporary key held in memory to satisfy the SDK constructor. Write commands continue to use your configured signing credentials.

Optional Solana configuration:

| Setting | Environment variable | Root flag |
| --- | --- | --- |
| Solana RPC endpoint | `PEAQOS_SVM_RPC_URL` | `--svm-rpc-url` |
| Requested Solana cluster | `PEAQOS_SVM_NETWORK` | `--svm-network` |

Place root flags before the command group. Flags override environment variables, which override `.env`. Both settings are optional and have no default. They remain separate from `PEAQOS_RPC_URL`, `PEAQOS_NETWORK`, and `TOKENOMICS_DEPLOYMENT_ID`; choosing a cluster does not choose a machine's home chain. The CLI loads these settings for configuration, `whoami` diagnostics and native current-state status reads.

`machine status MACHINE_ID --json` preserves successful EVM management reads. When Solana settings are supplied and the SDK reports no local management record or a foreign home, it also supports the SDK ID-only native current-state read. No original onboarding inputs, history bound, wallet selection, journal or home override is required. The `native_current_state` section preserves `absent`, `reserved`, `pending_external`, `present`, `conflict`, `in_flight` and `unavailable`, with separate finalized peaq and confirmed native observations. Native identity and mirrored tier remain separate from authoritative peaq subscription state. Successful observations exit 0, including pending/conflicting states; missing configuration/dependencies exit 3 and read errors exit 2 while retaining available observations. `present` does not prove a creation receipt, original payload or completed linkage. Dedicated native DID-document command integration remains separate.

To display a requested cluster alongside your existing wallet diagnostics:

```bash
peaqos --svm-network devnet whoami
```

This displays an unverified setting and any available SDK metadata. It does not contact Solana or change the selected peaq deployment. Keep credential-bearing RPC URLs in local environment configuration rather than command arguments.

Status-specific codes include `CONFIG_ERROR` for local configuration, `DEPENDENCY_MISSING` for missing SDK packages, and `TIMEOUT` for transport timeouts. `MACHINE_HOMED_ELSEWHERE` means the EVM management read found another home chain; for a supported Solana home, configure both SVM settings and a compatible SDK to enable the ID-only fallback described above. Unknown SDK codes remain visible in `sdk_code` when provided.

**Machine IDs are full-width `uint256`.** Pass them as canonical unsigned decimal — no `0x`, no leading zeros, no exponents. They come back as decimal **strings** in `--json`, because a JSON number cannot carry a `uint256` without rounding it.

#### What every write does, in order

1. Validate your input locally. Nothing reaches the chain until it passes.
2. Reconcile the journal. A previously submitted transaction for the same action and machine is settled first — and blocks, rather than being repeated.
3. Ask the SDK for a preview. The chain, target contract, method signature, current state and intended effect all come from there; the CLI infers none of them.
4. Show that preview, plus what this specific action risks, and ask.
5. Submit once, recording the hash **before** waiting for a receipt.

`--json` changes output only. **It never confirms a write.** For scripts, `--yes` is the only non-interactive consent; without it, a non-TTY stdin fails before anything is approved or submitted. Previews and prompts go to stderr, so stdout carries exactly one JSON object.

#### Reruns never resubmit

Submitted hashes are appended to `./peaqos.log` (mode `0600`) before the receipt wait, which is the only way a timeout stays recoverable. Re-running the same command reconciles that record instead of sending a second transaction:

* **still pending** — reports the prior hash and exits `2`. Age, mempool visibility and account nonce are *not* treated as evidence it was dropped.
* **confirmed** — reports the reconciled result and exits `0`.
* **failed or conflicting** — reports every disagreement and exits `2`. Nothing is retried automatically, ever.

A pending write blocks by *action and machine*, not by its details. A pending transfer blocks another transfer regardless of recipient or safe/unsafe mode; a pending renewal blocks another even if a fresh quote differs.

DID arrays and transfer data are stored as **hashes**, never contents. So a rerun must supply the same `--file` or `--data-hex`: if the rebuilt input does not match the recorded hash, the CLI says so and stops rather than reconciling the wrong payload.

#### The parts with teeth

**`approve-all` is not scoped to one machine.** It grants the operator every MachineRegistry machine the signer owns, including ones onboarded later. The preview says so in those words.

**`transfer` is safe by default.** `safeTransferFrom` asks a contract recipient whether it can receive an ERC-721 and reverts if it cannot. `--unsafe` selects `transferFrom`, which does not — an incompatible contract recipient strands the NFT permanently. `--data-hex` selects the four-argument safe overload and is rejected with `--unsafe`, which has no such overload. The sender is read from SDK machine state, never typed. **Transfer does not rotate the DID controller**; the retained controller is shown before and after.

**Clearing a controller is its own command.** `set-controller` refuses the zero address and `clear-controller` takes no address, so a typo truncated to zero cannot revoke permissions while you believed you were granting them. Clearing states that the controller loses its owner-or-controller rights.

**DID setters replace whole arrays.** The file or index list you pass is the complete new state; anything currently stored and absent from it is removed. Authentication indices point into the verification-method array *by position*, so reordering or shrinking it changes what they refer to. `--index` and `--clear` are mutually exclusive and one is required — clearing is a deliberate change, never a default.

**Subscription rails.** `--payment peaq` passes back the previewed net PEAQ; `--payment usdt` requires `--slippage-bps` (`0`..`10000`) and passes back the previewed maximum USDT, unchanged. Renewal extends from the current period end, not from now. Points and credits accrue to the machine's **owner** even when a controller pays, and output keeps owner and caller/payer as separate fields.

**Renewal renews at the stored tier unless `--tier` is given.** With `--tier` (`entry` | `basic` | `pro`, the same choices as `activate`), the new period is bought at that tier and priced at the **full new-tier bond**. The contract refuses a tier change while the subscription is `Active` and applies it from `Grace` or `Runoff`; the preview shows `Stored tier`, `Renews at`, `Tier change` and `Tier change allowed` (`yes`, or `not while Active — from <period end>`) before asking for consent, and the confirmation names both tiers when they differ. A refused change fails with `TIER_CHANGE_WHILE_ACTIVE` before any allowance is granted: re-run without `--tier` to renew now, or with `--tier` after the period end. A tier equal to the stored one is passed through as typed. On a deployment whose SDK record does not carry the tier-taking `renew` (none does as of 2026-09-22), `--tier` fails with `RENEWAL_TIER_UNSUPPORTED` before any RPC. `PEER_UNSET` means the protocol's InfoDesk registry has no address for a role the write needs — the deployment is mis-wired or mid-upgrade; nothing was submitted.

**Relocation is read-only.** `relocation status` distinguishes `pending`, `arrived`, `completed` and `cancelled`, and never calls destination arrival alone a completion. It takes no raw EVM, protocol or LayerZero chain IDs; the SDK derives them from each deployment. Initiation and cancellation are absent until the protocol publishes a fee quote.

```bash
# Read the full management state
peaqos machine status 42 --json

# Take a machine out of service, then restore it
peaqos machine suspend 42 --yes
peaqos machine resume 42 --yes

# Renew at the stored tier, paying in USDT with 0.5% accepted movement
peaqos machine subscription renew 42 --payment usdt --slippage-bps 50 --yes

# Renew at a different tier (accepted from Grace or Runoff, refused while Active)
peaqos machine subscription renew 42 --payment peaq --tier pro --yes

# Hand ownership over safely, then let the new owner set their own controller
peaqos machine transfer 42 0xRecipient --yes
```

Exit codes and `error_code` values are the ones documented under [`peaqos activate`](#peaqos-activate) — one taxonomy for both.

### `peaqos stream publish`

Chunk, encrypt, and sign data for the peaqOS stream pipeline. Takes raw data (local file or URL), splits it into fixed-size chunks using the SDK's `build_chunk_chain`, encrypts each chunk with a unique XChaCha20-Poly1305 key wrapped for owner/operator/machine recipients, signs the chain with an Ed25519 key, and writes output files to a local directory.

Optionally upload encrypted blobs and chunk envelopes to S3-compatible storage with `--s3`. Local output is always written; S3 upload is additive.

```bash
peaqos stream publish \
  --input ./sensor-data.bin \
  --output-dir ./chunks/ \
  --owner-public-key <x25519-hex> \
  --operator-public-key <x25519-hex> \
  --machine-public-key <x25519-hex> \
  --signing-key-file ./ed25519.key \
  --machine-did did:peaq:0x... \
  --machine-key-id did:peaq:0x...#keys-1

# URL input (downloaded automatically)
peaqos stream publish \
  --input https://example.com/data.bin \
  --output-dir ./chunks/ \
  ...

# Custom chunk size and JSON output
peaqos stream publish \
  --input ./data.bin \
  --output-dir ./chunks/ \
  --chunk-size 1024 \
  --json \
  ...

# S3 upload (requires optional boto3 extra — see below)
peaqos stream publish \
  --input ./data.bin \
  --output-dir ./chunks/ \
  --s3 s3://my-bucket/data-stream/ \
  ...

# MinIO or other S3-compatible endpoint
peaqos stream publish \
  --input ./data.bin \
  --output-dir ./chunks/ \
  --s3 s3://peaq-stream/chunks/ \
  --s3-endpoint http://localhost:9000 \
  --s3-region us-east-1 \
  ...
```

#### Flags

| Flag | Required | Default | Description |
|------|----------|---------|-------------|
| `--input` | Yes | — | File path or URL to the source data. |
| `--output-dir` | Yes | — | Directory to write chunk files. Created if missing. |
| `--owner-public-key` | Yes | — | Owner X25519 public key (hex). |
| `--operator-public-key` | Yes | — | Operator X25519 public key (hex). |
| `--machine-public-key` | Yes | — | Machine X25519 public key (hex). |
| `--signing-key-file` | Yes | — | Path to Ed25519 private key file (hex). |
| `--machine-did` | Yes | — | Machine DID (e.g. `did:peaq:<address>`). |
| `--machine-key-id` | Yes | — | DID key reference (e.g. `did:peaq:<address>#keys-1`). |
| `--chunk-size` | No | 262144 | Bytes per chunk (256 KB default). |
| `--json` | No | — | Output manifest as JSON to stdout. |
| `--s3` | No | — | S3 bucket path (e.g. `s3://my-bucket/prefix/`). Enables upload. |
| `--s3-region` | No | from env | Region for the S3-compatible service. |
| `--s3-endpoint` | No | from env | Custom S3-compatible endpoint URL (MinIO, R2, etc.). |

#### S3 upload (optional)

S3 support requires the optional `boto3` dependency:

```bash
pip install peaq-os-cli[s3]
```

When `--s3` is used, the command uploads each `chunk-{n}.bin`, rewrites each `chunk-{n}.json` with an updated `storageRef`, uploads the envelopes and `manifest.json`, and adds `s3Bucket` / `s3Prefix` to the manifest. Upload progress is printed to stderr (suppressed by global `--quiet`). Partial upload failures are not cleaned up — re-run the command to retry idempotently.

**Credentials and endpoint configuration**

| Variable | Description |
|----------|-------------|
| `PEAQOS_S3_ACCESS_KEY_ID` | Access key ID for the S3-compatible service. |
| `PEAQOS_S3_SECRET_ACCESS_KEY` | Secret access key. |
| `PEAQOS_S3_REGION` | Default region (overridden by `--s3-region`). |
| `PEAQOS_S3_ENDPOINT` | Default custom endpoint URL (overridden by `--s3-endpoint`). |

When `PEAQOS_S3_*` credentials are not set, the CLI falls back to the standard `boto3` credential chain (shared credentials file, instance profiles, etc.).

#### Output

Creates `chunk-{n}.json` (envelope), `chunk-{n}.bin` (encrypted data), and `manifest.json` in the output directory. Without `--s3`, each chunk's `storageRef` is `null`.

**Example (human output):**

```
Chunking 1048576 bytes into 4 chunks...

Published 4 chunks to ./chunks/

  Schema:       peaq.stream.chunks.v1
  Source Hash:  0xabc123...
  Chunk Size:   262144 bytes
  Machine DID:  did:peaq:0xAbc...

  Files:
    chunk-0.json  chunk-0.bin
    chunk-1.json  chunk-1.bin
    chunk-2.json  chunk-2.bin
    chunk-3.json  chunk-3.bin
    manifest.json
```

**Example (human output with `--s3`):**

```
Chunking 1048576 bytes into 4 chunks...
Uploading chunk-0.bin to S3...
Uploading chunk-0.json to S3...
...

Published 4 chunks to ./chunks/
Uploaded 4 chunks to s3://my-bucket/data-stream/

  Schema:       peaq.stream.chunks.v1
  Source Hash:  0xabc123...
  Chunk Size:   262144 bytes
  Machine DID:  did:peaq:0xAbc...
  S3 Bucket:    my-bucket
  S3 Prefix:    data-stream/

  Files (local + S3):
    chunk-0.json  chunk-0.bin  → s3://my-bucket/data-stream/chunk-0.bin
    chunk-1.json  chunk-1.bin  → s3://my-bucket/data-stream/chunk-1.bin
    chunk-2.json  chunk-2.bin  → s3://my-bucket/data-stream/chunk-2.bin
    chunk-3.json  chunk-3.bin  → s3://my-bucket/data-stream/chunk-3.bin
    manifest.json              → s3://my-bucket/data-stream/manifest.json
```

With `--json` and `--s3`, stdout includes `s3Bucket`, `s3Prefix`, and a `chunks` array with per-chunk `storageRef` values.

### `peaqos show machine`

Display the current MCR profile envelope for a machine DID, including current metadata, rating and recent events. These public HTTP reads require no wallet, RPC, program accounts or IDLs. With `TOKENOMICS_DEPLOYMENT_ID` set, use a decimal machine DID; without it, legacy queries retain address-form machine DIDs.

```bash
peaqos show machine did:peaq:42
peaqos show machine did:peaq:42 --json   # raw JSON to stdout
```

Example output:

```
  Machine: did:peaq:42

    Machine ID:  42
    Operator  :  did:peaq:0x9Eea...641C

    Current Metadata:
      documentation_url:  https://example.com/docs
      data_visibility  :  public

    MCR Snapshot:
      Rating     :  B
      Score      :  31 / 100
      Bond Status:  bonded

    Recent Events:
      Total            :  10
      Last Event       :  2026-04-20T14:30:00Z
      Last Origin Value:  123
      Last Currency    :  HKD
      Last Subunit     :  100
      Last Status      :  ok
      Last USD Value   :  0.13
```

When the most recent event is a revenue event, the block surfaces the PRO-336 / PRO-334 FX fields. `Last USD Value` is rendered as USD dollars (Decimal-quantised to 2 places — `usd_value=13` → `0.13`, `usd_value=150` → `1.50`). When `amount_status` is `"unsupported_currency"` or `"fx_unavailable"`, the `Last USD Value` row shows `—` (em-dash) so the CLI never displays a misleading USD number for a row whose FX state the server flagged as unreliable:

```
    Recent Events:
      Total            :  10
      Last Event       :  2026-04-20T14:30:00Z
      Last Origin Value:  100
      Last Currency    :  XYZ
      Last Status      :  unsupported_currency
      Last USD Value   :  —
```

Activity events (eventType=1) omit all five FX lines.

### `peaqos show operator machines`

List machines owned or controlled by an operator in a tabular summary (`peaqID`, `Machine ID`, `MCR`, `Rating`). Operator DIDs remain address-based in both modes. Membership does not establish native ownership or permission to sign Solana consent, and this endpoint does not promise complete native enumeration.

```bash
peaqos show operator machines did:peaq:0x<40-hex>
peaqos show operator machines did:peaq:0x<40-hex> --json   # raw JSON to stdout
```

Example output:

```
  Operator: did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C
  Machines: 3
  Membership: owned or controlled

peaqID                                              Machine ID   MCR   Rating
────────────────────────────────────────────────────────────────────────────
did:peaq:45                              45           31    B
did:peaq:46                              46           75    A
did:peaq:47                              47           55    BB
```

### `peaqos qualify event`

Submit one machine event on-chain via the Event Registry (`submitEvent`).

#### Prerequisites

Configure `.env` in the CLI directory (or export the same variables): private key, RPC URL, and contract addresses. See `peaq_os_cli.config.load_client`.

#### Required flags

| Flag | Meaning |
|------|---------|
| `--machine-id` | Positive integer machine identity. |
| `--type` | `revenue` or `activity`. |
| `--value` | Non-negative integer **as ISO 4217 subunit** per PRO-334. **BREAKING**: pre-PRO-334 callers passed whole-currency amounts; the wire is now subunit. Example: `HK$1.23 → --value=123 --currency HKD`; `¥100 → --value=100 --currency JPY` (JPY has no minor unit). Partner is responsible for the conversion. |
| `--ts` | Event time: Unix seconds (digits only) or ISO 8601 with timezone (`Z` or `+hh:mm`). |

Use a timestamp **on or before** the chain's block time. If `--ts` is ahead of the network clock, the contract can revert with `FutureTimestamp`.

#### Common options

| Flag | Meaning |
|------|---------|
| `--trust` | `self` (default), `onchain`, or `hardware`. |
| `--source-chain` | `same`, `peaq`, or `base` (maps to a chain id for the SDK). |
| `--source-tx` | 32-byte tx hash (hex, `0x` optional). **Required** when `--trust` is `onchain`. |
| `--raw-data` | File path; file bytes are hashed and stored as the event data hash. |
| `--metadata` | File path; bytes are sent as on-chain metadata. |
| `--currency` | Per PRO-336 §6 — currency code for revenue events (e.g. `USD`, `HKD`, 3-10 uppercase alphanumeric chars). Activity events take `""`. **Omit** to use the SDK's smart default (`"USD"` for revenue, `""` for activity). |

#### Examples

```bash
# Revenue event (self-reported trust, default source chain)
peaqos qualify event --machine-id 42 --type revenue --value 100 --ts 1735000000
```

```
Event submitted.
  Machine ID:  42
  Type:        revenue
  Value:       100
  Trust:       self-reported
  Tx:          0x3f4a8c1e2d9b7f05a6c3e8d1f4b2a7c9e0d5f3b1a8e2c6d9f7b4a1e3c5d8f2b4
  Data Hash:   0xa1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890
```

```bash
# Activity with ISO timestamp
peaqos qualify event --machine-id 42 --type activity --value 0 --ts "2026-04-22T12:00:00Z"

# On-chain-verified event with a source tx hash
peaqos qualify event --machine-id 42 --type revenue --value 200 --ts 1735000000 \
  --trust onchain \
  --source-tx 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab

# Attach a payload file (raw data is hashed on submit)
peaqos qualify event --machine-id 42 --type activity --value 0 --ts 1735000000 \
  --raw-data ./sensor.bin
```

### `peaqos qualify mcr`

Read a machine's credit rating through the SDK's public HTTP query client. A configured or locked wallet is not opened for this read.

```bash
# Tokenomics mode: the deployment selects the MCR endpoint.
export TOKENOMICS_DEPLOYMENT_ID=peaq-mainnet
peaqos qualify mcr did:peaq:42
peaqos qualify mcr did:peaq:42 --json
peaqos show machine did:peaq:42 --json
peaqos show operator machines did:peaq:0x9Eeab1aCcb1A701aEfAB00F3b8a275a39646641C --json
```

Machine DIDs use canonical decimal IDs in `0..2^256-1`, without leading zeros except `0`. Operator DIDs always use `did:peaq:0x<40-hex>`. Legacy machine DIDs retain that address form only when `TOKENOMICS_DEPLOYMENT_ID` is absent from both the environment and `.env`. A present but empty deployment setting is an error. Tokenomics queries use the SDK-selected endpoint and ignore legacy API overrides; there is no automatic legacy fallback.

`--json` preserves the SDK response envelope and converts every `machine_id` to a decimal string, including nested profile/list entries. Scores, protocol chain IDs and timestamps retain their numeric types; null metrics stay null. The rating label is `mcr`; the CLI does not synthesize a duplicate `mcr_rating` field.

A non-empty `rating_unavailable` explains why no rating can be computed. Human output labels the score as a floor and shows the reason; a zero score alone does not imply unavailable history. Null trend, average revenue and last-update values display as unavailable. `mcr_degraded` remains independent of this reason.

Native profile fields, when returned, stay inside `peaqos`: `owner` and nullable `controller` retain their base58 case, `home_chain` is a protocol ID (Solana is `5`), and `machine_type`/`relocating` retain their types. Missing fields remain absent. A null `operator` means not designated; it does not imply missing native ownership. Metadata comes from the current profile, without a legacy attribute fallback.

After Click accepts command syntax, handled `--json` failures emit one stdout object with `status: "error"`, `error_code`, `message`, `guidance`, and any safe `context`. There is no duplicate Click error line. Exit codes are `1` for input/confirmation, `2` for API/network/response failures, and `3` for configuration/capability failures. Typed `404`, distinct `409` home/eligibility codes and `503` outage codes are preserved; raw server bodies and signing material are omitted.

### `peaqos monetize`

Manage a machine's **Tokenomics 2.0 monetisation** decision in the MCR. `status` is a public read; `opt-in` and `opt-out` are signed, off-chain decisions. The SDK owns compatibility, authorization context, message signing, retries, HTTP and response validation. Live query compatibility is independent of the planned native write rollout.

#### Usage

```text
peaqos monetize status  KEY [--timeout-seconds SECONDS] [--max-unavailable-retries INTEGER] [--unavailable-retry-delay-seconds SECONDS] [--json]
peaqos monetize opt-in  KEY [--timeout-seconds SECONDS] [--max-unavailable-retries INTEGER] [--unavailable-retry-delay-seconds SECONDS] [--yes] [--json]
peaqos monetize opt-out KEY [--timeout-seconds SECONDS] [--max-unavailable-retries INTEGER] [--unavailable-retry-delay-seconds SECONDS] [--yes] [--json]
```

`KEY` is either a canonical decimal machine ID in `0..2^256-1`, or `did:peaq:<canonical-decimal-machine-id>`. A leading zero (except the value `0`), sign, whitespace, fraction, exponent, hexadecimal value, address-based DID, or value at least `2^256` is rejected locally before SDK I/O. The decimal DID suffix is already the machine ID; the CLI performs no legacy DID lookup.

| Option | Default | Behaviour |
| --- | --- | --- |
| `--timeout-seconds` | `30.0` | Positive, finite end-to-end deadline for each SDK call. Confirmation time is outside it. |
| `--max-unavailable-retries` | `2` | Non-negative retries for the write after `MACHINE_UNAVAILABLE`; SDK reads are not retried. |
| `--unavailable-retry-delay-seconds` | `1.0` | Positive, finite pause before each SDK write retry. |
| `--yes` / `-y` | off | The only non-interactive confirmation for `opt-in` and `opt-out`. |
| `--json` | off | Emit one state or structured error object on stdout. It changes output only and never confirms a write. |

Every command requires `TOKENOMICS_DEPLOYMENT_ID`. The SDK resolves the MCR URL, chain ID, Machine Registry, and API version from that approved deployment and the live compatibility signal. `status` and already-satisfied decisions require no signer. A needed EVM write uses the selected `PEAQOS_OWS_WALLET` and shared vault-passphrase settings, or `PEAQOS_PRIVATE_KEY` when no wallet is selected; its signer must be the current owner or controller. An OWS failure never falls back to a raw key. Optional signing dependencies load only when a write is needed.

Legacy `PEAQOS_MCR_API_URL`, `IDENTITY_REGISTRY_ADDRESS`, and `PEAQOS_RPC_URL` values are not read by these monetisation commands. An explicitly supplied legacy `--api-url` is rejected with exit `3` and is intentionally hidden from help.

Before a write, the CLI reads the current state. An already-satisfied state returns successfully without a prompt, signature, or PUT. Otherwise the SDK prepares the authorization context and the CLI shows the deployment, decimal machine ID, current/requested states, home protocol ID, native owner and selected signing chain/address. Unavailable metadata is labeled explicitly. The reviewed context is passed back to the SDK, which rejects changed home/owner context before signing and on retries. Without `--yes`, stdin must be a TTY and the operator must confirm that the decision is stored off-chain in the MCR with that signing address as authorizer. Non-TTY input or a declined prompt exits `1` without a PUT.

Opt-in eligibility, including bond and lifecycle state, is enforced by the MCR. Opt-out is not locally blocked by those conditions; only an exact final `503 MACHINE_UNAVAILABLE` from its initial state read permits proceeding with unknown current status. A TTY confirmation or `--yes` is still required; other failures, including `503 CHAIN_UNAVAILABLE`, stop the write. Compatibility, authorization and context checks remain mandatory.

Planned Solana writes require the SDK's verified server capability and the current native owner, using the selected OWS wallet and the SDK's Ed25519 message signer. Native controllers, EVM operators and former owners are not authorized by those roles alone. The currently verified local SDK leaves this capability disabled. `--yes` skips only the prompt; it cannot enable native rollout or bypass validation. No private key export, alternate signer, transaction, gas or rent workflow is used.

#### Examples

```bash
# Turn monetisation on (prompts on interactive stdin)
peaqos monetize opt-in did:peaq:42

# Turn it off from a script after reviewing the decision
peaqos monetize opt-out 42 --yes

# Read the current state (public, no signing key)
peaqos monetize status 42
```

```
  Monetization status

    Deployment:  agung-2026-08-28
    Machine ID:  42
    Status:      OPTED_IN
    Signer:      0x7099...79C8
    Updated At:  2026-07-13T12:00:04Z
```

```bash
# Machine-readable JSON for jq / scripts
peaqos monetize status 42 --json
```

```json
{
  "machine_id": "42",
  "status": "OPTED_IN",
  "signer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  "updated_at": 1783944004
}
```

A `PENDING` state renders `Signer` and `Updated At` as `—` (and as `null` in JSON). `machine_id` is always a decimal string in JSON, including values above JavaScript's safe-integer range.

Tokenomics 2.0 state normally starts at `PENDING`; Tokenomics 1.0 decisions, IDs, and signatures are not translated or imported.

#### Typical failures

| Situation | Exit | What you see |
|-----------|------|----------------|
| Invalid or noncanonical `KEY` or option value | `1` | Validation message before SDK I/O |
| Declined prompt or non-TTY write without `--yes` | `1` | No write was sent |
| Missing/unknown `TOKENOMICS_DEPLOYMENT_ID` or incompatible MCR API | `3` | Deployment or compatibility guidance |
| Explicit legacy `--api-url` | `3` | Use `TOKENOMICS_DEPLOYMENT_ID`; raw URL overrides are unsupported |
| Signing key is not owner or controller (`UNAUTHORIZED_SIGNER`) | `2` | Owner/controller authorization guidance |
| Ineligible opt-in (`MACHINE_NOT_BONDED` / `MACHINE_DEACTIVATED`) | `2` | Server code and remediation |
| Network failure, timeout, cancellation, bad response, or API rejection | `2` | Stable mapped error; server code when available |

A timeout or network failure during a PUT is ambiguous: the MCR may have applied it. The CLI reports that the state is unknown and instructs you to rerun the same command. That rerun performs the GET first and sends no second PUT when the requested state is already present.

### `peaqos monetize provision`

Provision an **opted-in** machine as a compute provider node from a schema-driven manifest. These commands are thin wrappers over the SDK's manifest runner: the SDK fetches and validates the manifest, runs every command through its executor, redacts secrets, and decides success. The CLI adds argument parsing, interactive prompts, terminal rendering, and the resume state file — it never runs a provisioning command itself.

Success means **verification probes pass**, not that install commands exited zero. The manifest is provider-agnostic: all provider specifics (commands, prompts, captures, probes, commission field) live in the manifest, never here.

#### Usage

```text
peaqos monetize provision run       <provider> --machine <key> [--mode manual|auto] [--grant-sudo]
                                               [--inputs <file>] [--manifest-repo <url>] [--manifest-version <v>]
                                               [--state-file <path>] [--resume] [--yes]
peaqos monetize provision preflight <provider> [--machine <key>] [--mode manual|auto] [--grant-sudo]
                                               [--inputs <file>] [--manifest-repo <url>] [--manifest-version <v>]
peaqos monetize provision verify    <provider> [--state-file <path>] [--machine <key>] [--inputs <file>]
                                               [--mode manual|auto] [--grant-sudo]
                                               [--manifest-repo <url>] [--manifest-version <v>] [--json]
```

- **`<provider>`** — Manifest provider key, e.g. `akash`. Lowercase letters, digits, and single hyphens; the slug is interpolated into the manifest URL, so anything else is rejected.
- **`--machine`** — Required on `run`: a canonical decimal Tokenomics machine ID or `did:peaq:<canonical-decimal-machine-id>`. It drives the deployment-bound monetisation pre-check. This key contains no payout address, so `run` also requires `PEAQOS_MACHINE_WALLET_ADDRESS` for the manifest's `context.machine_wallet_address`; the CLI never substitutes the signing key. On `preflight` and `verify`, `--machine` remains an optional provisioning-context value and does not perform the monetisation pre-check.
- **`--mode`** — `manual` (default) confirms each command step; `auto` runs unattended and **requires `--grant-sudo`**. The exact command is always shown before it runs.
- **`--grant-sudo`** — Allow `sudo: required` commands to elevate, scoped to the manifest's `allowedCommands`. `sudo: optional` commands always run unprivileged.
- **`--inputs`** — YAML or JSON file of manifest input values. Any required input still missing is prompted; **secret inputs are read hidden** and never echoed, logged, or persisted.
- **`--state-file`** — Resume/idempotency state, default `./peaqos-provision-state.json`. Written atomically (`0600`) after each completed step and at owner-action pauses.
- **`--resume`** — Continue an interrupted run. The persisted non-secret inputs are restored, so the original `--inputs` file is not needed (secrets are re-prompted, since they are never persisted); an explicit `--inputs` value still wins. Applied only after the state file's provider, machine wallet, manifest digest, resolved inputs, and schema version all match this invocation; any mismatch exits `1` rather than skipping steps done under different values.
- **`--yes` / `-y`** — Auto-confirm command steps. **Owner-action steps still pause** — funding, DNS, and signing handoffs are never auto-confirmed.

`run` requires `PEAQOS_MANIFEST_REPO_URL` (or `--manifest-repo`), `TOKENOMICS_DEPLOYMENT_ID` for the public monetisation pre-check, and `PEAQOS_MACHINE_WALLET_ADDRESS` for provider payout context. The pre-check does not use `PEAQOS_MCR_API_URL`, `IDENTITY_REGISTRY_ADDRESS`, `PEAQOS_RPC_URL`, or a signing key.

#### What `run` does

```text
[1/6] Monetisation status   deployment-bound SDK GET; anything but OPTED_IN stops here
[2/6] Manifest              fetched and pinned by sha256 over the source bytes
[3/6] Inputs                merged, prompted, validated; pin written to the state file
[4/6] Pre-flight            a blocking failure aborts before any phase runs
[5/6] Provisioning          steps run, state checkpointed after each one
[6/6] Verification          the node is live only when every success probe passes
```

#### Examples

```bash
# Full manual run (confirms each command; owner actions always pause)
export PEAQOS_MACHINE_WALLET_ADDRESS=0x9a5F1E244c15e491Ae571c5bF77fDD836ddc37C5
peaqos monetize provision run akash \
  --machine did:peaq:42 \
  --inputs ./akash-inputs.yaml
```

```
  Provider node provisioned.
    Provider:         akash
    Version:          0.3.0
    Probes:           4/4 passed
    providerAddress:  akash1abc…xyz
```

```bash
# Dry gate before committing to a run
peaqos monetize provision preflight akash --inputs ./akash-inputs.yaml

# Unattended run within the manifest's sudo allowlist
peaqos monetize provision run akash \
  --machine 42 \
  --inputs ./akash-inputs.yaml --mode auto --grant-sudo --yes

# Continue after an interruption (persisted inputs are restored; secrets are re-prompted)
peaqos monetize provision run akash \
  --machine did:peaq:42 --resume

# Re-check an already-provisioned node against the pinned manifest
peaqos monetize provision verify akash --json | jq '.[] | select(.passed == false)'
```

`verify` refetches the **pinned** manifest, recomputes its digest, and rebuilds probe context from the persisted inputs, captures, and wallet. It never resolves `latest` silently, and a capture a probe needs but the state file cannot supply is an error — captures come from provisioning, so they are never prompted or rendered empty.

#### Typical failures

| Situation | Exit | What you see |
|-----------|------|----------------|
| Machine not opted in (`PENDING` / `OPTED_OUT`) | `2` | `Machine … is not opted in … Run 'peaqos monetize opt-in …' first` (no manifest fetched) |
| Bad `<provider>` slug, bad `--machine`, `--mode auto` without `--grant-sudo` | `1` | Names the problem and the fix |
| Missing `PEAQOS_MACHINE_WALLET_ADDRESS` on `run` | `1` | Requests the provider payout wallet override |
| Manifest fetch / schema failure (incl. non-`compute` category) | `2` | Names the URL or the offending schema path |
| Input fails the manifest's validation | `1` | Names the input key and constraint |
| Blocking pre-flight check | `2` | Lists the failing checks with remediation (nothing is provisioned) |
| Step failure | `2` | Names `phase/step` and hints `--resume` |
| `--resume` binding mismatch | `1` | Names the differing field; start a fresh run |
| Resume needs an unrestorable secret capture | `2` | Names the capture; start a fresh run |
| Verification probes failed | `2` | Lists the failed probes (the node is **not** reported live) |
| `verify` digest mismatch / stale override | `2` | `manifest changed since provisioning`, or requires a fresh `run` |
| Missing `PEAQOS_MANIFEST_REPO_URL` or `TOKENOMICS_DEPLOYMENT_ID` | `3` | Missing-var message |

### `peaqos scale agent pair`

Pair an AI agent to an activated machine via the challenge-sign flow. Requests a pairing challenge, collects the agent's EIP-191 signature, and creates the pairing with the signed proof. The one-time `pairing_token` is printed on success. Subsequent commands (such as `peaqos scale search`) use that token via `--pairing-token-file`.

Requires an activated machine and a platform API key (`PEAQOS_ORCH_API_KEY`). The token is rendered to stdout exactly once and is never written to `peaqos.log` or `--verbose` output.

```bash
# Interactive: prompts for the EIP-191 signature
peaqos scale agent pair \
  --machine-id mach_1 \
  --agent-address 0xAgent...0101 \
  --agent-provider teneo \
  --agent-role machine-market-buyer

# Non-interactive: reads signature from file, skips confirmation
peaqos scale agent pair \
  --machine-id mach_1 \
  --agent-address 0xAgent...0101 \
  --agent-provider teneo \
  --agent-role machine-market-buyer \
  --agent-signature-file sig.txt \
  --per-tx-limit 5 --daily-limit 20 --currency USD \
  --allowed-skills pyth-price-feed,walrus-store \
  --yes

# JSON output (requires --agent-signature-file)
peaqos scale agent pair \
  --machine-id mach_1 \
  --agent-address 0xAgent...0101 \
  --agent-provider teneo \
  --agent-role machine-market-buyer \
  --agent-signature-file sig.txt \
  --json
```
### `peaqos scale machine onboard`

Onboard a machine into the Machine Market. Four-step wizard: request an identity challenge, sign it (via key file, OWS wallet, or manual paste), register the machine with identity proof, and activate it.

The CLI verifies that the signer address matches one of the DID controller addresses returned by the challenge endpoint before proceeding to registration.

**Signing modes** (checked in order):

1. `--identity-signature-file` — read a pre-signed EIP-191 signature from file.
2. `--identity-key-file` — read a private key from file and sign the challenge.
3. OWS wallet (`PEAQOS_OWS_WALLET` set) — sign via the active wallet. The key is exported from the vault for the duration of signing only.
4. None of the above — display the challenge and prompt for a pasted signature.

A raw `PEAQOS_PRIVATE_KEY` client does **not** auto-sign; it always falls through to mode 4 (manual prompt). Only `--identity-signature-file`, `--identity-key-file`, or an active OWS wallet avoid the prompt.

```bash
# Sign with a DID controller key file (non-interactive)
peaqos scale machine onboard \
  --identity-ref peaqos:machine:my-bot \
  --display-name "My Edge Node" \
  --owner-id operator-42 \
  --machine-type edge-node \
  --runtime-profile linux-docker \
  --identity-key-file ./controller.key \
  --capabilities price-feed,qvac \
  --skill-keys pyth-price-feed \
  --labels env=production,region=eu \
  --yes

# Auto-sign via OWS wallet (PEAQOS_OWS_WALLET must be set)
peaqos scale machine onboard \
  --identity-ref did:peaq:0xAbCd...1234 \
  --display-name "My Edge Node" \
  --owner-id operator-42 \
  --machine-type edge-node \
  --runtime-profile linux-docker \
  --yes

# Pre-signed signature from file
peaqos scale machine onboard \
  --identity-ref peaqos:machine:my-bot \
  --display-name "My Edge Node" \
  --owner-id operator-42 \
  --machine-type edge-node \
  --runtime-profile linux-docker \
  --identity-signature-file sig.txt \
  --yes

# Interactive: prompts for a pasted EIP-191 signature
peaqos scale machine onboard \
  --identity-ref did:peaq:0xAbCd...1234 \
  --display-name "My Edge Node" \
  --owner-id operator-42 \
  --machine-type edge-node \
  --runtime-profile linux-docker

# JSON output (requires a non-interactive signing method)
peaqos scale machine onboard \
  --identity-ref peaqos:machine:my-bot \
  --display-name "My Edge Node" \
  --owner-id operator-42 \
  --machine-type edge-node \
  --runtime-profile linux-docker \
  --identity-key-file ./controller.key \
  --json
```

#### Flags

| Flag | Required | Description |
|------|----------|-------------|
| `--identity-ref` | Yes | Machine identity reference (`did:peaq:0x...` or `peaqos:machine:<id>`). |
| `--display-name` | Yes | Human-readable machine name. |
| `--owner-id` | Yes | Operator/owner identifier. |
| `--machine-type` | Yes | Machine type (e.g. `edge-node`, `robot`). |
| `--runtime-profile` | Yes | Runtime profile (e.g. `linux-docker`). |
| `--capabilities` | No | Machine capabilities (comma-separated). |
| `--skill-keys` | No | Skill keys the machine supports (comma-separated). |
| `--labels` | No | Machine labels (`key=value`, comma-separated). |
| `--identity-signature-file` | No | Path to file containing a pre-signed EIP-191 signature. |
| `--identity-key-file` | No | Path to file containing the DID controller private key for signing. Mutually exclusive with `--identity-signature-file`. |
| `--skip-activate` | No | Register in `draft` status without activating. |
| `-y`, `--yes` | No | Skip confirmation prompt. |
| `--json` | No | Output raw JSON. Requires a non-interactive signing method (`--identity-signature-file`, `--identity-key-file`, or OWS wallet). |

#### Example output

```
[1/4] Requesting identity challenge...
[2/4] Signing identity challenge...
      Signed with key from ./controller.key
      Signer: 0x1111...1111
[3/4] Registering machine...
[4/4] Activating machine...

Machine onboarded successfully (active).

  Machine ID:  mach_abc123
  Identity:    did:peaq:0xAbCd...1234
  Status:      active
  Name:        My Edge Node

  Next: peaqos scale agent pair --machine-id mach_abc123
```

### `peaqos scale machine status`

Display details of a single machine.

```bash
peaqos scale machine status mach_abc123
peaqos scale machine status mach_abc123 --json
```

#### Example output

```
Machine mach_abc123

  Display name:  My Edge Node
  Status:        active
  Identity:      peaqos:machine:my-bot
  Owner:         operator-42
  Type:          edge-node
  Runtime:       linux-docker
  Capabilities:  price-feed, qvac
  Skills:        pyth-price-feed
  Created:       2026-05-16T12:00:00Z
```

### `peaqos scale machine list`

List all machines registered on the orchestration service.

```bash
peaqos scale machine list
peaqos scale machine list --json
```

#### Example output

```
Machines

  ID                     Name                      Status   Identity               Type
  ─────────────────────────────────────────────────────────────────────────────────────────
  mach_abc123            My Edge Node              active   peaqos:machine:my-bot  edge-node
  mach_def456            Test Machine              draft    did:peaq:0xAbCd...1e8f robot

2 machines found.
```

### `peaqos scale search`

Search for services matching a task in the Machine Market. The agent describes what it needs (service type, capabilities, budget) and gets back a ranked list of matching quotes.

Requires an active machine and a valid agent pairing token (`--pairing-token-file`).

```bash
peaqos scale search \
  --machine-id mach_1 \
  --service-type oracle.price-feed \
  --pairing-token-file ./token.txt

peaqos scale search \
  --machine-id mach_1 \
  --service-type oracle.price-feed \
  --pairing-token-file ./token.txt \
  --json   # raw JSON to stdout
```

#### Flags

| Flag | Required | Description |
| `--machine-id` | Yes | Machine performing the search. |
| `--service-type` | Yes | Service type (e.g. `oracle.price-feed`). |
| `--pairing-token-file` | Yes | Path to file containing the agent pairing token. |
| `--agent-pairing-id` | No | Agent pairing ID. |
| `--operation` | No | Desired operation (e.g. `get-latest-price`). |
| `--capabilities` | No | Required capabilities (comma-separated). |
| `--region` | No | Preferred region. |
| `--max-results` | No | Max quotes to return. |
| `--budget-amount` | No | Budget amount. |
| `--budget-max` | No | Maximum budget amount. |
| `--budget-currency` | No | Budget currency (e.g. `USD`). |
| `--native-only` | No | Require native execution (no external handoff). |
| `--allow-handoff` | No | Allow external handoff. |
| `--provider-credentials` | No | Path to JSON file with provider credentials. |
| `--json` | No | Output raw JSON to stdout. |

#### Example output

```
Searching for "oracle.price-feed" services...

Found 2 matching services.

#  Skill Key         Provider   Score  Execution    Integration
────────────────────────────────────────────────────────────────
1  pyth-price-feed   pyth       0.95   native       native
2  dia-price-feed    dia        0.72   ext-handoff  partner-required

Search ID: msearch_449650d9b2be

Use: peaqos scale order <service-id> to place an order (when available).
```

When no quotes are returned:

```
Searching for "compute.marketplace" services...

No matching services found.

Search ID: msearch_abc123

Try broadening your search: remove --native-only, increase --budget-amount, or try a different --service-type.
```

#### Environment

Orchestration-specific variables (in `.env` or environment):

| Variable | Purpose |
|----------|---------|
| `PEAQOS_ORCHESTRATION_URL` | Base URL of the Machine Markets API. |
| `PEAQOS_ORCH_API_KEY` | Platform API key (deployment-level auth). |
| `PEAQOS_ORDER_STEP_DELAY_SEC` | Optional seconds to pause between order placement steps. |

The orchestration URL and API key can also be passed as CLI flags (`--orchestration-url`, `--orch-api-key`).

### `peaqos scale order <service-uuid>`

Place a market order end-to-end: create the order, handle payment when required, then execute. The service UUID is the first argument (from a prior `peaqos scale search` quote). Registered subcommands (`status`, `list`, `received`, `dispute`) take precedence over service IDs.

Requires an active machine, agent pairing ID, and pairing token (`--pairing-token-file`). Progress steps are written to `stderr`; the final summary or `--json` payload goes to `stdout`.

**Payment flows** (chosen from the service quote):

| Pattern | Steps | When |
| ------- | ----- | ---- |
| No payment | `[1/2]` create → execute | `paymentStatus` is `not-required` (e.g. native Pyth) |
| Wallet payment | `[1/5]` create → payment intent → send payment → proof → execute | EVM USDC, Solana USDT, etc. |
| Escrow | Same as wallet, but step 4 calls escrow lock instead of payment proof | External handoff with on-chain escrow |
| x402 | `[1/6]` create → payment intent → sign → proof → execute → confirm | Agentic Market providers (paid HTTP, e.g. Wolfram Alpha over USDC) |

For the **x402** rail the CLI signs the provider's payment challenge locally with the active wallet (`client.account` — the OWS wallet when `PEAQOS_OWS_WALLET` is set, otherwise the local key) and hands the signed `PAYMENT-SIGNATURE` header to peaqOS, which pays the provider during execute. There is **no separate on-chain transfer** and no "paste the transaction hash" prompt. After order creation, you confirm once before payment intent, signing, proof submission, and execution; delivery is then confirmed automatically (step 6). If execution fails after proof is recorded, the error reports the current payment status so you can verify whether the authorization is held.

**Payment transfer modes** (step 3 when wallet payment is required; the x402 rail signs instead of transferring):

| Mode | How |
| ---- | --- |
| **OWS** | `PEAQOS_OWS_WALLET` set — CLI signs and sends the ERC-20 transfer via the OWS vault; tx hash captured automatically. Passphrase from `OWS_PASSPHRASE` or an interactive prompt. |
| **Manual** | No OWS wallet — CLI prints amount, chain, token, and payee; operator completes the transfer and pastes the tx hash at the `>` prompt. |
| **Pre-supplied** | `--payment-tx-hash` (+ `--payment-chain`, `--payment-token`) — skips step 3; proof submitted with the given hash/signature. |

Use `--skip-payment` only when payment was already completed externally; combine with `--payment-tx-hash` to record proof without re-sending funds.

#### Flags

| Flag / argument | Required | Description |
| --------------- | -------- | ----------- |
| `<service-uuid>` | Yes | Service ID from search results. |
| `--machine-id` | Yes | Machine placing the order. |
| `--agent-pairing-id` | Yes | Agent pairing ID (from `peaqos scale agent pair`). |
| `--pairing-token-file` | Yes | Path to agent pairing token file. |
| `--search-id` | No | Search ID from a prior `peaqos scale search`. |
| `--quote-id` | No | Quote ID from search results. |
| `--operation` | No | Requested operation (e.g. `get-latest-price`). |
| `--budget-amount` | No | Budget amount. |
| `--budget-currency` | No | Budget currency (e.g. `USD`). |
| `--input` | No | JSON file with operation input. |
| `--provider-credentials` | No | JSON file with provider credentials (never logged). |
| `--payment-tx-hash` | No | Pre-completed payment tx hash (EVM) or signature (Solana). |
| `--payment-chain` | No | Chain for proof (e.g. `base`, `solana`, `peaq`). Required with `--payment-tx-hash`. |
| `--payment-token` | No | Token for proof (e.g. `USDC`, `USDT`). Required with `--payment-tx-hash`. |
| `--skip-payment` | No | Skip the on-chain payment step (use with `--payment-tx-hash` when proof is supplied separately). |
| `-y`, `--yes` | No | Skip confirmation prompts. |
| `--json` | No | Output JSON to stdout (implies `--yes`). Contains `order`, `execution`, and `payment` when a payment record exists. |

Set `PEAQOS_ORDER_STEP_DELAY_SEC` to a non-negative number of seconds to pause between placement steps for demos or eventually consistent order state. When the variable is unset, no delay is applied.

#### Examples

**No-payment service** (native execution, two steps):

```bash
peaqos scale order svc_pyth_btc_usd \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --search-id msearch_449650d9b2be \
  --quote-id quote_abc123 \
  --operation get-latest-price \
  --yes
```

```
[1/2] Creating order...
[2/2] Executing order...

Order executed.

  Order ID:      ord_e89c70259bdb
  Service:       BTC/USD (pyth-price-feed)
  Status:        delivered
  Payment:       not_required

  Confirm delivery: peaqos scale order received ord_e89c70259bdb
```

**Manual wallet payment** (operator sends USDC, pastes hash):

```bash
peaqos scale order svc_dia_eth_usd \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --yes
# Step 3 prompts: paste 0x... tx hash after transferring off-wallet
```

**OWS auto-payment** (`PEAQOS_OWS_WALLET` in `.env`):

```bash
export PEAQOS_OWS_WALLET=agent
export OWS_PASSPHRASE='...'   # optional; otherwise prompted once
peaqos scale order svc_dia_eth_usd \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --yes
```

```
[3/5] Sending payment...
      Signing transfer via OWS (wallet=agent, chain=base)
      0x3333...3333 → 0x4444...4444  1.00 USDC
      Tx: 0x602d...5584
```

**Pre-supplied payment** (skip transfer step):

```bash
peaqos scale order svc_dia_eth_usd \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --payment-tx-hash 0x602d5584... \
  --payment-chain base \
  --payment-token USDC \
  --yes
```

**x402 paid provider** (local signing, no on-chain transfer):

```bash
peaqos scale order "agentic-market:...:query:92c9d0a2" \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --input query.json
```

```
[1/6] Creating order...

Order summary:

  Order ID:  ord_3ba63e8494a8
  Service:   wolfram-alpha
  Status:    created
  Payment:   payment_pending
Proceed with payment and order execution? [y/N]: y
[2/6] Creating payment intent...
Payment required (x402).

  Amount:    $0.02
  Chain:     eip155:8453
  Token:     USDC (0x8335...2913)
  Pay from:  0x9fbd...4913
  Pay to:    0x6302...Ad57

  The agent wallet will sign the x402 authorization next.
[3/6] Signing x402 payment...
[4/6] Recording payment proof...
[5/6] Executing order...
[6/6] Confirming delivery...

Order executed.

  Order ID:      ord_3ba63e8494a8
  Service:       Wolfram Alpha
  Status:        delivered
  Payment:       paid (x402)

  Result: x^3/3 + C
```

**External handoff** (escrow + handoff URL):

```
Order placed (external handoff).

  Order ID:      ord_63ab7e934300
  Service:       Aethir GPU Cloud
  Status:        handoff
  Handoff:       Open Aethir Cloud → https://app.aethir.com/

  After using the external service:
    peaqos scale order received ord_63ab7e934300
```

**JSON output** (scripting):

```bash
peaqos scale order svc_pyth_btc_usd \
  --machine-id mach_1 \
  --agent-pairing-id pair_67c50f8fdd3c \
  --pairing-token-file ./token.txt \
  --yes --json
```

```json
{
  "order": { "id": "ord_e89c70259bdb", "status": "delivered", "...": "..." },
  "execution": {
    "status_code": 200,
    "run": { "...": "..." },
    "outcome": { "...": "..." }
  }
}
```

When the order requires payment, the response also includes `"payment": { "status": "held", ... }`.

On partial failure after create, the error includes the order ID, current payment status, and a hint to run `peaqos scale order status <id>`.

### `peaqos scale order status <order-id>`

Check order and payment state.

```bash
peaqos scale order status ord_e89c70259bdb
peaqos scale order status ord_e89c70259bdb --json
```

#### Example output

```
Order ord_e89c70259bdb

  Service:        BTC/USD (pyth-price-feed)
  Status:         delivered
  Execution:      native
  Created:        2026-05-16T12:00:00Z

  Payment:
    Status:       not_required
    Rail:         —
    Amount:       —

  Next: peaqos scale order received ord_e89c70259bdb
```

`--json` returns `{ "order": { ... }, "payment": { ... } }`.

### `peaqos scale order list`

List market orders for a machine (`GET /market/orders`). Supports page-by-page listing with `--limit` and `--cursor`, or fetching every order in one shot when using `--json` without `--limit`.

**Flags:**

| Flag | Description |
|------|-------------|
| `--machine-id` | **Required.** Machine whose orders to list. |
| `--limit` | Page size (1–500). Omitted → server default (100). |
| `--cursor` | Opaque cursor from a previous response (next page). |
| `--json` | Machine-readable output (see below). |

**Human output** (default) returns one page. When the API includes a `next_cursor`, a copy-paste hint is printed after the table. Cursors are shown verbatim in the hint and are never written to logs (even with `--verbose`).

```bash
peaqos scale order list --machine-id mach_prod_smoke_mcr_1
peaqos scale order list --machine-id mach_prod_smoke_mcr_1 --limit 10
peaqos scale order list --machine-id mach_prod_smoke_mcr_1 \
  --limit 10 --cursor eyJ2IjoxLCJvZmZzZXQiOjJ9
```

#### Example output (human)

```
Orders for mach_prod_smoke_mcr_1

  ID                     Service                   Status      Execution   Created
  ─────────────────────────────────────────────────────────────────────────────────
  ord_e89c70259bdb       BTC/USD (pyth-price-feed) confirmed   native      2026-05-16T12:25:00Z
  ord_63ab7e934300       Aethir GPU Cloud          disputed    ext-handoff 2026-05-16T13:00:00Z

2 orders shown.

Next page: peaqos scale order list --machine-id mach_prod_smoke_mcr_1 --cursor eyJ2IjoxLCJvZmZzZXQiOjJ9
```

When there is no next page, only the count line is shown (no `Next page:` line).

**JSON output:**

| Invocation | Shape |
|------------|--------|
| `--json` (no `--limit`) | Root **array** of all orders across pages (auto-paginated). |
| `--json --limit N` | Single page: `{ "items": [...], "next_cursor": "..." \| null }`. |

```bash
# All orders (pipe-friendly for jq)
peaqos scale order list --machine-id mach_1 --json | jq '.[] | .id'

# Manual paging
peaqos scale order list --machine-id mach_1 --json --limit 5
```

```json
[
  { "id": "ord_e89c70259bdb", "status": "delivered", "...": "..." },
  { "id": "ord_63ab7e934300", "status": "handoff", "...": "..." }
]
```

```json
{
  "items": [{ "id": "ord_e89c70259bdb", "...": "..." }],
  "next_cursor": "eyJ2IjoxLCJvZmZzZXQiOjJ9"
}
```

Invalid `--limit` (0, negative, or above 500) exits with code `1` and: `Limit must be between 1 and 500.`

### `peaqos scale order received <order-id>`

Confirm delivery and release escrowed funds. The order must be in `delivered` status (native execution or after external handoff).

```bash
peaqos scale order received ord_e89c70259bdb \
  --pairing-token-file ./token.txt

peaqos scale order received ord_e89c70259bdb \
  --pairing-token-file ./token.txt \
  --json
```

#### Example output

```
Delivery confirmed.

  Order ID:  ord_e89c70259bdb
  Status:    confirmed
  Payment:   release_pending
```

### `peaqos scale order dispute <order-id>`

Raise a dispute and freeze escrowed payment. Requires `--reason` and the pairing token. Prompts for confirmation unless `--yes` or `--json`.

```bash
peaqos scale order dispute ord_63ab7e934300 \
  --reason "Service unavailable after payment" \
  --pairing-token-file ./token.txt \
  --yes

# Optional evidence JSON file
peaqos scale order dispute ord_63ab7e934300 \
  --reason "Incorrect deliverable" \
  --evidence ./evidence.json \
  --pairing-token-file ./token.txt \
  --json
```

#### Example output

```
Dispute raised.

  Order ID:  ord_63ab7e934300
  Status:    disputed
  Payment:   frozen
  Reason:    Service unavailable after payment
```

#### Integration tests

Order lifecycle integration tests live at `tests/integration/commands/scale/test_order_integration.py`. Enable with `PEAQOS_CLI_ORCH_INTEGRATION=1` and the `PEAQOS_INTEGRATION_*` variables documented in that file.

### `peaqos stream grant`

Grant a buyer decryption access to a published chunk chain. Reads chunk envelope files from a publish output directory, unwraps the owner's per-chunk keys, re-wraps them for the buyer, and writes `peaq.stream.buyer-access.v1` files. Chunk envelope files are never modified — only new buyer access files are produced.

**Prerequisites:**

* A publish output directory containing `chunk-*.json` envelope files (from `peaqos stream publish`). The encrypted `chunk-*.bin` blobs and `manifest.json` are not read by this command.
* Owner X25519 private key file (the key used when the chain was published).
* Buyer X25519 public key (hex) and buyer recipient ID (e.g. `did:peaq:<buyer-address>`).

Progress and the human summary are written to `stderr`; `--json` writes only the summary object to `stdout`.

**Key file format:** `--owner-private-key-file` expects an X25519 private key as a single line of hex — exactly 64 hex characters, with an optional `0x` prefix. This is **not** the secp256k1 `0x`-prefixed 64-hex format used by `read_key_file` in Utilities below.

#### Flags

| Flag | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--chunk-dir` | Yes | — | Directory containing published chunk envelope files (`chunk-*.json`). |
| `--buyer-public-key` | Yes | — | Buyer's X25519 public key (hex). |
| `--buyer-id` | Yes | — | Buyer recipient ID (e.g. `did:peaq:<buyer-address>`). |
| `--owner-private-key-file` | Yes | — | Path to file containing the owner's X25519 private key (hex). |
| `--output-dir` | Yes | — | Directory to write buyer access files (created if missing). |
| `--max-file-size` | No | `512000` | Maximum buyer access file size in bytes (500 KB). |
| `--json` | No | — | Output summary JSON to stdout (no other stdout output). |

#### Output files

Buyer access files use schema `peaq.stream.buyer-access.v1`. Files are named `<buyerPublicKeyHex>-<n>.json` with a 1-based index (e.g. `a1b2c3d4...-1.json`, `a1b2c3d4...-2.json`). Entries are batched up to `--max-file-size` (default 500 KB). A single oversized entry gets its own file; entries are never split across files.

#### Examples

```bash
peaqos stream grant \
  --chunk-dir ./publish-output \
  --buyer-public-key a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --owner-private-key-file ./owner-x25519.key \
  --output-dir ./access
```

Progress and summary on `stderr`:

```
Granting access to 4 chunks for buyer did:peaq:0xBuyer00000000000000000000000000000001...

Wrote 1 buyer access file to ./access

  Schema:       peaq.stream.buyer-access.v1
  Buyer ID:     did:peaq:0xBuyer00000000000000000000000000000001
  Chunks:       4
  Files:        1

    a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456-1.json  (12.3 KB)
```

```bash
# Smaller batch size → more access files
peaqos stream grant \
  --chunk-dir ./publish-output \
  --buyer-public-key a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --owner-private-key-file ./owner-x25519.key \
  --output-dir ./access \
  --max-file-size 1024

# JSON summary to stdout (stderr empty)
peaqos stream grant \
  --chunk-dir ./publish-output \
  --buyer-public-key a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --owner-private-key-file ./owner-x25519.key \
  --output-dir ./access \
  --json
```

```json
{
  "schemaVersion": "peaq.stream.buyer-access.v1",
  "buyerId": "did:peaq:0xBuyer00000000000000000000000000000001",
  "chunkCount": 4,
  "fileCount": 1,
  "outputDir": "./access",
  "files": [
    {
      "name": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456-1.json",
      "sizeBytes": 12634
    }
  ]
}
```

#### Common errors

| Condition | Exit code | Message |
| --------- | --------- | ------- |
| Wrong owner private key (`key_commitment` mismatch) | `2` | `Key commitment verification failed — wrong owner key?` |
| No `chunk-*.json` files in `--chunk-dir` | `1` | `No chunk files found in {path}` |
| Gap or duplicate chunk indices | `1` | `Missing chunk at index {n}` / `Duplicate chunk at index {n}` |
| Invalid buyer public key hex | `1` | Validation error before SDK call |
| Empty `--buyer-id` | `1` | `--buyer-id must not be empty.` |
| Output directory not writable | `1` | `Cannot write to: {path}` |

### `peaqos stream consume`

Decrypt a purchased chunk chain and reassemble the original data. Verifies the chain integrity, decrypts each chunk using the buyer's wrapped key, and writes the reassembled original data to an output file.

Supports two input modes:

* **Local directories** — provide `--chunk-dir`, `--access-dir`, and `--data-dir` pointing to directories already on disk.
* **Remote download** — provide `--download-url` pointing to an HTTP/HTTPS release package. The package is downloaded to a work directory first, then the existing verify → decrypt → reassemble pipeline runs on the downloaded files. The work directory is deleted after successful reassembly unless `--keep-files` is set.

**Prerequisites:**

* Buyer X25519 private key file (the key whose public key was used in grant).
* For **local mode**: chunk directory (`chunk-*.json` envelopes, optionally `manifest.json` from `peaqos stream publish`), encrypted data directory (`chunk-*.bin` blobs), and buyer access directory (`*.json` files from `peaqos stream grant`).
* For **remote mode**: an HTTP/HTTPS URL to a **self-contained release package** that bundles everything the pipeline needs — chunk envelopes (`chunk-*.json`), encrypted data blobs (`chunk-*.bin`), and buyer access files (`*.json`) — exposed either as a `manifest.json` listing those files or as a ZIP archive containing them. This is a self-hosted bundle, distinct from the SDK `S3DeliveryChannel` pre-signed URL, which delivers only the buyer access files (the ciphertext stays referenced by each chunk's `storage_ref`). Manifest file names are restricted to flat `*.json` / `*.bin` names. A query token on the URL (for a self-hosted endpoint that authorises that way) is preserved when fetching each file — but note an S3 **pre-signed single-object** URL cannot address the manifest's sub-paths (SigV4 signs the object path, so a changed path returns 403). A pre-signed URL pointing at a single **ZIP** object still works: the manifest probe returns 403/404 and the command falls back to fetching the signed object as a ZIP.

> **Roundtrip note (acceptance-criterion deviation).** `--download-url` does **not** consume the URL produced by `peaqos stream distribute` / `S3DeliveryChannel` directly. That pre-signed URL delivers only the first buyer-access file; the chunk envelopes are not delivered and the ciphertext stays referenced by each chunk's `storage_ref`, and the SDK ships no buyer-side S3 receiver. A complete distribute → consume roundtrip therefore requires SDK/orchestration support (an S3 delivery receiver plus a descriptor enumerating every access file, chunk-envelope location, and a `storage_ref` resolver) that does not exist yet. Until then, point `--download-url` at a self-contained bundle as described above.

Progress and the human summary are written to `stderr`; `--json` writes only the summary object to `stdout`.

#### Flags

| Flag | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--chunk-dir` | Yes (local mode) | — | Directory containing chunk envelope files (`chunk-*.json`). Not used with `--download-url`. |
| `--access-dir` | Yes (local mode) | — | Directory containing buyer access files (`*.json`). Not used with `--download-url`. |
| `--data-dir` | Yes (local mode) | — | Directory containing encrypted data blobs (`chunk-*.bin`). Not used with `--download-url`. |
| `--buyer-private-key-file` | Yes | — | Path to file containing buyer's X25519 private key (hex). |
| `--buyer-id` | Yes | — | Buyer recipient ID matching the access files. |
| `--output` | Yes | — | Output file path for the reassembled data. |
| `--skip-verify` | No | `False` | Skip chain verification (not recommended, debugging only). |
| `--json` | No | `False` | Output summary JSON to stdout (no other stdout output). |
| `--download-url` | No | — | HTTP/HTTPS URL to a release package. Mutually exclusive with `--chunk-dir`, `--access-dir`, `--data-dir`. |
| `--work-dir` | No | temp dir | Working directory for downloaded files. Cleaned up after success unless `--keep-files`. Ignored when `--download-url` is absent. |
| `--keep-files` | No | `False` | Keep the work directory after successful reassembly. Ignored when `--download-url` is absent. |

#### Examples

```bash
# Local mode — directories already on disk
peaqos stream consume \
  --chunk-dir ./chunks \
  --access-dir ./access \
  --data-dir ./chunks \
  --buyer-private-key-file ./buyer-x25519.key \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --output ./recovered-data.bin

# Remote download — fetch a self-contained release bundle
peaqos stream consume \
  --download-url "https://bundles.example.com/releases/ord-001/" \
  --buyer-private-key-file ./buyer-x25519.key \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --output ./recovered-data.bin

# Remote download — keep the downloaded files for inspection
peaqos stream consume \
  --download-url "https://bundles.example.com/releases/ord-001/" \
  --work-dir ./downloaded \
  --keep-files \
  --buyer-private-key-file ./buyer-x25519.key \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --output ./recovered-data.bin

# Skip verification (debugging only)
peaqos stream consume \
  --chunk-dir ./chunks \
  --access-dir ./access \
  --data-dir ./chunks \
  --buyer-private-key-file ./buyer-x25519.key \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --output ./recovered-data.bin \
  --skip-verify

# JSON summary to stdout
peaqos stream consume \
  --chunk-dir ./chunks \
  --access-dir ./access \
  --data-dir ./chunks \
  --buyer-private-key-file ./buyer-x25519.key \
  --buyer-id did:peaq:0xBuyer00000000000000000000000000000001 \
  --output ./recovered-data.bin \
  --json
```

Human output on `stderr`:

```
Downloading release package from https://bundles.example.com/releases/ord-001/...
Downloading chunk-0.json...
Downloading chunk-0.bin...
Downloading chunk-1.json...
Downloading chunk-1.bin...
Downloading buyer-access-abc123.json...
Verifying chain... ✓ 4 chunks valid.
Decrypting chunk 0/4...
Decrypting chunk 1/4...
Decrypting chunk 2/4...
Decrypting chunk 3/4...

Reassembled 1048576 bytes to ./recovered-data.bin

  Source Hash:  0xabc123...
  Chunks:       4
  Buyer ID:     did:peaq:0xBuyer00000000000000000000000000000001
```

JSON output:

```json
{
  "output": "./recovered-data.bin",
  "totalBytes": 1048576,
  "chunkCount": 4,
  "sourceHash": "0xabc123...",
  "buyerId": "did:peaq:0xBuyer00000000000000000000000000000001",
  "verified": true
}
```

#### Download behaviour

When `--download-url` is provided, the command:

1. Creates the work directory (`--work-dir` if given, otherwise a temp dir).
2. Downloads land in a command-owned, uniquely named `peaqos-package-*` subdirectory of the work directory, so a pre-existing `--work-dir`'s files (including any prior `peaqos-package-*` kept via `--keep-files`) are never touched.
3. Fetches `manifest.json` from the URL path (a query token is preserved). If the manifest contains a `"files"` list, each file is downloaded individually with per-file progress on `stderr` (suppressed under `--quiet`). The manifest itself is persisted so its `sourceHash` survives.
4. If `manifest.json` returns 404 (absent) or 403 (e.g. a pre-signed single object), or has no `"files"` key, the base URL is downloaded as a ZIP archive and extracted.
5. Downloaded files are sorted (recursively, so a ZIP wrapping the package in a top-level directory works) into `chunks/`, `data/`, and `access/` subdirectories; `manifest.json` is co-located with the chunks so its `sourceHash` is recovered before the pipeline runs.
6. On success, cleanup removes only what the command created: a work directory it created is removed entirely; otherwise only the `peaqos-package-*` staging subdirectory is removed, and an `--output` inside it is preserved. `--keep-files` skips cleanup; a cleanup failure is reported as a warning.
7. On any error, the work directory is preserved for debugging.

#### Common errors

| Condition | Exit code | Message |
| --------- | --------- | ------- |
| Chain verification failed | `2` | `Chain verification failed at chunk {n}: {reason}` |
| Missing encrypted data for a chunk | `2` | `Missing encrypted data for chunk {n}` |
| No buyer access for a chunk | `2` | `No buyer access for chunk {n} ({chunk_id})` |
| Wrong buyer private key | `2` | `Decryption failed for chunk {n} — access not granted for this buyer private key` |
| `key_commitment` mismatch | `2` | `Key commitment verification failed for chunk {n}` |
| `plaintext_hash` mismatch | `2` | `Data integrity check failed for chunk {n} — plaintext hash mismatch` |
| Download HTTP error | `2` | `Download failed: HTTP {code} from {url}` |
| Download timeout | `2` | `Download failed: connection timed out after 60s` |
| Downloaded archive not a valid ZIP | `2` | `Downloaded archive is not a valid ZIP file: {detail}` |
| No chunk files in dir | `1` | `No chunk files found in {path}` |
| No encrypted data files | `1` | `No encrypted data files found in {path}` |
| No buyer access files | `1` | `No buyer access files found in {path}` |
| `--download-url` with a dir flag | `1` | `--download-url is mutually exclusive with --chunk-dir, --access-dir, and --data-dir.` |
| Invalid `--download-url` scheme | `1` | `--download-url must start with http:// or https://.` |
| Missing `--chunk-dir` (local mode) | `1` | `--chunk-dir is required when --download-url is not provided.` |
| Empty `--buyer-id` | `1` | `--buyer-id must not be empty.` |
| Output path not writable | `1` | `Cannot write to: {path}` |

### `peaqos stream pay`

Transfer tokens on-chain to a seller and optionally submit the transaction hash as payment proof. Supports native token and ERC-20/SPL contract transfers on peaq, Base, and Solana.

When `--confirmation-url` is provided, proof is submitted immediately after the transfer. If omitted, only the transfer executes and a `peaqos stream payproof` hint is printed so proof can be submitted separately.

Progress lines are written to `stderr`; the transfer summary and `--json` payload go to `stdout`. The transaction hash is always written to `stdout` before the proof step, so it is captured even if proof submission fails.

#### Flags

| Flag | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--seller-address` | Yes | — | Recipient address (EVM `0x...` or Solana base58). |
| `--amount` | Yes | — | Human-readable transfer amount (e.g. `"10.5"`). |
| `--chain` | Yes | — | Target chain: `peaq`, `base`, or `solana`. |
| `--order-id` | Yes | — | Order ID for the purchase. |
| `--confirmation-url` | No | — | URL to submit payment proof after transfer. Omit to transfer only. |
| `--token-address` | No | — | ERC-20 contract (EVM) or SPL mint address (Solana). Omit for native token. |
| `--token-decimals` | No | — | Token decimal count override (for tokens not in the well-known registry). |
| `--rpc-url` | No | — | RPC endpoint. Required for `--chain base` and `--chain solana`. |
| `--private-key-file` | No | from config | Path to buyer private key file (0x-prefixed hex). Falls back to `PEAQOS_PRIVATE_KEY`. |
| `--json` | No | `False` | Output result as JSON to stdout. |

#### Examples

```bash
# Native PEAQ transfer (no proof)
peaqos stream pay \
  --seller-address 0xSeller...1234 \
  --amount 10.5 \
  --chain peaq \
  --order-id order-001

# Base EVM transfer with proof submission
peaqos stream pay \
  --seller-address 0xSeller...1234 \
  --amount 1.0 \
  --chain base \
  --order-id order-002 \
  --rpc-url https://mainnet.base.org \
  --token-address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  --confirmation-url https://api.example.com/payments/proof

# Solana SPL transfer
peaqos stream pay \
  --seller-address So111...base58 \
  --amount 5.0 \
  --chain solana \
  --order-id order-003 \
  --rpc-url https://api.mainnet-beta.solana.com \
  --token-address EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

# JSON output with proof
peaqos stream pay \
  --seller-address 0xSeller...1234 \
  --amount 10.5 \
  --chain peaq \
  --order-id order-001 \
  --confirmation-url https://api.example.com/payments/proof \
  --json
```

**Human output (with `--confirmation-url`):**

```
Transferring 10.5 on peaq to 0xSeller...1234...

  Tx Hash:     0xabc123...
  Chain:       peaq
  Status:      submitted

  Proof:       accepted
  Order ID:    order-001
  Submitted:   2026-06-25T12:00:05Z
```

**Human output (without `--confirmation-url`):**

```
Transferring 10.5 on peaq to 0xSeller...1234...

  Tx Hash:     0xabc123...
  Chain:       peaq
  Status:      submitted

  Submit proof manually: peaqos stream payproof --tx-hash 0xabc123... --order-id order-001 --confirmation-url <url>
```

**JSON output:**

```json
{
  "txHash": "0xabc123...",
  "chain": "peaq",
  "status": "submitted",
  "payerAddress": "0xPayer...5678",
  "payeeAddress": "0xSeller...1234",
  "amount": "10.5",
  "token": "PEAQ",
  "tokenAddress": null,
  "proof": {
    "accepted": true,
    "orderId": "order-001",
    "submittedAt": "2026-06-25T12:00:05Z"
  }
}
```

`"proof"` is `null` when `--confirmation-url` is omitted.

#### Error codes

| Condition | Exit | Message |
| --------- | ---- | ------- |
| Invalid or empty `--amount` | `1` | Validation message |
| Empty `--seller-address` or `--order-id` | `1` | Validation message |
| Missing Solana dependency | `1` | Install instructions (`pip install peaq-os-sdk[solana]`) |
| Signing failure | `1` | Sanitized message (no key material) |
| Insufficient balance | `2` | `Insufficient balance for transfer` |
| Transaction reverted | `2` | `Transaction reverted: {reason}` |
| Proof HTTP failure | `2` | `Payment proof submission failed: {reason}` |
| Missing env vars / config | `3` | Actionable config message |

### `peaqos stream payproof`

Submit a payment proof for a completed on-chain transfer. Use this when the transfer was done outside of `peaqos stream pay`, or when the proof step from that command failed and needs to be retried.

Progress is written to `stderr`; the proof result and `--json` payload go to `stdout`.

#### Flags

| Flag | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--tx-hash` | Yes | — | EVM transaction hash (`0x...`) or Solana transaction signature. |
| `--order-id` | Yes | — | Order ID associated with the payment. |
| `--confirmation-url` | Yes | — | URL to submit the payment proof to. |
| `--chain` | Yes | — | Chain the transfer was on: `peaq`, `base`, or `solana`. |
| `--payer-address` | Yes | — | Buyer wallet address that signed the transfer. |
| `--payee-address` | Yes | — | Seller wallet address that received the transfer. |
| `--amount` | Yes | — | Human-readable amount transferred (must match the original transfer). |
| `--token` | No | — | Token symbol metadata (e.g. `USDC`). |
| `--token-address` | No | — | Token contract or SPL mint address. Omit for native transfers. |
| `--json` | No | `False` | Output result as JSON to stdout. |

#### Examples

```bash
# Submit proof for a previously completed transfer
peaqos stream payproof \
  --tx-hash 0xabc123... \
  --order-id order-001 \
  --confirmation-url https://api.example.com/payments/proof \
  --chain peaq \
  --payer-address 0xPayer...5678 \
  --payee-address 0xSeller...1234 \
  --amount 10.5

# ERC-20 transfer proof (Base USDC)
peaqos stream payproof \
  --tx-hash 0xdef456... \
  --order-id order-002 \
  --confirmation-url https://api.example.com/payments/proof \
  --chain base \
  --payer-address 0xPayer...5678 \
  --payee-address 0xSeller...1234 \
  --amount 1.0 \
  --token USDC \
  --token-address 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913

# JSON output
peaqos stream payproof \
  --tx-hash 0xabc123... \
  --order-id order-001 \
  --confirmation-url https://api.example.com/payments/proof \
  --chain peaq \
  --payer-address 0xPayer...5678 \
  --payee-address 0xSeller...1234 \
  --amount 10.5 \
  --json
```

**Human output:**

```
Submitting payment proof...

  Proof:       accepted
  Order ID:    order-001
  Submitted:   2026-06-25T12:00:05Z
```

**JSON output:**

```json
{
  "accepted": true,
  "orderId": "order-001",
  "submittedAt": "2026-06-25T12:00:05Z"
}
```

#### Error codes

| Condition | Exit | Message |
| --------- | ---- | ------- |
| Empty `--tx-hash` or `--order-id` | `1` | Validation message |
| Proof HTTP failure | `2` | `Payment proof submission failed: {reason}` |
| Missing env vars / config | `3` | Actionable config message |

### `peaqos stream distribute`

Listen for buyer payment confirmation and deliver buyer access files. After publishing a chunk chain, the seller runs this command to wait for a buyer's payment to be confirmed, then automatically generates buyer access files and delivers them to S3.

The command polls `--confirmation-url` every `--poll-interval` seconds until the endpoint reports a confirmed payment or `--timeout` seconds elapse. On confirmation the SDK builds buyer access files (the same as `peaqos stream grant`), uploads them to the S3 path under `{prefix}{buyer_id}/`, and returns a pre-signed download URL.

**Prerequisites:**

* A published chunk directory containing `chunk-*.json` envelope files (from `peaqos stream publish`).
* Owner X25519 private key file (the same key used at publish time).
* A reachable HTTP endpoint at `--confirmation-url` that returns a JSON body containing at minimum `status`, `buyer_id`, and `buyer_public_key_hex`.
* AWS S3 credentials in `PEAQOS_S3_ACCESS_KEY_ID` / `PEAQOS_S3_SECRET_ACCESS_KEY` (or the standard `boto3` credential chain) when `--delivery s3`.
* `boto3` installed (`pip install peaq-os-cli[s3]`).

Progress lines are written to `stderr`; the human summary is written to `stdout`. `--json` writes only the result object to `stdout`.

**Key file format:** `--owner-private-key-file` expects an X25519 private key as a single line of hex — exactly 64 hex characters, with an optional `0x` prefix. This is the same format accepted by `peaqos stream grant`.

#### Flags

| Flag | Required | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--chunk-dir` | Yes | — | Directory containing published chunk envelope files (`chunk-*.json`). |
| `--owner-private-key-file` | Yes | — | Path to file containing the owner's X25519 private key (hex). |
| `--confirmation-url` | Yes | — | HTTP endpoint to poll for payment confirmation. |
| `--order-id` | Yes | — | Purchase order identifier (`purchaseId` in the orchestration API). |
| `--poll-interval` | No | `30` | Seconds between confirmation polls. |
| `--timeout` | No | `3600` | Max seconds to wait for confirmation before giving up. |
| `--delivery` | Yes | — | Delivery channel. Currently only `s3`. |
| `--s3` | Conditional | — | S3 bucket path (e.g. `s3://my-bucket/prefix/`). Required when `--delivery s3`. |
| `--s3-region` | No | from env | S3 region (overrides `PEAQOS_S3_REGION`). |
| `--s3-endpoint` | No | from env | Custom S3-compatible endpoint URL (MinIO, R2, etc.). |
| `--presign-expiry` | No | `3600` | Pre-signed download URL expiry in seconds. |
| `--max-file-size` | No | `512000` | Maximum buyer access file size in bytes (500 KB). |
| `--json` | No | — | Output result as JSON to stdout (no other stdout output). |

#### Examples

```bash
# Basic — poll every 30 seconds, 1-hour timeout
peaqos stream distribute \
  --chunk-dir ./publish-output \
  --owner-private-key-file ./owner-x25519.key \
  --confirmation-url https://api.example.com/orders/ord-001/status \
  --order-id ord-001 \
  --delivery s3 \
  --s3 s3://my-bucket/distributes/

# Faster polling with a shorter timeout
peaqos stream distribute \
  --chunk-dir ./publish-output \
  --owner-private-key-file ./owner-x25519.key \
  --confirmation-url https://api.example.com/orders/ord-001/status \
  --order-id ord-001 \
  --delivery s3 \
  --s3 s3://my-bucket/distributes/ \
  --poll-interval 10 \
  --timeout 300

# Longer-lived download links (24 hours)
peaqos stream distribute \
  --chunk-dir ./publish-output \
  --owner-private-key-file ./owner-x25519.key \
  --confirmation-url https://api.example.com/orders/ord-001/status \
  --order-id ord-001 \
  --delivery s3 \
  --s3 s3://my-bucket/distributes/ \
  --presign-expiry 86400

# JSON output to stdout
peaqos stream distribute \
  --chunk-dir ./publish-output \
  --owner-private-key-file ./owner-x25519.key \
  --confirmation-url https://api.example.com/orders/ord-001/status \
  --order-id ord-001 \
  --delivery s3 \
  --s3 s3://my-bucket/distributes/ \
  --json
```

Progress on `stderr`, summary on `stdout`:

```
Waiting for payment confirmation (polling every 30s)...
Payment confirmed for buyer did:peaq:0xBuyer00000000000000000000000000000001 (tx: 0xabc...)

Distributing 4 chunks to s3://my-bucket/distributes/

  Buyer ID:      did:peaq:0xBuyer00000000000000000000000000000001
  Access Files:  1
  Download URL:  https://my-bucket.s3.amazonaws.com/distributes/did:peaq:0xBuyer.../access-1.json?X-Amz-Signature=...
  Expires:       3600s
```

JSON output:

```json
{
  "order_id": "ord-001",
  "buyer_id": "did:peaq:0xBuyer00000000000000000000000000000001",
  "tx_hash": "0xabc...",
  "chunks_distributed": 4,
  "access_files_count": 1,
  "delivery": {
    "channel": "s3",
    "download_url": "https://my-bucket.s3.amazonaws.com/distributes/...?X-Amz-Signature=...",
    "delivered_at": "2026-06-25T12:00:00Z",
    "presign_expiry": 3600
  }
}
```

#### Common errors

| Condition | Exit code | Message |
| --------- | --------- | ------- |
| Payment confirmation timed out | `2` | `Payment confirmation timed out after {N}s for order {order_id}` |
| S3 upload failed | `2` | `S3 upload failed: {reason}` |
| `--delivery s3` without `--s3` | `1` | `--s3 is required when --delivery is s3` |
| `--poll-interval` not positive | `1` | `--poll-interval must be a positive integer.` |
| `--timeout` not positive | `1` | `--timeout must be a positive integer.` |
| Non-confirmed payment status from SDK | `1` | `payment.status — ... (constraint: confirmed)` |
| No `chunk-*.json` files in `--chunk-dir` | `1` | `No chunk files found in {path}` |
| Missing or unreadable owner key file | `1` | `Could not read key file: {path} (...)` |
| `boto3` not installed | `3` | `boto3 is required for S3 delivery but is not installed — install with: pip install boto3` |

## Errors

The CLI maps SDK and network exceptions to stable exit codes:

| Exit code | Meaning                                                      |
| --------- | ------------------------------------------------------------ |
| `0`       | Success                                                      |
| `1`       | User / validation error (bad input, cap, rate limit)         |
| `2`       | Network, RPC, or on-chain error (connection, HTTP, revert)   |
| `3`       | Configuration error (missing env vars, invalid private key)  |

Subcommands funnel exceptions through `peaq_os_cli.errors.handle_sdk_error`, which raises `click.ClickException` with the mapped exit code and a user-friendly message. Known on-chain revert reasons and Faucet API error codes are translated by `map_revert_reason` and `map_faucet_error`.

## Output formatting

Subcommands render results through `peaq_os_cli.formatting` to keep human output consistent across the CLI:

* `truncate_address` — shortens a hex address to `0x{first4}...{last4}`.
* `format_timestamp` — renders a Unix timestamp as ISO 8601 UTC, or `—` for `None`.
* `format_key_value` — aligns `key: value` pairs so the colons line up.
* `format_table` — left-aligned columnar table with a header separator.
* `print_json` — writes `json.dumps(data, indent=2)` to stdout for pipe-friendly output.
* `print_step` — writes `[{step}/{total}] {label}` progress lines to stderr, suppressed when the active Click context is quiet.

## Utilities

Input parsing and validation helpers in `peaq_os_cli.utils`:

* `parse_timestamp` — accepts pure-digit Unix seconds or ISO 8601 strings with an explicit UTC offset (`...Z` or `...+HH:MM`). Raises `click.BadParameter` on unrecognised input.
* `validate_did_format` — requires `did:peaq:0x` followed by 40 hex characters.
* `validate_address_format` — requires a `0x`-prefixed 40-hex-character address.
* `read_key_file` — reads, strips, and validates a `0x`-prefixed 64-hex private key file. Raises `click.ClickException` with exit code `1` on missing files or invalid content.
