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
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.
- No main key exposure. The compound script uses the compounder wallet only. Your main validator key never appears in any automated context.
- Locked delegate destination. The
delegategrant uses--allowed-validators, so the compounder can only restake to your own valoper — never redirect to another. - Locked send destination (only relevant if using the treasury split). The
sendgrant uses--allow-list, so the compounder can only send funds to your pre-approved treasury address. - Revocable at any time. All grants can be revoked instantly from your main validator key. The compounder becomes inert with no ability to act on your behalf.
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.
- Validator wallet — your main staking address, keyring alias
my-validator(or whatever you named it), keyring-backendfile. Placeholder:YOUR_VALIDATOR_COSMOS_ADDRESS. - Valoper address — starts with
infinitevaloper1…. Used in delegate + withdraw-commission messages. Placeholder:YOUR_VALIDATOR_VALOPER_ADDRESS. - Compounder wallet — the new hot wallet you create in Step 1. Keyring-backend
test. Holds ~5 DROP for gas only. Placeholder:YOUR_COMPOUNDER_ADDRESS. - Treasury address (optional — only for the split setup) — any Cosmos address you control. Placeholder:
YOUR_TREASURY_ADDRESS.
Before You Start
infinitedinstalled and accessible inside your validator container- SSH access to your validator node
- Your validator is synced and actively signing
- ~5 DROP spare to fund the compounder wallet for gas
bcandjqinstalled in the container (apt-get install -y bc jqif missing)- If using the optional split — a treasury address ready
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' 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' 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:
- LP-seed accumulator — feeding a paired treasury for future liquidity provision. FoxxOne's payment routers work exactly this way (70% ops / 30% LP-seed for the FOXXEY market).
- DAO or project treasury — sinking a portion of validator rewards into a community fund.
- Personal cold storage — auto-sweeping a percentage into a secure long-term wallet so you're not compounding everything.
- Infrastructure sinking fund — covering hosting and monitoring costs in a separate account rather than out of your main balance.
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.
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.
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 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).
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.