CLI - Custom Commissions

The following is a step-by-step guide for Solana validators to set up and run the Pye CLI for automated rewards distribution to Programmable Stake Account (PSA) lockups.

Repository: github.com/pyefi/pye-cli-2arrow-up-right

Estimated Setup Time Required: 15-20 minutes

How it Works

The Pye CLI automates the settlement of excess rewards owed to stakers under custom commission terms. The CLI runs as a long-lived daemon that:

  1. Fetches pending reward payments for your organization from the Pye backend

  2. Calculates the excess owed to each staker (what they’re entitled to minus what they already received)

  3. Batches and submits SOL transfers on-chain (up to 50 per transaction) through payer wallet. If payer wallet runs out of funds it will automatically backfill appropriate payments

  4. Sleeps for a configurable interval, then repeats

The CLI never modifies your vote account, withdrawer keys, or node configuration. It only reads reward data from the Pye API and sends SOL transfers from a dedicated payer wallet. It is completely free.

Prerequisites

  • Pye API key — sign in to the Pye Dashboardarrow-up-right, create an organization and complete verification, then navigate to Custom Commissions and generate an API key

  • Dedicated server — a small EC2 instance (e.g. t3.micro with 2 vCPUs, 1 GB RAM) or equivalent is sufficient, however we recommend 2GB+ RAM to ensure no issues during package installation. The CLI is lightweight (~5 MB memory) and makes minimal RPC calls.

  • Rust toolchain — edition 2024 compatible (rustc 1.85+)

  • Solana CLI tools — for keypair generation (solana-keygen)

  • Payer wallet — a Solana filesystem keypair funded with SOL (see Payer Wallet Setuparrow-up-right)

  • Stable internet — the CLI must reach both the Solana RPC endpoint and the Pye API

Important: Run only one instance of the CLI per organization. Multiple simultaneous instances are not supported and may result in duplicate transfers.

Quick Reference

For experienced users who want the commands without the explanation:

See the sections below for detailed instructions, systemd setup, and troubleshooting.

Installation

1. Install system dependencies (Ubuntu/Debian)

If you are running a different Linux distribution, install the equivalent packages for your platform (gcc, make, pkg-config, and OpenSSL development headers).

2. Install Rust

3. Clone and build

The binary is at target/release/pye-cli.

The first build downloads and compiles all dependencies. On a t3.micro this takes approximately 8-10 minutes. You will see one compiler warning about unreachable code — this is expected and does not affect functionality.

To update to a newer version later:

4. Verify installation

You should see output ending with pye-cli 2.0.0 (or later). A JSON log line may appear before the version — this is normal and can be ignored.

Payer Wallet Setup

The payer wallet is the Solana account that funds all excess reward transfers to lockup accounts. This is not just a fee payer — the actual SOL for reward distributions is sent from this wallet. Keeping it funded is critical to the CLI’s operation.

Create a keypair

Install the Solana CLI tools if you don’t already have them:

Generate a new keypair for the payer wallet:

Restrict file permissions:

Note the public key in the output — this is the address you will fund with SOL.

Fund the wallet

Transfer SOL to the payer wallet’s public key from your existing wallet or exchange. You can verify the balance with:

How much SOL to hold

The payer wallet needs enough SOL to cover:

  • Reward transfers — the total excess rewards owed to all lockups each cycle. This depends on the number of lockups, their stake amounts, and the commission terms.

  • Transaction fees — a small amount (~0.000005 SOL per transaction, batched at up to 50 transfers per transaction).

You can fund the wallet with as much as you are comfortable with to cover payouts into the future. As a starting recommendation, fund the wallet with enough SOL to cover at least 2-3 epochs of expected reward payouts. Monitor the balance regularly and top up before it runs low — if the wallet has insufficient funds, transfers will fail. However as soon as funds are topped up, the CLI will backfill and payout all outstanding payments in the past.

Configuration

All options can be set via CLI flags or environment variables. Environment variables are recommended for production deployments.

Required

Variable
Flag
Description

PAYER

--payer

Absolute path to the payer keypair JSON file

PYE_API_KEY

--pye-api-key

Your organization’s Pye API key

Optional

Variable
Flag
Default
Description

RPC_URL

--rpc-url

https://api.mainnet-beta.solana.com

Solana RPC endpoint

API_URL

--api-url

Pye production API

Pye backend URL (do not change unless instructed)

CYCLE_SECS

--cycle-secs

60

Seconds between polling cycles

RUST_LOG

info

Log verbosity: error, warn, info, debug, trace

Environment file

Create a .env file in the same directory as the binary (or wherever you run the CLI from):

The CLI loads .env from the current working directory on startup. When running via systemd, use the EnvironmentFile directive instead (see Production Deploymentarrow-up-right).

Usage

validator-lockup-manager

Runs continuously as a daemon, polling the Pye API and automatically distributing excess rewards.

Behavior:

  • Polls the Pye backend API every -cycle-secs seconds (default: 60) to check for pending reward payments

  • The polling interval controls how often the CLI checks — it does not mean transfers happen every 60 seconds. The Pye backend determines when payments are ready (typically once per epoch after reward data is aggregated). If no payments are pending, the cycle is a no-op.

  • For each payment where the expected amount exceeds the base amount, creates a SOL transfer to the lockup account

  • Batches up to 50 transfer instructions per Solana transaction

  • Transfers execute automatically with no confirmation prompt

  • On failure, logs the error and continues to the next cycle without crashing

Production Deployment

systemd service

Create the service file:

Create the environment file:

Enable and start:

Monitoring

View live logs:

Logs are in JSON format (designed for CloudWatch ingestion). Example output:

Log entries include:

  • Payment fetch results and counts

  • Transaction signatures on successful transfers

  • Errors encountered during processing

Check service status:

Reward Calculation

For each pending payment, the Pye backend computes the excess reward across all three reward streams (inflation, MEV tips, and block rewards) and returns pre-aggregated values. The CLI then calculates:

  • expected_amount = what the staker is entitled to under their custom commission terms

  • base_amount = what the staker already received through normal Solana staking mechanics

  • If the transfer amount is zero, no transfer is made for that payment

  • Transfers are batched at 50 instructions per Solana transaction to stay within compute limits

Migrating from V1 to V2

Skip this section if you are setting up the CLI for the first time.

chevron-rightMigration Guidehashtag

V2 is a fundamentally different architecture. The V1 CLI was a heavy client that computed everything locally — it deserialized on-chain Pye accounts via Anchor, queried Jito’s MEV API directly, walked RPC slots to calculate block rewards, and reported metrics to InfluxDB. V2 replaces all of that with a single API call to the Pye backend, which now handles reward aggregation server-side. This means V2 has no Anchor dependency, no Jito integration, no on-chain account parsing, and no InfluxDB metrics. The result is a simpler binary with far fewer runtime dependencies and RPC calls.

When to migrate

The best time to migrate is right after an epoch boundary, once V1 has finished processing the previous epoch’s rewards. Watch the V1 logs (journalctl -u pye-cli -f) and wait for a clean sleep interval — do not stop V1 while it is actively submitting transactions. There is no risk of duplicate payments: the Pye backend tracks which payments have been sent, so V2 will only pick up pending payments that V1 has not already processed.

Step-by-step migration

1. Stop the CLI and disable V1

Wait for V1 to complete its current cycle, then stop it:

2. Contact Pye Team

Let the Pye team know that you are migrating so that we can reset and remove payments already completed.

3. Install and start using CLI V2

Clone the new repository and build:

3. Obtain your API key

V2 authenticates via an organization-scoped API key instead of a vote pubkey + issuer list. Your API key is provided during onboarding. If you haven’t received one, contact the Pye team.

4. Update your environment file

V1 environment:

V2 environment:

Note: RPC has been renamed to RPC_URL. The VOTE_PUBKEY, ISSUERS, CONCURRENCY, BLOCK_RETRY_DELAY, and DRY_RUN variables are no longer used and should be removed.

5. Update your systemd unit file

Replace the ExecStart line and EnvironmentFile path:

6. Start V2

What changed

Aspect
V1
V2

Repository

pyefi/pye-cli (workspace)

pyefi/pye-cli-2 (single crate)

Solana SDK

v2

v3

Reward computation

Local — Anchor deserialization, Jito API, RPC slot walking

Server-side — single Pye API call returns pre-computed values

On-chain dependencies

Anchor, pye-core-cpi library

None

Metrics

solana-metrics (InfluxDB datapoints)

tracing (JSON logs for CloudWatch)

Validator identity

--vote-pubkey + --issuers flags

Organization API key

Commands

validator-pye-account-manager, transfer-excess-rewards

validator-lockup-manager

Concurrency

--concurrency flag (parallel RPC)

Sequential batching (50 instructions/TX)

Dry run

--dry-run flag

Removed

Error handling

Panics on most errors

Logs errors and continues to next cycle

Removed flags

The following V1 flags have no V2 equivalent:

  • -vote-pubkey / VOTE_PUBKEY — the Pye backend identifies your validators from your API key

  • -issuers / ISSUERS — lockup filtering is now handled server-side

  • -program-id / PROGRAM_ID — V2 does not interact with on-chain Pye program accounts

  • -concurrency / CONCURRENCY — V2 makes minimal RPC calls; no parallel request tuning needed

  • -block-retry-delay / BLOCK_RETRY_DELAY — block reward calculation is now server-side

  • -dry-run / DRY_RUN — removed in V2

Troubleshooting & FAQ

chevron-rightCLI exits with ReadKeypairError hashtag

The payer keypair file path is invalid or the file is not readable. Verify the path and file permissions (chmod 600).

chevron-rightBuild fails or takes too longhashtag

On t3.micro (1 GB RAM), the build may appear stuck or freeze during compilation of large crates like rustls or brotli. This is usually caused by the system running out of memory. Add swap space and retry — the build will resume where it left off:

After the build completes, remove the swap file to reclaim disk space — the default 8 GB root volume is tight once build artifacts and Solana CLI tools are installed:

Alternatively, launch the instance with a 16 GB root volume to avoid space issues entirely and 2GB RAM to avoid issues with crate installation. The initial build takes ~8-10 minutes on a t3.micro. Subsequent builds after code changes are faster since dependencies are cached.

chevron-rightAre there risks of duplicate transfers?hashtag

There is a risk if you are running multiple CLIs or versions of the CLI. Ensure only one instance of the CLI is running per organization. Check with systemctl status pye-cli and ps aux | grep pye-cli.

chevron-rightSolanaClientError or RPC errorshashtag

The CLI could not communicate with the Solana RPC endpoint. Verify your RPC_URL is correct and reachable. If using the public mainnet RPC, note that it has rate limits — consider using a dedicated RPC provider for production.

chevron-rightTransfers are not being madehashtag

Check that the payer wallet has sufficient SOL to cover both the reward transfer amounts and transaction fees. The payer wallet is the source of all SOL sent to lockup accounts — not just fees. Top up the wallet and re-run or wait for the next cycle.

chevron-rightReqwestError: error sending request for urlhashtag

The CLI could not reach the Pye backend API. Check that the server has outbound internet access and DNS resolution is working. You can test connectivity with: curl -s <https://gwtgzlzfnztqhiulhgtm.supabase.co>. If using a firewall or security group, ensure HTTPS (port 443) outbound traffic is allowed.

chevron-rightPyeApiError with “API Key not found”hashtag

Your API key is invalid, expired, or not yet provisioned. Verify PYE_API_KEY is set correctly. If the issue persists, contact the Pye team to confirm your key is active.

chevron-rightFor CLI users, are there any other consequence if the node is down outside epoch changes?hashtag

Yes. CLI v1 depends on the RPC node and Jito’s API. It requires knowing the active stake for the previous epoch to calculate rewards correctly.

There is no way to determine active stake for current_epoch - 2. This means missing epochs can result in unrecoverable reward calculation gaps.

There is a separate CLI command that allows calculating rewards for current_epoch - 1. If the CLI is started with the long-running manager command, you would want to use the transfer command and then restart the manager to wait for the next boundary.

chevron-rightFor CLI users, how many epochs should avoid being missed? What happens to rewards for missed epochs?hashtag

You should avoid missing any epoch. If you miss an epoch and were supposed to pay lamports to a Lockup, the UI will highlight this error.

Currently there is no harsh protocol penalty — mostly social consequences. However, there has been exploration into allowing stakers to early-withdraw if payments are missed for a certain number of epochs.

Because stakers are locked up, you can make good on the payment in epoch + 2 (as long as it’s before maturity). Stakers are still rewarded, but they may miss out on compounding for the missed epoch.

chevron-rightFor CLI users, does the CLI need to be online at the exact moment of the epoch boundary? What happens if we need to reboot the server for maintenance.hashtag

There are two commands in CLI v1 (v0.1.3):

  • validator-pye-account-manager

    • Long-running process

    • Waits for the next epoch boundary

    • If started late, it does not backfill missed epochs

  • transfer-excess-rewards

    • One-shot command

    • If run in epoch N, it calculates rewards for epoch N-1 and executes the transfer

Safe timing considerations:

  • Avoid rebooting in the first ~12 hours after an epoch boundary

  • MEV data (e.g., from Jito) can take hours to populate

  • The middle of the epoch is safest

The lockup Payment Tracker uses checkpoint-based recovery, so no data is missed after restarts.

Practical recommendation: Schedule maintenance mid-epoch (≈ 1–1.5 days after epoch start).

chevron-rightFor CLI users, does the CLI Hot Wallet (used for distributing rewards) need to be whitelisted/registered on-chain, or is it simply a funding source for gas/transactions? If it's just a funding source, can we rotate this wallet periodically without protocol interaction?hashtag

Yes — it is purely a funding source and can be rotated periodically without protocol interaction. This operation is done off-chain.

chevron-rightFor CLI users, is it tied to an IP or can we run it again on a new server if it goes down for any reason?hashtag

CLI v1 was built without dependencies on Pye’s backend.

There are no IP restrictions other than:

  • The RPC node being used

  • Jito’s MEV API (currently no IP restrictions)

There is ongoing exploration into using durable nonces to mitigate double-spend risks if multiple instances are run simultaneously.

chevron-rightFor CLI users of CLI V1, will migrating to CLI V2 break the V1 integration?hashtag

No, it won’t break it. It’s a completely separate code base and you can continue to use V1 if you so choose.

If you choose to migrate to V2, you will need to schedule a maintenance window in-between epochs to spin down V1 and spin up V2. At the next epoch boundary V2 will make the 1st payment.

Migration to V2 is straightforward, any node operator should have no issues running the commands to spin V2 up. From a state transition perspective, the important thing to understand is that V1 CLI is is winded down during this maintenance window and that the V2 CLI is spun up and will fund the next epoch payments.

chevron-rightFor CLI users, how heavy are the RPC request costs?hashtag

It is efficiently written to reduce RPC calls. The two areas where it uses a decent amount of RPC calls is getting the block metadata where your validator was the leader and polling for the epoch boundary. The concurrency of get_block calls can be limited by the —currency CLI argument. The epoch polling frequency can be adjusted via the —cycle-secs CLI argument.

For a single iteration (one epoch):

Total RPC Calls ≈

I # get_program_accounts (per issuer)

  • (0 or 1) # get_multiple_accounts (if allow_post_maturity)

  • (slots_remaining / cycle_secs) # get_epoch_info during wait

  • 4 # get_block_time, get_vote_accounts, get_leader_schedule, get_account(SlotHistory)

  • B # get_block (based on validator's blocks in epoch)

  • P * (3 to 6) # per pye_account stake fetching

  • P * (1 to 2) # per pye_account inflation reward

  • T * 2 # transfers (T = accounts with excess rewards, if not dry_run)

Typical Example for a validator with:

  • 1 issuer

  • 500 blocks produced in the epoch

  • 5 active pye accounts (no transient accounts)

  • Not dry_run, all 5 have excess rewards

≈ 1 + 100 + 4 + 500 + (5 * 3) + (5 * 1) + (5 * 2)

≈ 1 + 100 + 4 + 500 + 15 + 5 + 10

≈ 635 RPC calls per iteration

Last updated