# 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](https://github.com/pyefi/pye-cli-2)

**Estimated Setup Time Required:** 15-20 minutes

{% embed url="<https://www.loom.com/share/80e907b740dc4d0e9809314a7a153309>" %}

### 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 Dashboard](https://app.pye.fi/), 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 Setup](about:blank#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:

```bash
# Install deps & Rust
sudo apt-get update && sudo apt-get install -y build-essential pkg-config libssl-dev
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

# Build
git clone https://github.com/pyefi/pye-cli-2.git
cd pye-cli-2
cargo build --release

# Generate Payer Keypair
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"                                                      
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"                                           
solana-keygen new --outfile /home/ubuntu/keypair.json                                                                         
chmod 600 /home/ubuntu/keypair.json                                                   

# Configure
cat > /home/pye/.env <<EOF
PAYER=/home/ubuntu/keypair.json
PYE_API_KEY=your-api-key-here
RPC_URL=https://api.mainnet-beta.solana.com
RUST_LOG=info
EOF

# Run
./target/release/pye-cli validator-lockup-manager

# Setup systemd 
sudo tee /etc/systemd/system/pye-cli.service > /dev/null <<EOF                                                     
[Unit]                                                                                                             
Description=Pye CLI V2 — Validator Lockup Manager
After=network.target                                                                                               
                                                                                                                     
[Service]                                                                                                          
Type=simple                                                                                                        
User=ubuntu                                                                                                        
Group=ubuntu                                                                                                       
WorkingDirectory=/home/ubuntu/pye-cli-2                                                                            
ExecStart=/home/ubuntu/pye-cli-2/target/release/pye-cli validator-lockup-manager                                   
EnvironmentFile=/home/ubuntu/.env                                                                                  
Restart=on-failure                                                                                                 
RestartSec=30                                                                                                      
                                                                                                                     
[Install]                                                                                                          
WantedBy=multi-user.target                                                                                       
EOF
                                                                                                                     
cp ~/pye-cli-2/.env ~/.env                                                                                         
chmod 600 ~/.env                                                                                        
sudo systemctl daemon-reload                                                                                       
sudo systemctl enable pye-cli                                                                                      
sudo systemctl start pye-cli     
```

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

### Installation

#### 1. Install system dependencies (Ubuntu/Debian)

```bash
sudo apt-get update && sudo apt-get install -y build-essential pkg-config libssl-dev
```

> 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

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
```

#### 3. Clone and build

```bash
git clone https://github.com/pyefi/pye-cli-2.git
cd pye-cli-2
cargo build --release
```

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:

```bash
cd pye-cli-2
git pull origin master
cargo build --release
sudo systemctl restart pye-cli
```

#### 4. Verify installation

```bash
./target/release/pye-cli --version
```

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:

```bash
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"                                                      
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"           
```

Generate a new keypair for the payer wallet:

```bash
solana-keygen new --outfile /home/pye/keypair.json
```

Restrict file permissions:

```bash
chmod 600 /home/pye/keypair.json
```

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:

```bash
solana balance /home/pye/keypair.json --url https://api.mainnet-beta.solana.com
```

#### 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 10 epochs of expected reward payouts. As soon as the payer has under 3 epochs left an automated 'low balance' alert email will be sent out. 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. You can top up funds through the Validator dashboard UI or by sending it to the payer keypair directly (obtained during setup).

### 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):

<pre class="language-bash"><code class="lang-bash"><strong>cd ~/pye-cli-2 
</strong><strong>
</strong><strong>cat > /home/pye/.env &#x3C;&#x3C;EOF
</strong>PAYER=/home/ubuntu/keypair.json
PYE_API_KEY=your-api-key-here
RPC_URL=https://api.mainnet-beta.solana.com
RUST_LOG=info
EOF
</code></pre>

The CLI loads `.env` from the current working directory on startup. When running via systemd, use the `EnvironmentFile` directive instead (see [Production Deployment](about:blank#production-deployment)).

### Usage

#### `validator-lockup-manager`

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

```bash
pye-cli validator-lockup-manager \\
  --payer /home/pye/keypair.json \\
  --pye-api-key your-api-key
```

**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:

```bash
sudo tee /etc/systemd/system/pye-cli.service > /dev/null <<EOF
[Unit]
Description=Pye CLI V2 — Validator Lockup Manager
After=network.target

[Service]
Type=simple
User=pye
Group=pye
WorkingDirectory=/home/ubuntu/pye-cli-2
ExecStart=/home/ubuntu/pye-cli-2/target/release/pye-cli validator-lockup-manager
EnvironmentFile=/home/pye/.env
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF
```

Create the environment file:

```bash
sudo tee /home/ubuntu/.env > /dev/null <<EOF
PAYER=/home/ubuntu/keypair.json
PYE_API_KEY=your-api-key-here
RPC_URL=https://api.mainnet-beta.solana.com
RUST_LOG=info
EOF
sudo chmod 600 /home/ubuntu/.env
```

Enable and start:

```bash
sudo systemctl daemon-reload
sudo systemctl enable pye-cli
sudo systemctl start pye-cli
```

#### Monitoring

View live logs:

```bash
journalctl -u pye-cli -f
```

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

```json
{"level":"INFO","fields":{"message":"handle_payments_to_be_sent: Payments: 12"}}
{"level":"INFO","fields":{"message":"Transaction sent successfully: 4xK9...mR2v"}}
{"level":"ERROR","fields":{"message":"PyeApiError: Status Code 401 Message: API Key not found"}}
```

Log entries include:

* Payment fetch results and counts
* Transaction signatures on successful transfers
* Errors encountered during processing

Check service status:

```bash
systemctl status pye-cli
```

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

```
transfer_amount = expected_amount - base_amount
```

* **`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.*

<details>

<summary>Migration Guide</summary>

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:

```bash
journalctl -u pye-cli -f   # watch for cycle completion, then Ctrl+C
sudo systemctl stop pye-cli
sudo systemctl disable pye-cli
```

**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:

```bash
git clone <https://github.com/pyefi/pye-cli-2.git>
cd pye-cli-2
cargo build --release
```

**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:

```bash
# V1 — remove these
RPC=https://api.mainnet-beta.solana.com
PAYER=/home/pye/keypair.json
VOTE_PUBKEY=YourVoteAccountPubkey
ISSUERS=Issuer1,Issuer2
CONCURRENCY=50
BLOCK_RETRY_DELAY=1800
DRY_RUN=false
```

V2 environment:

```bash
# V2 — replace with these
RPC_URL=https://api.mainnet-beta.solana.com
PAYER=/home/pye/keypair.json
PYE_API_KEY=your-api-key-here
RUST_LOG=info
```

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:

```bash
sudo tee /etc/systemd/system/pye-cli.service > /dev/null <<EOF
[Unit]
Description=Pye CLI V2 — Validator Lockup Manager
After=network.target

[Service]
Type=simple
User=pye
Group=pye
WorkingDirectory=/home/pye/pye-cli-2
ExecStart=/home/pye/pye-cli-2/target/release/pye-cli validator-lockup-manager
EnvironmentFile=/home/pye/.env
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF
```

**6. Start V2**

```bash
sudo systemctl daemon-reload
sudo systemctl enable pye-cli
sudo systemctl start pye-cli
journalctl -u pye-cli -f   # verify it starts cleanly
```

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

</details>

### Troubleshooting & FAQ

<details>

<summary><strong>CLI exits with <code>ReadKeypairError</code></strong> </summary>

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

</details>

<details>

<summary><strong>Build fails or takes too long</strong></summary>

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:

```bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
cargo build --release
```

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:

```bash
sudo swapoff /swapfile
sudo rm /swapfile
```

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.

</details>

<details>

<summary><strong>Are there risks of duplicate transfers?</strong></summary>

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

</details>

<details>

<summary><strong><code>SolanaClientError</code> or RPC errors</strong></summary>

&#x20;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.

</details>

<details>

<summary><strong>Transfers are not being made</strong></summary>

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.

</details>

<details>

<summary><strong><code>ReqwestError: error sending request for url</code></strong></summary>

&#x20;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.

</details>

<details>

<summary><strong><code>PyeApiError</code> with “API Key not found”</strong></summary>

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.

</details>

<details>

<summary>For CLI users, are there any other consequence if the node is down outside epoch changes?</summary>

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.

</details>

<details>

<summary>For CLI users, how many epochs should avoid being missed? What happens to rewards for missed epochs?</summary>

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.

</details>

<details>

<summary>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.</summary>

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

</details>

<details>

<summary>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?</summary>

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

</details>

<details>

<summary>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?</summary>

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.

</details>

<details>

<summary>For CLI users of CLI V1, will migrating to CLI V2 break the V1 integration?</summary>

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.

</details>

<details>

<summary>For CLI users, how heavy are the RPC request costs?</summary>

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

</details>
