How I Cut Repetitive Validator Ops Costs by 90%
I’m sharing how I built internal web apps on top of the Cosmos SDK authz module to dramatically reduce the cost of repetitive validator…
How I Cut Repetitive Validator Ops Costs by 90%
I’m sharing how I built internal web apps on top of the Cosmos SDK authz module to dramatically reduce the cost of repetitive validator operations.

Background
I’ve been operating 15+ Cosmos ecosystem chains as part of a large validator team. For validator operators, there are two recurring operational tasks:
- Checking newly posted governance proposals and voting
- Periodically claiming rewards

Cosmos validator operations
In practice, those tasks looked like this:
- Get approval to access the Ledger (hardware wallet) and take it out of the vault
- Set up a local environment with the validator client
- Connect the Ledger and connect the wallet
- Write and run the appropriate transaction command in the CLI
- Repeat this across every chain I operate
Governance voting required visiting each chain’s governance page daily to avoid missing proposals. Reward claiming required CLI + Ledger handling, so an engineer had to be involved.
The finance team had to request an engineer whenever claims were needed. Running claims across all chains could take two engineers an entire day, creating significant operational overhead.
The core problems
- Operational bottleneck: Every signing action depended on physical key access. If the key holder was away or Ledger access was blocked, operations stalled.
- Security burden: The more frequently the Ledger was used, the higher the risk of operator-key exposure. Voting and claiming are highly repetitive, so the risk was non-trivial.
- Scaling cost: As the number of chains increased, the same process grew linearly.
All of these came from one root cause:
The signer for all operations had to be the Operator Key (the account that owns the validator and signs on-chain transactions, typically stored in a cold wallet like a Ledger).
If I could change who signs, the entire operational model could change — and Cosmos SDK’s authz module provides exactly that.
The Authz Delegation Model
Cosmos SDK’s authz module lets an account delegate execution authority for specific message types to another account. The core mechanism:
- The Granter (Operator account) grants permission for specific message types to a Grantee (separate account).
- The Grantee wraps delegated messages inside MsgExec and executes them.
- The chain verifies the authz grant and processes the transaction as if the Granter executed it.
[Operator Key (Granter)] ──── authz grant ────► [Grantee Wallet]
│
MsgExec(MsgVote)
│
[Governance Proposal]
In this model, the Operator Key is used only once to create the grant. After that, recurring operations are executed by the Grantee wallet.
Restricting scope by message type
Authz permissions can be constrained at the message type level. I split the system into two purpose-built apps, granting only the minimum needed permissions for each:
- Vote App account: grant only MsgVote
- Claim App account: grant only MsgSetWithdrawAddress, MsgWithdrawValidatorCommission In addition, I fixed the withdraw target address in the web app to a team-managed account.
Even if the Grantee account is compromised, it cannot perform actions outside the granted message types. For claims, the app constrains the withdraw destination to a fixed team-controlled address.
Grant Setup
Both apps use GenericAuthorization to delegate authority per message type. I also set an expiration time to clearly bound how long a grant is valid.
const grant = {
authorization: {
typeUrl: "/cosmos.authz.v1beta1.GenericAuthorization",
value: GenericAuthorization.encode({
msg: "/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission",
}).finish(),
},
expiration: Timestamp.fromPartial({
seconds: BigInt(Math.floor(expiryDate.getTime() / 1000)),
}),
};
Expired grants become invalid automatically. If responsibilities change, I can revoke explicitly — or just let the grant expire.
1. Vote App
1.1 Goals
- Unified governance monitoring: Previously, I had to manually visit governance pages for each chain.
Vote App aggregates all proposals in
voting_periodacross every chain I operate, in one view. - Web-based delegated voting: Once a proposal is reviewed, voting can be executed directly in the web UI — no Ledger, no CLI — signed by the Grantee wallet.
1.2 Configuration-driven architecture
When I onboard a new chain, I don’t modify code. I only add chain metadata to chains.json.
{
"cosmos-hub": {
"name": "Cosmos Hub",
"denom": "uatom",
"decimal": 6,
"rpc": ["https://rpc.cosmos.network"],
"rest": ["https://rest.cosmos.network"],
"chainType": "BaseAccount",
"validatorAddress": "cosmosvaloper1...",
"operatorAddress": "cosmos1...",
"ticker": "ATOM",
"scanner": "https://www.mintscan.io/cosmos/tx/",
"proposalLink": "https://www.mintscan.io/cosmos/proposals/"
}
}
Currently supported chainType values:
BaseAccountEthermintAccountInjectiveAccountInitiaAccount
1.3 Signing strategy by chain type
Cosmos ecosystem chains don’t share a single signing flow. I introduced a router layer that selects the correct execution strategy based on chainType.
// Select a mutation strategy by chain type
const mutation = match(chainType)
.with("BaseAccount", () => useVote())
.with("EthermintAccount", () => useEthermintVote())
.with("InjectiveAccount", () => useEVMVote())
.with("InitiaAccount", () => useInitiaVote())
.exhaustive();
This keeps a stable interface while isolating chain-specific differences internally. When I add a new chain type, it does not affect existing code paths.
1.4 Voting flow on BaseAccount chains
For BaseAccount chains, the voting flow is:
- Pre-check whether the connected wallet has a valid MsgVote authz grant via
useAuthzGrant - Build
MsgVoteusing the Granter (operatorAddress) as the voter - Wrap it with
MsgExecand have the Grantee sign and broadcast
const msgVote = {
typeUrl: "/cosmos.gov.v1beta1.MsgVote",
value: {
proposalId,
voter: operatorAddress, // granter
option: voteOption,
},
};
const msgExec = {
typeUrl: "/cosmos.authz.v1beta1.MsgExec",
value: {
grantee: granteeAddress,
msgs: [Any.fromPartial(msgVote)],
},
};
After a successful transaction, I handle cache invalidation via centrally managed query keys — so it stays explicit which caches get invalidated after each mutation.
1.5 Direct signing for Ethermint / Initia
For Ethermint-based chains and Initia, the standard CosmJS flow doesn’t work as-is. I built a separate direct-sign pipeline.
Common flow:
- Fetch
account_numberandsequencevia REST - Apply the chain-specific pubkey type URL
makeSignDoc → signDirect → raw tx broadcast
Pubkey type URL differences:
- Ethermint:
/ethermint.crypto.v1.ethsecp256k1.PubKey - Initia:
/initia.crypto.v1beta1.ethsecp256k1.PubKey
Both use EVM-compatible key types, but their type URL paths differ. If I use CosmJS SigningStargateClient without overriding this, it can interpret the pubkey type incorrectly and the signature fails. I fixed this by explicitly injecting the correct type URL per chain.
1.6 Execution pipeline for Injective
Injective uses @injectivelabs/sdk-ts.
// Fetch account state
const accountDetails = await new ChainRestAuthApi(restEndpoint)
.fetchAccount(granteeAddress);
// Set timeoutHeight based on the latest block
const latestBlock = await new ChainRestTendermintApi(restEndpoint)
.fetchLatestBlock();
// Build MsgAuthzExec and Keplr signDirect
const { signBytes } = createTransaction({
message: MsgAuthzExec.fromJSON({ grantee, msgs: [msgVote] }),
...accountDetails,
timeoutHeight,
});
1.7 Endpoint reliability
To prevent a single RPC/REST outage from taking the service down, I implemented multi-endpoint support with fallback logic:
- Track health status per endpoint
- Allow operators to manually override or reset a specific endpoint
- Automatically fallback to the next endpoint on request failure
- On app initialization, pre-check all endpoints in the background
Vote App UI
2. Claim App
Claim App reuses the same foundation as Vote App. I reused the chain configuration approach, the chain-type signing strategy, and the endpoint reliability design. Below are the parts that differ.
2.1 Goals
- Unified reward/commission dashboard: Previously, I had to visit explorers chain by chain just to check accumulated commission. Claim App queries chain data directly and shows commission across all chains in one view.
- Claim transaction history: I implemented per-chain claim history, also sourced directly from chain data.
- Operational model change: Before, finance could not realistically execute claims due to CLI + Ledger complexity, so engineers had to do it. After Claim App, finance can execute claims directly from the web UI.
2.2 Claim transaction design: set → withdraw → reset
For BaseAccount claims, I designed the system to execute three messages in a single transaction:
MsgSetWithdrawAddress(claimAddress)→ set withdraw address to the claim receiving accountMsgWithdrawValidatorCommission→ withdraw commissionMsgSetWithdrawAddress(operatorAddress)→ restore withdraw address to the original operator address
If step 3 is skipped, the withdraw address remains changed and can affect later operational flows. By bundling all three messages into a single MsgExec and executing atomically, a successful transaction guarantees the state is restored.

Claim App UI
Results

Operational improvement results
Closing Thoughts
The core of this project wasn’t “building new features” — it was changing the operating model.
By managing execution authority as policy via authz, I reduced operator-key exposure while also lowering the marginal cost of scaling multi-chain operations.
I previously worked on frontend development for Cosmos ecosystem chains. The chain-specific transaction structures and signing flows I learned then were directly reusable here. This project reinforced a key lesson: past domain experience can become decisive leverage when solving a completely different problem later.
Source Code
- Vote App:
[https://github.com/heyapplemango/cosmos-vote-app-with-authz](https://github.com/heyapplemango/cosmos-vote-app-with-authz) - Claim App:
[https://github.com/heyapplemango/cosmos-claim-app-with-authz](https://github.com/heyapplemango/cosmos-claim-app-with-authz)
메타데이터
- post_id
- 0bc65019d31f
- slug
- how-i-cut-repetitive-validator-ops-costs-by-90-0bc65019d31f
- url
- https://medium.com/@heymango/how-i-cut-repetitive-validator-ops-costs-by-90-0bc65019d31f
- canonical_url
- https://medium.com/@heymango/how-i-cut-repetitive-validator-ops-costs-by-90-0bc65019d31f
- author_url
- https://medium.com/@heymango
- status
- ok
- fetched_at
- 2026-06-22 17:31:34