> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peaq.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Proxy operator fleet

> Register and manage multiple machines from one operator wallet using registerFor. Covers per-machine keys, batch registration, and fleet queries.

One owner, many machines. The proxy operator pattern lets a single wallet register and control a fleet of machines. Each machine gets its own peaqID on registration; the Machine NFT is minted in a follow-up `mintNft` call. The proxy operator holds the on-chain ownership of every identity.

## When to use proxy registration

Use `registerFor` when:

* You operate physical hardware that cannot hold its own keys (offline sensors, embedded devices)
* You manage a fleet and want centralized identity control
* You need batch onboarding of many machines from a single script. The IdentityRegistry contract puts no cap on machines per operator, but the operator's `machines` DID attribute is capped at 100 entries (newer registrations push older ones out of the index), and the [MCR API operator endpoint](/peaqos/api-reference/get-operator-machines) paginates with `limit ≤ 20` per request — iterate via `offset` against `pagination.total` to walk the full set, or enumerate against the contract directly for fleets that need more than the indexed 100.

Use [self-managed onboarding](/peaqos/guides/self-managed-onboarding) when the machine holds its own key and registers itself.

## Prerequisites

* Node.js ≥ 22 or Python 3.10+
* A funded proxy operator wallet with at least (N \* 1.1) PEAQ (1 PEAQ bond + gas per machine)
* The proxy operator must be self-registered first: `registerMachine` from the proxy's own key, exactly as in [self-managed onboarding](/peaqos/guides/self-managed-onboarding). The proxy's `machineId` is required to publish the fleet under its DID.
* Environment variables configured per the [install guide](/peaqos/install)
* 2FA set up and confirmed on the operator wallet (see [self-managed onboarding](/peaqos/guides/self-managed-onboarding) steps 3 and 4)

<Note>
  JS examples load `.env` via `import "dotenv/config"`. Python's `from_env()` reads from the shell, so export the file first with `set -a && source .env && set +a`.
</Note>

## Single machine registration

<Steps>
  <Step title="Create the proxy client">
    The proxy operator's private key signs all transactions on behalf of the fleet.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      import "dotenv/config";
      import { PeaqosClient } from "@peaqos/peaq-os-sdk";

      const proxy = PeaqosClient.fromEnv();
      console.log("Proxy operator:", proxy.address);
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      from dotenv import load_dotenv
      from peaq_os_sdk import PeaqosClient

      load_dotenv() # load envs from .env file

      proxy = PeaqosClient.from_env()
      print("Proxy operator:", proxy.address)
      ```
    </CodeGroup>
  </Step>

  <Step title="Generate a keypair for the machine">
    Each machine needs its own address. Generate one per device.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      const machine = PeaqosClient.generateKeypair();
      console.log("Machine address:", machine.address);
      // Store machine.privateKey securely if the device needs to sign later.
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      machine_address, machine_key = PeaqosClient.generate_keypair()
      print("Machine address:", machine_address)
      # Store machine_key securely if the device needs to sign later.
      ```
    </CodeGroup>
  </Step>

  <Step title="Register the machine via proxy">
    `registerFor` registers the machine address on the IdentityRegistry. The proxy's signing address (`msg.sender`) becomes the on-chain owner. The call sends 1 PEAQ as the bond.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      const machineId = await proxy.registerFor(machine.address);
      console.log("Registered machine", machine.address, "with machine ID", machineId);
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      machine_id = proxy.register_for(machine_address)
      print(f"Registered machine {machine_address} with machine ID {machine_id}")
      ```
    </CodeGroup>
  </Step>

  <Step title="Mint the Machine NFT (proxy)">
    Registration only mints the Identity NFT. The proxy mints the Machine NFT to the machine's address.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      await proxy.mintNft(machineId, machine.address);
      const nftTokenId = await proxy.tokenIdOf(machineId);
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      proxy.mint_nft(machine_id, machine_address)
      nft_token_id = proxy.token_id_of(machine_id)
      ```
    </CodeGroup>
  </Step>

  <Step title="Fund the machine wallet">
    The machine signs its own DID writes, so its wallet needs a small amount of PEAQ for gas. Use the [Gas Station](/peaqos/concepts/gas-station) from the proxy (2FA-gated), or top it up directly from any PEAQ-holding address.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      const FAUCET_URL = "https://depinstation.peaq.network";

      await proxy.fundFromGasStation(
        {
          ownerAddress: proxy.address,
          targetWalletAddress: machine.address,
          chainId: "peaq",
          twoFactorCode: "654321", // Fresh TOTP code from the proxy operator's authenticator
          // requestId is recommended for idempotent reruns
        },
        FAUCET_URL,
      );
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      FAUCET_URL = "https://depinstation.peaq.network"

      proxy.fund_from_gas_station(
          owner_address=proxy.address,
          target_wallet_address=machine_address,
          chain_id="peaq",
          two_factor_code="654321",  # Fresh TOTP code from the proxy operator's authenticator
          faucet_base_url=FAUCET_URL,
      )
      ```
    </CodeGroup>
  </Step>

  <Step title="Machine writes its own DID attributes">
    `writeMachineDIDAttributes` always writes to the **caller's** DID. The proxy can't write to the machine's DID for it. Spin up a separate client backed by the machine's key and have the machine sign its own attribute writes. Skip this step and the machine is unreachable through the MCR API: `GET /machine/{did}` and `GET /mcr/{did}` will 404 because no `machineId` attribute is bound to the machine's address.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      import { PeaqosClient } from "@peaqos/peaq-os-sdk";

      // Build a per-machine client by reusing the proxy's RPC + contracts
      // but signing with the machine's own key.
      const machineClient = new PeaqosClient({
        rpcUrl: proxy.rpcUrl,
        privateKey: machine.privateKey,
        contracts: proxy.contracts,
      });

      await machineClient.writeMachineDIDAttributes({
        machineId,
        nftTokenId,
        operatorDid: `did:peaq:${proxy.address}`,
        documentationUrl: "https://example.com/docs",
        dataApi: "https://example.com/events",
        dataVisibility: "public",
      });
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      # Construct a per-machine client directly: same RPC and contracts
      # as the proxy, but signed with the machine's own key. No env mutation.
      machine_client = PeaqosClient(
          rpc_url=proxy.rpc_url,
          private_key=machine_key,
          identity_registry=proxy.contracts.identity_registry,
          identity_staking=proxy.contracts.identity_staking,
          event_registry=proxy.contracts.event_registry,
          machine_nft=proxy.contracts.machine_nft,
          did_registry=proxy.contracts.did_registry,
          batch_precompile=proxy.contracts.batch_precompile,
      )

      machine_client.write_machine_did_attributes(
          machine_id=machine_id,
          nft_token_id=nft_token_id,
          operator_did=f"did:peaq:{proxy.address}",
          documentation_url="https://example.com/docs",
          data_api="https://example.com/events",
          data_visibility="public",
      )
      ```
    </CodeGroup>
  </Step>

  <Step title="Proxy publishes the fleet on its DID">
    `writeProxyDIDAttributes` writes the proxy's `machineId` and the list of machine IDs it controls onto the proxy's DID. This is what the [`GET /operator/{did}/machines`](/peaqos/api-reference/get-operator-machines) endpoint reads from.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      // proxyMachineId is the proxy operator's own machineId from its
      // self-registration (registerMachine). Persist it once and reuse.
      const proxyMachineId = /* number, e.g. 7 */ 7;

      await proxy.writeProxyDIDAttributes({
        proxyMachineId,
        machineIds: [machineId], // grow this list as you add machines
      });
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      # proxy_machine_id is the proxy operator's own machineId from its
      # self-registration (register_machine). Persist it once and reuse.
      proxy_machine_id = 7  # replace with the proxy's actual machine_id

      proxy.write_proxy_did_attributes(
          proxy_machine_id=proxy_machine_id,
          machine_ids=[machine_id],
      )
      ```
    </CodeGroup>

    See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics) for the full rationale.
  </Step>
</Steps>

## Batch registration (10 machines)

Register multiple machines sequentially. Each iteration runs the full activate path (register, mint NFT, fund the machine, machine writes its own DID); then the proxy publishes the full fleet on its DID at the end.

<CodeGroup>
  ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
  import "dotenv/config";
  import { PeaqosClient } from "@peaqos/peaq-os-sdk";

  const proxy = PeaqosClient.fromEnv();
  const proxyMachineId = 7; // the proxy's own machineId from its self-registration
  const FLEET_SIZE = 10;
  const FAUCET_URL = "https://depinstation.peaq.network";

  const fleet = [];

  for (let i = 0; i < FLEET_SIZE; i++) {
    const machine = PeaqosClient.generateKeypair();

    try {
      const machineId = await proxy.registerFor(machine.address);
      await proxy.mintNft(machineId, machine.address);
      const nftTokenId = await proxy.tokenIdOf(machineId);

      await proxy.fundFromGasStation(
        {
          ownerAddress: proxy.address,
          targetWalletAddress: machine.address,
          chainId: "peaq",
          twoFactorCode: "654321", // Fresh TOTP code per call
        },
        FAUCET_URL,
      );

      const machineClient = new PeaqosClient({
        rpcUrl: proxy.rpcUrl,
        privateKey: machine.privateKey,
        contracts: proxy.contracts,
      });
      await machineClient.writeMachineDIDAttributes({
        machineId,
        nftTokenId,
        operatorDid: `did:peaq:${proxy.address}`,
        documentationUrl: "https://example.com/docs",
        dataApi: "https://example.com/events",
        dataVisibility: "public",
      });

      fleet.push({
        address: machine.address,
        privateKey: machine.privateKey,
        machineId,
      });
      console.log(`[${i + 1}/${FLEET_SIZE}] Activated ${machine.address} (machineId ${machineId})`);
    } catch (err) {
      console.error(`[${i + 1}/${FLEET_SIZE}] Failed for ${machine.address}:`, err);
      // Decide: skip this machine and continue, or abort the batch.
    }
  }

  // Publish the proxy's full fleet in one DID write.
  await proxy.writeProxyDIDAttributes({
    proxyMachineId,
    machineIds: fleet.map((m) => m.machineId),
  });

  console.log("Fleet:", fleet.length, "machines activated");
  // Persist fleet to a JSON file or database for later use.
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
  from peaq_os_sdk import PeaqosClient

  proxy = PeaqosClient.from_env()
  proxy_machine_id = 7  # the proxy's own machineId from its self-registration
  FLEET_SIZE = 10
  FAUCET_URL = "https://depinstation.peaq.network"

  fleet = []

  for i in range(FLEET_SIZE):
      machine_address, machine_key = PeaqosClient.generate_keypair()

      try:
          machine_id = proxy.register_for(machine_address)
          proxy.mint_nft(machine_id, machine_address)
          nft_token_id = proxy.token_id_of(machine_id)

          proxy.fund_from_gas_station(
              owner_address=proxy.address,
              target_wallet_address=machine_address,
              chain_id="peaq",
              two_factor_code="654321",  # Fresh TOTP code per call
              faucet_base_url=FAUCET_URL,
          )

          # Build a per-machine client directly: same RPC and contracts
          # as the proxy, but signed with the machine's own key.
          machine_client = PeaqosClient(
              rpc_url=proxy.rpc_url,
              private_key=machine_key,
              identity_registry=proxy.contracts.identity_registry,
              identity_staking=proxy.contracts.identity_staking,
              event_registry=proxy.contracts.event_registry,
              machine_nft=proxy.contracts.machine_nft,
              did_registry=proxy.contracts.did_registry,
              batch_precompile=proxy.contracts.batch_precompile,
          )
          machine_client.write_machine_did_attributes(
              machine_id=machine_id,
              nft_token_id=nft_token_id,
              operator_did=f"did:peaq:{proxy.address}",
              documentation_url="https://example.com/docs",
              data_api="https://example.com/events",
              data_visibility="public",
          )

          fleet.append({
              "address": machine_address,
              "private_key": machine_key,
              "machine_id": machine_id,
          })
          print(f"[{i + 1}/{FLEET_SIZE}] Activated {machine_address} (machine_id {machine_id})")
      except Exception as err:
          print(f"[{i + 1}/{FLEET_SIZE}] Failed for {machine_address}: {err}")
          # Decide: skip this machine and continue, or abort the batch.

  # Publish the proxy's full fleet in one DID write.
  proxy.write_proxy_did_attributes(
      proxy_machine_id=proxy_machine_id,
      machine_ids=[m["machine_id"] for m in fleet],
  )

  print(f"Fleet: {len(fleet)} machines activated")
  # Persist fleet to a JSON file or database for later use.
  ```
</CodeGroup>

<Note>
  The CLI does this whole flow in one command: `peaqos activate --for 0xMachineAddress --machine-key ./machine.key`. See [CLI reference](/peaqos/cli#peaqos-activate).
</Note>

## Per-machine key vs shared key

Two approaches for managing machine keys in a proxy fleet:

| Approach         | How it works                                                                             | Trade-offs                                                                                                    |
| :--------------- | :--------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ |
| Per-machine key  | Generate a unique keypair per device. Store each key on-device or in a secrets vault.    | Strongest isolation. If one key leaks, only one machine is compromised. Requires per-device key distribution. |
| Shared proxy key | All machines share the proxy operator's key. Machines never sign their own transactions. | Simpler operationally. Single point of failure: if the proxy key leaks, the entire fleet is exposed.          |

**Recommendation:** Use per-machine keys when the device can store secrets (HSM, TEE, encrypted filesystem). Use a shared proxy key for offline or passive hardware (sensors, meters) that never needs to sign.

<Note>
  **On the roadmap.** Proxy key management for offline devices is evolving. The current SDK supports both patterns above. Watch the [roadmap](/roadmap) for additional device-key options as they land.
</Note>

## Fleet queries

After registration, query the MCR API to list all machines under a proxy operator.

```bash curl theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
curl "${PEAQOS_MCR_API_URL}/operator/did:peaq:0xProxyAddress/machines?offset=0&limit=20"
```

Response shape:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
{
  "operator_did": "did:peaq:0xProxyAddress",
  "machines": [
    { "did": "did:peaq:0xMachine1", "machine_id": 101, "mcr_score": 62, "mcr": "BB", "negative_flag": false },
    { "did": "did:peaq:0xMachine2", "machine_id": 102, "mcr_score": 45, "mcr": "B", "negative_flag": false }
  ],
  "pagination": { "offset": 0, "limit": 20, "total": 2 }
}
```

See [GET /operator/\{did}/machines](/peaqos/api-reference/get-operator-machines) for full details.

## Error handling

| Error                                                                          | Cause                                                  | Resolution                                                                         |
| :----------------------------------------------------------------------------- | :----------------------------------------------------- | :--------------------------------------------------------------------------------- |
| `ValidationError: machineAddress must be a valid 0x-prefixed Ethereum address` | Malformed address string                               | Check the address format (0x prefix, 40 hex characters)                            |
| `RuntimeError: AlreadyRegistered` / `RpcError: already registered`             | The machine address is already on the IdentityRegistry | Skip this address, or query the MCR API to find the existing machine ID            |
| `RuntimeError: InvalidMachineAddress` / `RpcError: zero address`               | Attempted to register the zero address                 | Pass a valid machine address                                                       |
| Insufficient balance                                                           | Proxy wallet does not hold enough PEAQ for bond + gas  | Top up the proxy wallet. For a batch of N machines, ensure at least N \* 1.1 PEAQ. |
