The CLI runs all seven stages from one command and waits out the external legs. You can also run one stage per invocation. The Python and JavaScript SDKs expose the same phases, and the scripts in the same flow in the SDKs run them from one invocation.
What you need
Getting PEAQ or USDC onto Solana is outside the CLI: bridge PEAQ through the OFT, or buy either token on Solana. Onboarding runs on mainnet only. There is no testnet, so the preview is the only rehearsal. A settled bond is not refunded unless peaq refuses it (see the point of no return).
What it costs
Two parts, priced differently. The SOL you pay is rent and network fees. The chain fixes them by the size of the machine’s record (the DID document, the machine type and the credential subject), so they are the same for every machine whose inputs have the same sizes. The bond is priced in USD and paid in PEAQ or USDC at the oracle’s price, so it moves with the price. The preview (--dry-run) quotes your bond, with the escrow to set, before anything is signed.
The SOL figure breaks down as follows. The request pays three rents on a machine’s first request: the machine’s terminal record (1,071,880 lamports), the request (2,169,160) and its escrow account (1,488,440), plus a 6,500-lamport fee. The settlement returns the escrow account’s rent less its fee (+1,482,440 on PEAQ; +1,479,440 on USDC, whose swap costs more compute). The mint pays the
Machine record (2,966,720 for the template below; it grows with the document, and was 2,890,520 for the 2026-10-06 runs’ smaller one), the MachineState record (878,840) and a 6,210-lamport fee (121,000 CU at the default price; 11,000 before 0.0.17, when the mint asked for 600,000). The link push pays the push account’s one-time rent (1,005,840) and a 6,200-lamport fee. The request account’s 2,169,160 stays locked: it only returns when the request is closed, which the CLI does not do.
Every Solana write pays a priority fee of 10,000 micro-lamports per compute unit by default: about 4,900 lamports across a PEAQ-rail onboarding, about 7,900 on USDC. These totals assume the mint’s compute limit sized from its simulation (about 121,000 CU, CLI 0.0.17); the mint’s fee fell from 11,000 to 6,210 lamports with it. A write sent without priority is more likely to be dropped by Solana’s public endpoint under load. Since CLI 0.0.17 / SDK 0.11.2, a write whose first send is lost is re-sent as the same signed bytes while it can still land: same signature, so it cannot land twice. A write that still never lands is replaced: a request or settlement by a fresh one, and a mint is minted again on the next run (CLI 0.0.16 / SDK 0.11.1, see Resume and troubleshoot). A dedicated Solana RPC endpoint makes drops rarer than the public one.
Fund the owner with at least 0.015 SOL per machine and the bond in the pay-in token plus the cushion the preview shows, and the operator with 0.05 PEAQ on peaq. Bonds are not withdrawable.
Configure
peaqos init writes .env with PEAQOS_NETWORK, TOKENOMICS_DEPLOYMENT_ID, PEAQOS_RPC_URL and the six contract address variables that every client needs, keyless previews included. The two Solana lines are not part of the wizard. The SDK scripts read the same .env. Keep endpoint URLs in .env, not in exported variables or command arguments. An exported variable wins over .env, and a stale exported PEAQOS_RPC_URL is a common cause of a failed read. The CLI warns once per variable when an exported value differs from .env, naming both hosts. --svm-rpc-url and --svm-network before the command name override both.
Keep the journal (peaqos.log for the CLI) and the original inputs in this directory for every later run. The journal is what lets a rerun reconcile instead of resubmitting. Use one directory per machine. Each CLI run also writes onboarding-<machine-id>.json here: a record of every invocation for you to read (a rerun never reads it).
Create the two wallets in the vault and note their public addresses. Fund them yourself; wallet creation does not.
OWS_PASSPHRASE from your secret store.
The DID document
Save asdid.json, the same template as the quickstart’s. controller is the owner wallet’s base58 address and publicKeyMultibase its key in multibase form (the quickstart derives it). authentication entries are zero-based indices into verificationMethods. The optional machine-level controller is a separate flag (--solana-controller), not a field here.
GET /machine/did:peaq:<id>), and changing them later is a separate write.
Limits come from the Solana program: 8 verification methods, 8 authentication indices, 8 service endpoints, 128 UTF-8 bytes per string field, 64 bytes for the machine type, 512 bytes of credential subject. Oversized input is rejected before anything is signed; nothing is truncated.
The argument set
Every invocation of one onboarding takes the same arguments. Set the shell variables first. The:? guards make the shell stop on an unset variable instead of passing an empty argument. The quickstart shows how to derive CREDENTIAL_SUBJECT_HEX from a serial number and publicKeyMultibase from the owner address. The blocks on this page carry # comments: run them in bash, or run setopt interactive_comments first in an interactive zsh.
--max-in, is added after the preview, because the preview suggests it: ARGS+=(--max-in "$MAX_IN"), in base units of the pay-in token (9 decimals for PEAQ, 6 for USDC).
The machine ID is the same derivation as on peaq,
uint256(keccak256(abi.encode(machineType, credentialSubject))), a decimal string in JSON and the DID did:peaq:<decimal id>. --for and --machine-key are rejected with --chain solana. The removed reservation flow’s options are refused by name with OPTION_NOT_FOR_CHAIN (exit 1), naming the replacement: --payment (use --pay-in), --max-net-peaq-amount and --max-usdt-amount (use --max-in). --from-block is accepted and ignored.
What stays fixed
The SDK fingerprints the machine as the mint and the link push need it: the owner, the DID document and controller, the manufacturer, the machine type and credential subject, the two native ceilings, and--evm-operator if you pass one. A changed machine type or credential subject is a different machine ID and a new onboarding; changing any of the others stops the remaining stages until you put the original values back. The request freezes the tier, the rail and the escrow when it opens, so keep --tier, --pay-in and --max-in for every later invocation too.
The fingerprint also covers the deployment record inside the installed SDK. Finish a machine before upgrading the CLI or the SDK; confirmed stages reconcile from the chain and are never paid twice.
The point of no return
Until[4/7] you can step out: --phase cancel_request returns the escrow in the pay-in token and the request’s rents to the owner, from a request that is still Pending or Committed. After the settlement there is no cancel. The bond goes to peaq; if peaq refuses it, the Trust Validator refunds it in PEAQ, on both rails, to the owner’s PEAQ token account (ACTIVATION_REFUNDED, with the reason and the amount), and a rerun opens a new request. The settlement’s terms open with “This cannot be cancelled afterwards.”
Run it in one command
1
Preview
Keyless; nothing is signed or sent. One screen: where the onboarding stands, the registration, the bond and what What production printed on 2026-10-07 for a fresh machine of an owner wallet that was already registered:Add the suggested escrow to the argument set, and keep it for every later invocation:A wallet that is not registered yet is quoted for the operator
--max-in escrows against the owner’s balance (on USDC, the swap’s estimated cost), the rents, the settlement path and the upgrade lock, and what each of the seven stages would do. A preview is not transaction evidence. peaqos -v and --json show everything behind it.[1/7] will register it under, so the first run sees its bond in advance. On USDC, check that the settlement row names the lookup table as verified.2
Run
[2/7]), then each stage’s terms and outcome on stderr: every amount in base units with the exact token figure, each cap, and the signer. A confirmed write also says how many sends it took (landed after 1 send; the mint adds the compute it used of its limit; CLI 0.0.17). --yes accepts each stage’s terms as printed; without it, each stage asks once. With --json, stderr carries only JSON lines (plan, consent, the stage and wait events) and stdout one JSON result at the end; each confirmation carries sends and resend_errors.What production printed for the USDC run of 2026-10-06, trimmed (the owner wallet was registered by an earlier machine, so [1/7] sent nothing), with the landed after lines that 0.0.17 adds under each confirmed write:landed after 2 sends · 93,009 of 121,000 CU.A first machine of an owner wallet sends the registration at [1/7] instead. With the node healthy, the proven runs took about 10 minutes from one command:Done means exit
0 and next_step.phase equal to complete; with --json, jq -r .next_step.phase on the output.3
If it stops early
Rerun the same command. It reads
peaqos.log, works out where the onboarding stands and starts there: a live request resumes at the stage its state names (Pending → [3/7], Committed → [4/7], Settled → [5/7], Bonded → [6/7]). Nothing recorded is ever signed again. A write is re-sent as the same signed bytes only inside the invocation that signed it, while it can still land; a rerun holds no bytes and re-sends nothing. That covers a wait that ran out (PENDING, exit 2, naming operator_binding, credit, bond or link_application), Ctrl-C (CANCELLED, exit 1), a timeout and a lost connection. Every stop prints the command as resume_command.Run it one stage at a time
The same onboarding, split up. Each command takes the full argument set and the wallet its stage needs:svm-operator for the registration, svm-owner for every Solana write (both from Configure).
1
Register the owner on peaq (operator wallet)
Skipped, and nothing signed, when the owner wallet is already bound to this operator.The node mirrors the binding to Solana a minute or two later, and the request needs it. With
--wait-minutes the stage waits for the mirror and ends confirmed. Without it, the stage exits PENDING (exit 2) right after the registration lands, and a rerun waits.2
Open the request, then settle it (owner wallet)
--phase and with --wait-minutes to wait for it, or preview --phase finalise until it no longer reports the credit unresolved. A per-phase preview prints every field as Section / Field: value: the request’s state is Terminal / Request state (pending, then committed), and on USDC the swap’s figure is Observations / Settlement / Swap / Simulated in amount. The settlement simulates again before signing and swaps up to that figure plus the slippage cushion, never more than the escrow; what the swap does not use comes back in USDC. Solana’s public endpoint can answer one preview’s reads at slightly different slots. If the request reads committed while the settlement still says REQUEST_NOT_COMMITTED, preview once more.3
Mint the machine and push the link (owner wallet)
Once peaq has booked the bond:The link push (
send_machine_registry_push on the Solana bridge adapter) tells peaq the machine exists on Solana and who operates it; the Trust Validator node then delivers it on peaq. A machine whose last push already carries its values reports reason: "link_already_pushed", loads no wallet and sends nothing. --wait-minutes lets the link stage wait for the node’s delivery and end at complete. Without it (the default is 0) the stage ends once the push is sent; rerun it with --wait-minutes to wait for the delivery. Nothing is sent again.Each stage’s summary covers that invocation only: the last stage’s Spent lists the mint and the push, and its audit trail reads the earlier stages’ transactions as “not read”. For the whole onboarding’s cost read peaqos machine history, or --dry-run --audit without --phase.Cancel request on Solana: confirmed <signature> and how many sends it took (CLI 0.0.17; before, it printed no result line). With no request open, the outcome is decided before consent and the passphrase: a cancel this journal recorded ends already done; nothing was signed; a cancel the journal did not record (the node’s, once the request expired) ends REQUEST_CLOSED; a request closed after the mint ends REQUEST_NOT_CANCELLABLE.
What “complete” means
Onboarding is complete whennext_step.phase (and onboarding_state.evidence.stage.phase) reads complete, or the human output prints the summary with link seq N applied. Behind that is one on-chain read: peaq’s MachineBridgeAdapter.lastAppliedMirrorPushSeq for the machine (the watermark) is at or above the sequence of your link push. The block in the summary is the finalized block the watermark was read at, not the block the push applied in. A later push still in flight, such as an operator change, is reported as later_push_pending; the onboarding stays complete.
A Solana receipt for the push proves the send, not the application, and the CLI does not use it as evidence. The push’s receipt carries the node’s queue entry (TvRelayQueued, with its outbox nonce); the node then calls tvReceive on peaq’s bridge adapter, and the watermark moves. Every output block says where it came from: provenance: "journal_claim" is what the CLI recorded at submit time, chain_verified is what the SDK read back from peaq and Solana.
See what ran
From any directory, with only the deployment’s.env (no journal, original inputs or wallet):
owner, operator, node, layerzero), where it landed and what it cost its payer. A delivery by the node’s tvReceive reads node, and -v links the push to the node’s outbox (tv_outbox:). A transaction the owner did not pay can still move the owner’s balances: when the node cancels an expired request, its row shows the node’s fee and then what came back to the owner (owner +3,657,600 lamports, and the escrow), and the owner’s total is its net change. The registration and the binding belong to the owner wallet: on a later machine of the same owner they are marked reused and stay out of that machine’s totals. A source that could not be read is named and the command exits 2; the rows shown are still exact, and a rerun reads it.
In the machine’s directory, onboarding-<machine-id>.json holds every invocation (start, end, exit code, stages), the references, spent, the audit trail and, once complete, the same history. peaqos activate "${ARGS[@]}" --dry-run --audit re-reads the onboarding from peaqos.log: every transaction confirmed against the chain with its slot or block, what it spent, and the link push’s relay by the node with its outbox nonce.
Read the state
With the machine ID alone, no wallet, journal or DID document (MACHINE_ID is the machine_id from any output, the preview included):
--chain solana, the CLI falls back to the Solana read when the peaq record is missing or homed elsewhere and PEAQOS_SVM_RPC_URL is configured. That result has status: "observed" and a native_current_state with separate peaq (finalized) and Solana (confirmed) observations, not an atomic snapshot. With --chain solana the CLI reads the native management state instead, including the top-level link block with the watermark (not_pushed, pending_application, linked or unavailable); it needs the native machine to exist. present proves the accounts exist, not that this onboarding created them or that the link is complete.
Resume and troubleshoot
Never delete the journal, never change the inputs, never submit a replacement transaction yourself. The SDK records every signed reference before broadcast. Every refusal before signing says so.
Exit codes:
1 input or consent (and CANCELLED), 2 chain, read or transaction failure (and PENDING after a wait), 3 configuration or missing dependency. With --json, read error_code and the retained evidence, not only the exit code. A stage failure sits in phases[] with its error_code, detail and signed, so a refusal before signing never reads as a failed transaction.
The same flow in the SDKs
Both SDKs run the same seven stages. The scripts below are the ones that onboarded the proven machines (Python on both rails, JavaScript on both rails): each tab gives the whole file to copy, then walks through the parts that matter, quoted as they are. Each does whatever the SDK’s status read says is next until the machine iscomplete. Each journals every write before it is broadcast and resumes from that journal, so a second invocation after any stop continues and never signs a recorded write again. Since SDK 0.11.2 a write whose first send is lost is re-sent as the same signed bytes, inside the invocation that signed it; a new invocation holds no bytes and re-sends nothing. Neither signs on peaq: register the owner wallet first, with the CLI’s [1/7] (--phase registration) or the SDK’s register_solana_operator / registerSolanaOperator. The machine’s directory holds the same .env as the CLI and the DID document as did.json; the owner wallet’s passphrase is read from OWS_PASSPHRASE.
- Python
- JavaScript
Run it with the machine’s values; every Solana write is signed by the OWS wallet Save the whole script as
The client is keyless (nothing on peaq is signed), and the machine’s inputs come from the arguments and The run is one loop over the status read. The terminal stages (A terminal stage: the waits (A native stage, the mint or the link push, against a fresh plan and the journal’s reconciled history:Once Its output on production, 2026-10-06 (PEAQ):
--owner-wallet names:onboard_solana.py, in an environment with the SDK installed (pip install -U "peaq-os-sdk[solana,ows]", 0.11.2 or newer). It reads the machine directory’s .env itself, and the passphrase from OWS_PASSPHRASE in your shell:onboard_solana.py (the whole script)
onboard_solana.py (the whole script)
did.json:request, credit, finalise, bond) go through ActivateSolanaTerminalParams, the two native ones through ActivateSolanaMachineParams:credit, bond) sign nothing; the writes sign with the owner and journal before broadcast:COMPUTE_UNIT_LIMIT (600,000) is the mint’s compute limit, and the SDK uses an explicit compute_unit_limit as given. Since SDK 0.11.2 the plan carries plan.native.suggested_compute_unit_limit: the limit sized from the mint’s simulation (the simulated units × 1.3, rounded up to the next 1,000, at least 50,000; about 121,000 on production, where the mint used 93,009). Pass that value instead of the constant to pay the fee actually needed, 6,210 lamports instead of 11,000 at the default price.The constant’s docstring says a journaled write is reconciled, never re-sent. Since 0.11.2 the SDK does re-send a write whose first send was lost: the same signed bytes, every 2 s while the signature is unknown and the blockhash valid (at most 40 times), inside the process that signed them, so the write can land at most once. result.confirmation.send_count (SendCount(sends, resend_errors)) says how many sends it took; it is None for a row this process did not send. A mint that ran out of compute and created nothing (receipt reason compute_budget_exceeded) is minted again on the next run, like a mint that never landed: the recovery’s failed_without_effect, and no_effect (which also covers never_landed), admit it. The script’s for_request already keeps a closed request’s rows out of a new request; since 0.11.2 the SDK’s request phase reads the terminal record itself and opens a new request at the next nonce after a cancelled or refunded one. A phase run alone on such a request (credit, bond, finalise, cancel_request, or the settlement preview) is refused with REQUEST_CLOSED before anything is signed.The journal appends one serialize_solana_onboarding_attempt row per write and flushes it to disk before the SDK broadcasts; a rerun decodes the rows with decode_solana_onboarding_attempt and hands them back:complete, the script reads the onboarding back from the chains:[2/7] request: complete … nonce 1 · 24 s, [3/7] credit: complete … 121 s, [4/7] finalise: complete … 10 s, [5/7] bond: complete … 212 s, [6/7] native_onboarding: confirmed (native_creation_confirmed, new_attempt), [7/7] linkage with link push quote: ready · transport trust_validator · messaging fee 0 · rent 1005840 · compute 120000 · total 1010840 lamports, then tv relay: … nonce 3 applied True and done. Reference: SDK Python: Solana.A link push over LayerZero
Production delivers the link push through the Trust Validator node. A deployment may instead route it over LayerZero, per message type; the SDK reads the route from the chain before every push, and--phase linkage --dry-run names it. On a LayerZero route the push pays a messaging fee (about 962,000 lamports when it was production’s route) and needs a ceiling: --max-link-push-fee-lamports on the one-command run and on --phase linkage (max_link_push_fee_lamports / maxLinkPushFeeLamports in the SDKs). Without it the push is refused before signing (INVALID_INPUT, with the dry run as guidance); above it, FEE_LIMIT_EXCEEDED. LayerZero’s Executor then delivers it on peaq, and machine history shows the delivery’s actor as layerzero with a LayerZero Scan link.
After onboarding
- Manage the machine with the same
peaqos machinecommands plus--chain solana(status,transfer --unsafe,suspend,resume,did set-controller, the DID setters, and the Solana-onlyset-evm-operator), or the same SDK methods; the SDK resolves the home chain from the machine ID and refuses the wrong chain. Transfer, controller and the peaq operator link need the Solana owner signature; DID lists, suspend and resume take owner or controller. There is no safe transfer on Solana (transferrequires--unsafe,--data-hexis refused), andapprove/approve-alldo not exist; the SDKs refuseapproveMachineandsafeTransferMachinefor a Solana machine. Details: SDK JS, SDK Python. - Submit events with
submitEvent/submit_eventorpeaqos qualify event: the SDK routes a Solana-homed machine to the SolanaEventRegistryprogram and signs with the owner’s or controller’s OWS Solana signer (the peaq operator does not qualify). In Python setclient.solana_signerbefore the call, orsubmit_eventraisesSIGNER_UNAVAILABLE; the CLI attaches it itself. The first event for a machine also pays rent for its event-log account. - Credit rating: the 2.0 MCR scores a Solana-homed machine once its Solana indexer has the machine’s events; until then the response carries
home_chain: 5and arating_unavailablereason. A freshly created machine readsmcr: "Provisioned",mcr_score: 0andrating_unavailable: "no_events_yet". See the API reference. - Monetization opt-in for a Solana-homed machine is refused by the CLI and both SDKs with
SOLANA_MONETIZATION_UNVERIFIEDbefore anything is signed. The owner can sign the opt-in directly against the monetization endpoint. Rating reads and peaq-homed opt-in are unaffected. - Relocation between peaq and Solana stays switched off on chain. Onboarding on Solana is not a relocation: the machine is created there.
Related
- Onboarding quickstart: the same run from a blank terminal, wallets and funding included
- Activate: the one-transaction flow on peaq
- Omni-chain: Solana: what is live on Solana
- Smart contracts: Solana mainnet: the program IDs
- CLI: activate

