Auto-Compound Validator Rewards With authz

Set up a secure, keyless daily compounder that withdraws your validator's rewards and commission and restakes them back to your own validator — without ever exposing your main validator key. Uses the Cosmos SDK authz module, which was designed exactly for this pattern.

The base setup restakes 100% of rewards. An optional section further down shows how to split off a percentage to a treasury address — useful for DAO funding, LP-seed accumulators, or a personal cold-storage sink. Most operators want the base 100% restake.

Last updated: 2026-07-10

Test before scheduling This guide automates real on-chain transactions from your validator wallet. Before enabling any cron job, run the compound script manually at least twice and verify each transaction lands as expected. Verify all authz grants are typed correctly and locked to your own addresses (Step 3). A misconfigured grant can silently send funds to the wrong destination — the grants themselves are what protect you, so double-check them.

If you have testnet access, run the whole flow there first. If not, run the live version manually a few times and check the resulting delegation and treasury balances match what the script logged before letting cron take over.

Why This Is Safe

The compounder is a dedicated hot wallet with strictly limited on-chain permissions granted via authz. Your main validator key is never touched by any automated script, and the grants are revocable at any time.

This is the same mechanism REStake.app uses across the Cosmos ecosystem. The authz module was designed exactly for trusted delegation without key sharing.

Four Addresses You Will Use

Keep these handy. Every placeholder in the commands below refers to one of these four addresses.

Before You Start

The commands below assume the standard Infinite Drive node under the drive.sh service wrapper. Bare-metal setups running infinited directly on the host can strip the ./drive.sh exec infinite bash -lc '…' wrapper and run the inner command directly.

Step 1 — Create the Compounder Wallet

This is a dedicated hot wallet used only to execute authorized actions. It uses keyring-backend test so scripts can run without a passphrase prompt. It will never hold meaningful funds — only a small gas reserve.

Run inside your validator container:

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited keys add compounder \
  --keyring-backend test \
  --home ~/.infinited'
Save the mnemonic Store the mnemonic output in a secure location. Note the address shown — this is your YOUR_COMPOUNDER_ADDRESS. You will need it in Steps 3 and 4.

Step 2 — Fund the Compounder Wallet

Send 2–5 DROP from your validator wallet to the compounder address to cover gas fees. Top it up when it runs low — check the balance with infinited q bank balances YOUR_COMPOUNDER_ADDRESS.

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx bank send \
  YOUR_VALIDATOR_COSMOS_ADDRESS \
  YOUR_COMPOUNDER_ADDRESS \
  2000000000000000000drop \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

The value 2000000000000000000drop is 2 DROP in base units. Adjust upward if you want a bigger buffer.

Step 3 — Grant authz Permissions

Run each command from your validator node using your main validator key (you will be prompted for the keyring passphrase). These grants allow the compounder wallet to act on your behalf for the specific actions listed — nothing else.

For the base 100% restake setup, you only need Grants 1, 2, and 3. Grant 4 is only required if you want to enable the optional treasury split further down.

Grant 1 — Withdraw staking rewards

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz grant YOUR_COMPOUNDER_ADDRESS \
  generic \
  --msg-type /cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Grant 2 — Withdraw validator commission

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz grant YOUR_COMPOUNDER_ADDRESS \
  generic \
  --msg-type /cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Grant 3 — Delegate, locked to your validator

Use the typed delegate authorization with --allowed-validators to lock the compounder to restake only to your own validator. A generic MsgDelegate grant would allow a compromised compounder key to silently redirect future rewards to any other validator. Do not use the generic form here.

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz grant YOUR_COMPOUNDER_ADDRESS \
  delegate \
  --allowed-validators YOUR_VALIDATOR_VALOPER_ADDRESS \
  --spend-limit 1000000000000000000000000drop \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Grant 4 (optional) — Send to treasury, locked to one address

Skip this grant if you are running the base 100% restake setup. Only run it if you want to enable the optional treasury split described later in this guide.

The send grant with --allow-list restricts the compounder to send funds only to your pre-approved treasury address. Even if the compounder key were compromised, funds cannot be redirected elsewhere.

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz grant YOUR_COMPOUNDER_ADDRESS \
  send \
  --allow-list YOUR_TREASURY_ADDRESS \
  --spend-limit 100000000000000000000drop \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'
Verify grants before running the script Check the active grants for your compounder:

infinited q authz grants-by-grantee YOUR_COMPOUNDER_ADDRESS --home ~/.infinited

The delegate grant should show AUTHORIZATION_TYPE_DELEGATE with an allow_list containing only your valoper address — not GenericAuthorization. If you added Grant 4, the send grant should show SendAuthorization with an allow_list containing only your treasury address.

Step 4 — Write the Compound Script

Write this script to the persistent-data directory of your validator container. With the standard drive.sh setup, that directory maps to ~/.infinited/ inside the container.

The script defaults to 100% restake. To enable the optional treasury split, edit the two TREASURY_* variables at the top — instructions in the next section.

Replace the two highlighted validator addresses at the top before pasting.

cat > ~/drive/services/node0-infinite/persistent-data/compound.sh << 'SCRIPT'
#!/bin/bash
# Auto-compound: withdraw rewards + commission, restake to your own validator.
# Optional: split off a percentage to a treasury address before restaking.
# Uses authz grants — main validator key is never touched.
set -euo pipefail

VALIDATOR="YOUR_VALIDATOR_COSMOS_ADDRESS"
VALOPER="YOUR_VALIDATOR_VALOPER_ADDRESS"

# ── Treasury split (optional) ────────────────────────────────
# Leave both defaults for the standard 100% restake setup.
# Set both to enable a split — see the "Optional: split rewards to a treasury"
# section of the guide for how to configure and what Grant 4 you need first.
TREASURY_ADDR=""              # Cosmos address to receive the split; empty = disabled
TREASURY_PERCENT=0            # Percentage 1-99 sent to treasury; 0 = disabled

# ── Chain config (leave as-is for Infinite Drive) ─────────────
CHAIN_ID="infinite_421018-1"
GAS_PRICES="5000000000drop"
GAS_RESERVE="5000000000000000000"   # 5 DROP kept in validator wallet for gas
TX_TIMEOUT_ATTEMPTS=30              # 30 * 3s = ~90s max wait per tx

wait_for_tx() {
  local txhash="$1"
  local i code out
  for i in $(seq 1 $TX_TIMEOUT_ATTEMPTS); do
    sleep 3
    out=$(infinited q tx "$txhash" --home ~/.infinited --output json 2>/dev/null || true)
    if [ -n "$out" ]; then
      code=$(echo "$out" | jq -r '.code // 0')
      if [ "$code" = "0" ]; then return 0; fi
      echo "ERROR: tx $txhash failed on-chain, code $code" >&2
      echo "$out" | jq -r '.raw_log // "no log"' >&2
      return 1
    fi
  done
  echo "ERROR: tx $txhash not included after $((TX_TIMEOUT_ATTEMPTS * 3))s" >&2
  return 1
}

submit_and_wait() {
  local json_path="$1"
  local out txhash
  out=$(infinited tx authz exec "$json_path" \
    --from compounder \
    --chain-id "$CHAIN_ID" \
    --keyring-backend test \
    --home ~/.infinited \
    --gas auto --gas-adjustment 1.3 --gas-prices "$GAS_PRICES" \
    -y --output json)
  txhash=$(echo "$out" | jq -r '.txhash // empty')
  if [ -z "$txhash" ]; then
    echo "ERROR: submission returned no txhash" >&2
    echo "$out" >&2
    return 1
  fi
  echo "  submitted $txhash — waiting for inclusion..."
  wait_for_tx "$txhash"
}

echo "[1/2] withdraw rewards + commission"
cat > /tmp/withdraw.json << JSON
{
  "body": {
    "messages": [
      {
        "@type": "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
        "delegator_address": "${VALIDATOR}",
        "validator_address": "${VALOPER}"
      },
      {
        "@type": "/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission",
        "validator_address": "${VALOPER}"
      }
    ]
  }
}
JSON
submit_and_wait /tmp/withdraw.json

BALANCE=$(infinited q bank balances "$VALIDATOR" --home ~/.infinited --output json \
  | jq -r '.balances[]? | select(.denom=="drop") | .amount')
BALANCE=${BALANCE:-0}
echo "Balance: ${BALANCE} drop"

AVAILABLE=$(echo "${BALANCE} - ${GAS_RESERVE}" | bc)
if [ "$(echo "${AVAILABLE} < 1" | bc)" -eq 1 ]; then
  echo "Balance below gas reserve, skipping"
  exit 0
fi

# ── Split (optional) or restake all ────────────────────────────
if [ -n "$TREASURY_ADDR" ] && [ "$TREASURY_PERCENT" -gt 0 ]; then
  TREASURY_AMT=$(echo "${AVAILABLE} * ${TREASURY_PERCENT} / 100" | bc)
  STAKE_AMT=$(echo "${AVAILABLE} - ${TREASURY_AMT}" | bc)
  echo "Treasury (${TREASURY_PERCENT}%): ${TREASURY_AMT} drop"
  STAKE_PERCENT=$((100 - TREASURY_PERCENT))
  echo "Staking  (${STAKE_PERCENT}%): ${STAKE_AMT} drop"

  echo "[send] to treasury"
  cat > /tmp/send.json << JSON
{
  "body": {
    "messages": [
      {
        "@type": "/cosmos.bank.v1beta1.MsgSend",
        "from_address": "${VALIDATOR}",
        "to_address": "${TREASURY_ADDR}",
        "amount": [{"denom": "drop", "amount": "${TREASURY_AMT}"}]
      }
    ]
  }
}
JSON
  submit_and_wait /tmp/send.json
else
  STAKE_AMT="${AVAILABLE}"
  echo "Restaking 100%: ${STAKE_AMT} drop"
fi

echo "[2/2] delegate"
cat > /tmp/delegate.json << JSON
{
  "body": {
    "messages": [
      {
        "@type": "/cosmos.staking.v1beta1.MsgDelegate",
        "delegator_address": "${VALIDATOR}",
        "validator_address": "${VALOPER}",
        "amount": {"denom": "drop", "amount": "${STAKE_AMT}"}
      }
    ]
  }
}
JSON
submit_and_wait /tmp/delegate.json

echo "compound complete"
SCRIPT
chmod +x ~/drive/services/node0-infinite/persistent-data/compound.sh

The GAS_RESERVE keeps 5 DROP in the validator wallet at all times — the script never restakes below that floor, so gas for future manual transactions is always available.

Optional — Split Rewards to a Treasury

Skip this section entirely if you want the standard 100% restake. The base setup above is complete and safe as-is.

Enable this if you have a specific reason to route part of your rewards elsewhere. Common uses:

Setup — two changes only

If you added Grant 4 in Step 3, you already have the on-chain permission. The remaining change is editing the two variables at the top of compound.sh:

# Default — 100% restake
TREASURY_ADDR=""
TREASURY_PERCENT=0

# Example — send 30% to the treasury, restake the other 70%
TREASURY_ADDR="YOUR_TREASURY_ADDRESS"
TREASURY_PERCENT=30

Any value 1–99 is valid. The remainder is always restaked. Common ratios: 90/10, 80/20, 70/30, 50/50.

Save the file and re-run the manual test in Step 5. The script log will now show a [send] to treasury step between the withdraw and the delegate.

Changing your mind later To go back to 100% restake, set TREASURY_PERCENT=0 — the script will skip the send step even if Grant 4 is still active. No grant changes needed. The unused send grant is inert on its own; if you want it removed too, revoke it:

infinited tx authz revoke YOUR_COMPOUNDER_ADDRESS /cosmos.bank.v1beta1.MsgSend --from my-validator --chain-id infinite_421018-1 --keyring-backend file --home ~/.infinited --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y

Step 5 — Test the Script Manually

Run at least two manual tests before scheduling the cron. Each transaction is submitted and waited on individually — no passphrase prompt should appear. A successful run ends with compound complete.

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash ~/.infinited/compound.sh

Expected output for a 100% restake run:

[1/2] withdraw rewards + commission
  submitted ABC123… — waiting for inclusion...
Balance: 5076384885786055869 drop
Restaking 100%: 5071384885786055869 drop
[2/2] delegate
  submitted 789ABC… — waiting for inclusion...
compound complete

Or if the optional split is enabled:

[1/2] withdraw rewards + commission
  submitted ABC123… — waiting for inclusion...
Balance: 5076384885786055869 drop
Treasury (30%): 1521415465735816760 drop
Staking  (70%): 3549969420050239109 drop
[send] to treasury
  submitted DEF456… — waiting for inclusion...
[2/2] delegate
  submitted 789ABC… — waiting for inclusion...
compound complete

After a successful test, spot-check on the chain: look at your delegation on Validator Insights or DriveStats — it should have grown by the restake amount. If you used the split, verify the treasury address received the expected amount.

Step 6 — Schedule With Cron

Add a cron job on the host machine (not inside the container). The example below runs at 08:00 daily and logs output to ~/compound.log.

Check server timezone first Cron uses the server's local time, not necessarily UTC. Verify with timedatectl — it should show Time zone: Etc/UTC. If not, adjust the cron hour to match your server's offset from UTC.
(crontab -l 2>/dev/null; echo "0 8 * * * \
  cd /home/ubuntu/drive/services/node0-infinite && \
  ./drive.sh exec infinite bash ~/.infinited/compound.sh \
  >> /home/ubuntu/compound.log 2>&1") | crontab -

Verify the job is registered:

crontab -l

Monitor past runs:

tail -f ~/compound.log
Keep the compounder funded The compounder wallet needs ~5 DROP to pay gas. Check its balance periodically and top it up as needed. If it runs empty the cron will exit non-zero — check compound.log if you suspect a missed run.

Revoking Grants

Grants can be revoked individually or all at once. Revocation is instant and takes effect immediately for any future authz exec — past transactions are unaffected.

Show revoke commands

Run each revoke from your validator node using your main validator key. Substitute the message type in the last positional argument for whichever grant you want to remove.

Revoke a single grant

Withdraw rewards:

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz revoke YOUR_COMPOUNDER_ADDRESS \
  /cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Withdraw commission:

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz revoke YOUR_COMPOUNDER_ADDRESS \
  /cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Delegate (restake):

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz revoke YOUR_COMPOUNDER_ADDRESS \
  /cosmos.staking.v1beta1.MsgDelegate \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Send to treasury (only if you added Grant 4):

cd ~/drive/services/node0-infinite

./drive.sh exec infinite bash -lc 'infinited tx authz revoke YOUR_COMPOUNDER_ADDRESS \
  /cosmos.bank.v1beta1.MsgSend \
  --from my-validator \
  --chain-id infinite_421018-1 \
  --keyring-backend file \
  --home ~/.infinited \
  --gas auto --gas-adjustment 1.3 --gas-prices 5000000000drop -y'

Fully disable the compounder

To stop auto-compounding entirely, run each of the revoke commands above that apply to your setup (three for the base 100% restake, or four if the treasury split was enabled), then remove the cron job so it doesn't fire and fail every day:

crontab -e

Delete the compound.sh line and save. Verify with crontab -l.

Any leftover DROP in the compounder wallet can be swept back to your validator wallet via a normal bank send from the compounder key (the compounder's own funds are always its own to move — the authz grants only govern actions on behalf of your validator wallet).

Check what is currently granted Before revoking, list the active grants to see exactly what is in place:

infinited q authz grants-by-grantee YOUR_COMPOUNDER_ADDRESS --home ~/.infinited

Summary

A dedicated compounder wallet with tightly-scoped authz grants lets you auto-compound validator rewards daily without exposing your main key. The base setup restakes 100% and needs three grants. Adding a treasury split takes one extra grant plus two edited variables at the top of the script — useful if you have a specific destination for a slice of your rewards (LP-seed, DAO, cold storage), skip it otherwise.

Once the manual tests pass cleanly, schedule with cron and monitor via the log for the first few days. From there it runs itself.