> ## 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.

# Self-managed onboarding

> Register a single machine where the owner is the operator. Generate keys, set up 2FA, fund via Gas Station, and call registerMachine.

Owner equals operator. One keypair, one machine, one identity. This guide covers the full flow from environment setup through on-chain registration.

## Prerequisites

* Node.js ≥ 22 or Python 3.10+
* A funded EVM wallet (the owner address) with at least 1.1 PEAQ (1 PEAQ bond + gas)
* Environment variables configured per the [install guide](/peaqos/install)

## Environment setup

Set these environment variables before running any SDK call. Both SDKs read the same variable names.

```bash .env theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
# peaq mainnet RPC. See /peaqos/install#public-rpc-endpoints for alternatives
PEAQOS_RPC_URL=https://peaq.api.onfinality.io/public
PEAQOS_PRIVATE_KEY=0x<64-hex-chars>

# Mainnet contracts. fromEnv() requires every variable below to be set.
# For agung testnet addresses see /peaqos/install#agung-testnet-contracts.
IDENTITY_REGISTRY_ADDRESS=0xb53Af985765031936311273599389b5B68aC9956
IDENTITY_STAKING_ADDRESS=0x11c05A650704136786253e8685f56879A202b1C7
EVENT_REGISTRY_ADDRESS=0x43c6AF2E14dc1327dc3cc6c7117D1CD72fffEcbA
MACHINE_NFT_ADDRESS=0x2943F80e9DdB11B9Dd275499C661Df78F5F691F9
DID_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000800
BATCH_PRECOMPILE_ADDRESS=0x0000000000000000000000000000000000000805
```

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

## Full flow

<Steps>
  <Step title="Initialize the client from environment">
    `fromEnv` reads all required variables and returns a configured client. Throws `ValidationError` if any variable is missing.

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

      const client = PeaqosClient.fromEnv();
      console.log("Signer:", client.address);
      ```

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

      load_dotenv() # load envs from .env file

      client = PeaqosClient.from_env()
      print("Signer:", client.address)
      ```
    </CodeGroup>
  </Step>

  <Step title="Generate a keypair (optional)">
    If the machine does not already have a wallet, generate one. This is a local operation with no chain interaction.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      const keypair = PeaqosClient.generateKeypair();
      console.log("Address:", keypair.address);
      console.log("Key:", keypair.privateKey);
      // Store the private key securely. It cannot be recovered.
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      address, private_key = PeaqosClient.generate_keypair()
      print("Address:", address)
      print("Key:", private_key)
      # Store the private key securely. It cannot be recovered.
      ```
    </CodeGroup>

    If the machine already has a funded wallet, skip to step 5 and construct the client with that key directly. Steps 3-5 will fund the generated keypair.
  </Step>

  <Step title="Set up 2FA on the owner wallet">
    The Gas Station requires 2FA before it funds any address. Call `setupFaucet2FA` to start enrollment.

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

      const enrollment = await client.setupFaucet2FA(
        keypair.address,
        FAUCET_URL,
      );
      console.log("OTPAuth URI:", enrollment.otpauthUri);
      console.log("QR image:", enrollment.qrImageUrl);
      // Scan the QR or paste the URI into your authenticator app.
      // The QR URL expires after roughly 2 minutes.
      ```

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

      enrollment = client.setup_faucet_2fa(
          owner_address=keypair.address,
          faucet_base_url=FAUCET_URL,
      )
      print("OTPAuth URI:", enrollment["otpauth_uri"])
      print("QR image:", enrollment["qr_image_url"])
      # Scan the QR or paste the URI into your authenticator app.
      # The QR URL expires after roughly 2 minutes.
      ```
    </CodeGroup>
  </Step>

  <Step title="Confirm 2FA">
    After adding the secret to your authenticator app, submit the current TOTP code.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      await client.confirmFaucet2FA(
        keypair.address,
        FAUCET_URL,
        "123456", // Replace with your actual TOTP code
      );
      // No return value. Success means 2FA is active.
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      client.confirm_faucet_2fa(
          owner_address=keypair.address,
          faucet_base_url=FAUCET_URL,
          two_factor_code="123456",  # Replace with your actual TOTP code
      )
      # No return value. Success means 2FA is active.
      ```
    </CodeGroup>
  </Step>

  <Step title="Fund the machine wallet">
    Request initial gas from the Gas Station. The faucet either funds the wallet or skips if the balance is already sufficient.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      const result = await client.fundFromGasStation(
        {
          ownerAddress: keypair.address,
          targetWalletAddress: keypair.address,
          chainId: "peaq",
          twoFactorCode: "654321", // Fresh TOTP code
        },
        FAUCET_URL,
      );

      if (result.status === "success") {
        console.log("Funded:", result.txHash, result.fundedAmount);
      } else {
        console.log("Skipped, current balance:", result.currentBalance);
      }
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      result = client.fund_from_gas_station(
          owner_address=address,
          target_wallet_address=address,
          chain_id="peaq",
          two_factor_code="654321",  # Fresh TOTP code
          faucet_base_url=FAUCET_URL,
      )

      if result["status"] == "success":
          print("Funded:", result["tx_hash"], result["funded_amount"])
      else:
          print("Skipped, current balance:", result["current_balance"])
      ```
    </CodeGroup>
  </Step>

  <Step title="Register the machine">
    Call `registerMachine` to register the machine on the IdentityRegistry. The call sends 1 PEAQ as a bond with the transaction.

    <CodeGroup>
      ```typescript JS/TS theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      // Build a new client using the machine's own key
      const machineClient = new PeaqosClient({
        rpcUrl: client.rpcUrl,
        privateKey: keypair.privateKey,
        contracts: client.contracts,
      });

      const machineId = await machineClient.registerMachine();
      console.log("Registered, machine ID:", machineId);
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      # Build a new client using the machine's own key
      machine_client = PeaqosClient(
          rpc_url=client.rpc_url,
          private_key=private_key,
          identity_registry=client.contracts.identity_registry,
          identity_staking=client.contracts.identity_staking,
          event_registry=client.contracts.event_registry,
          machine_nft=client.contracts.machine_nft,
          did_registry=client.contracts.did_registry,
          batch_precompile=client.contracts.batch_precompile,
      )

      machine_id = machine_client.register_machine()
      print("Registered, machine ID:", machine_id)
      ```
    </CodeGroup>

    `registerMachine` mints the Identity NFT (`tokenId == machineId`) and stakes the bond. It does not mint the Machine NFT or write DID attributes; those are two separate calls in the next step.
  </Step>

  <Step title="Mint the Machine NFT and link the DID">
    Call `mintNft` to create the machine's financial NFT, then `writeMachineDIDAttributes` to bind `machineId` and the new `nftTokenId` to the machine's DID.

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

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

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark"}}
      machine_client.mint_nft(machine_id, address)
      nft_token_id = machine_client.token_id_of(machine_id)

      machine_client.write_machine_did_attributes(
          machine_id=machine_id,
          nft_token_id=nft_token_id,
          operator_did="",
          documentation_url="https://example.com/docs",
          data_api="https://example.com/events",
          data_visibility="public",
      )
      ```
    </CodeGroup>

    The Machine NFT gets an independent `tokenId` separate from `machineId`. See [Machine NFT ownership](/peaqos/concepts/machine-nft#ownership-semantics) for the full rationale.

    After all three calls, the machine has a peaqID, an Identity NFT, a Machine NFT, and a 1 PEAQ bond. It starts as `Provisioned`: on-chain, but not yet rated. It will graduate onto the rated MCR scale once it accumulates enough revenue and activity events.
  </Step>
</Steps>

## Error handling

| Error                                                                                           | Cause                                    | Resolution                                                         |
| :---------------------------------------------------------------------------------------------- | :--------------------------------------- | :----------------------------------------------------------------- |
| `ValidationError: PEAQOS_PRIVATE_KEY is required`                                               | Missing environment variable             | Check your `.env` file and shell export                            |
| `AlreadyRegistered` (both SDKs)                                                                 | The address is already registered        | Query the MCR API with the address to find the existing machine ID |
| `RuntimeError: Invalid 2FA code` / `ApiError: Invalid 2FA code`                                 | TOTP code expired or mistyped            | Wait for a fresh code from your authenticator and retry            |
| `RuntimeError: 2FA locked` / `ApiError: 2FA locked`                                             | Too many invalid 2FA attempts            | Wait before retrying                                               |
| `RuntimeError: Faucet rate limit exceeded` / `ApiError: Faucet rate limit exceeded`             | Too many funding requests                | Wait and retry after a short interval                              |
| `RuntimeError: Owner daily funding cap exceeded` / `ApiError: Owner daily funding cap exceeded` | Daily funding cap reached for this owner | Fund manually or wait until the cap resets                         |
| Insufficient balance for gas + bond                                                             | Wallet does not hold 1 PEAQ + gas        | Top up the machine wallet and retry                                |

## What the machine receives

After completing the full flow (register + mint NFT + write DID attributes):

* **peaqID** (W3C DID) anchoring the machine's identity
* **Identity NFT** minted automatically at registration (`tokenId == machineId`)
* **Machine NFT** (LayerZero V2 ONFT) minted via `mintNft`, with an independent `tokenId`
* **1 PEAQ bond** deposited into the IdentityStaking contract
* **Gas Station** funding (if requested)
