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-2
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:
Fetches pending reward payments for your organization from the Pye backend
Calculates the excess owed to each staker (what they’re entitled to minus what they already received)
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
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 Dashboard, 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.microwith 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 Setup)
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.microthis 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
PAYER
--payer
Absolute path to the payer keypair JSON file
PYE_API_KEY
--pye-api-key
Your organization’s Pye API key
Optional
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 Deployment).
Usage
validator-lockup-manager
validator-lockup-managerRuns continuously as a daemon, polling the Pye API and automatically distributing excess rewards.
Behavior:
Polls the Pye backend API every
-cycle-secsseconds (default: 60) to check for pending reward paymentsThe 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 termsbase_amount= what the staker already received through normal Solana staking mechanicsIf 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.
Migration Guide
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
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
CLI exits with ReadKeypairError
The payer keypair file path is invalid or the file is not readable. Verify the path and file permissions (chmod 600).
Build fails or takes too long
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.
Are there risks of duplicate transfers?
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.
SolanaClientError or RPC errors
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.
Transfers are not being made
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.
ReqwestError: error sending request for url
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.
PyeApiError with “API Key not found”
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.
For CLI users, are there any other consequence if the node is down outside epoch changes?
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.
For CLI users, how many epochs should avoid being missed? What happens to rewards for missed epochs?
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.
For 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.
There are two commands in CLI v1 (v0.1.3):
validator-pye-account-managerLong-running process
Waits for the next epoch boundary
If started late, it does not backfill missed epochs
transfer-excess-rewardsOne-shot command
If run in epoch
N, it calculates rewards for epochN-1and 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).
For 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?
Yes — it is purely a funding source and can be rotated periodically without protocol interaction. This operation is done off-chain.
For 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?
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.
For CLI users of CLI V1, will migrating to CLI V2 break the V1 integration?
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.
For CLI users, how heavy are the RPC request costs?
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