main branch of the cosmos/example repo first:
x/counter module example in main follows the same pattern and adds several features on top.
This walkthrough is meant to show you exactly what each feature is, what it does, and how you can add a similar feature to any module.
Minimal vs full counter
The full counter in themain branch adds quite a bit of functionality to the minimal tutorial counter.
| Feature | minimal x/counter | full x/counter |
|---|---|---|
| State | count | count + params |
| Messages | Add | Add + UpdateParams |
| Queries | Count | Count + Params |
| Validation | None | MaxAddValue limit, overflow check |
| Fees | None | AddCost charged via bank module |
| Authority | None | Governance-gated param updates |
| Errors | Generic | Named sentinel errors |
| Telemetry | None | OpenTelemetry counter metric |
| CLI | AutoCLI | AutoCLI + EnhanceCustomCommand |
| Simulation | None | simsx weighted operations |
| Block hooks | None | BeginBlock + EndBlock |
| Unit tests | None | Full keeper/msg/query test suite |
msg_server.go, query_server.go, module.go, and types/ is structurally similar between the two. Much of the new keeper logic lives in a single method: AddCount in keeper.go.
Params and authority
A module param is on-chain configuration that controls how the module behaves without changing the code. The full counter adds aParams type that lets the chain governance configure the module’s behavior at runtime. In the full module, params control how large an Add can be and how much it costs.
Where the code lives
proto/example/counter/v1/state.protodefines theParamstypeproto/example/counter/v1/tx.protoadds theUpdateParamsmessageproto/example/counter/v1/query.protoadds theParamsqueryx/counter/keeper/keeper.gostores the params and authorityx/counter/keeper/msg_server.gochecks the authority on updatesx/counter/keeper/query_server.goreturns the current params
Try it
You can inspect the current params with:Add this to your module
To add runtime-configurable params to your own module, make these changes:- Define a
Paramstype in proto - Add a privileged
UpdateParamsmessage - Add a query to read the current params
- Store the params and authority in your keeper
- Check the authority in
MsgServerbefore writing new params
state.proto
The relevant addition instate.proto is:
MaxAddValue caps how much a single Add call can increment the counter. AddCost sets an optional fee charged for each add operation.
tx.proto - UpdateParams
The relevant addition intx.proto is:
UpdateParams is a privileged message. Only the authority address can call it. By default that address is the governance module account, so params can only be changed through a governance proposal.
query.proto - Params
query.proto adds a second query to expose the current params:
The authority pattern
The keeper stores the authority address and checks it on everyUpdateParams call:
MsgServer, is the standard Cosmos SDK approach to governance-gated configuration.
Expected keepers and fee collection
This section shows the standard Cosmos SDK pattern for module-to-module interaction.x/counter uses an expected keeper to call into the bank module and charge a fee for each add operation.
Where the code lives
x/counter/types/expected_keepers.godefines the narrow bank keeper interfacex/counter/keeper/keeper.gostores the bank keeper dependency and charges the fee inAddCountapp.gopassesapp.BankKeeperintocounterkeeper.NewKeeperapp.goadds a module account entry so the counter module can receive fees
app.go changes
This feature requires twoapp.go changes:
- add
countertypes.ModuleName: niltomaccPerms - pass
app.BankKeeperintocounterkeeper.NewKeeper(...)
app.go, those changes look like this:
Try it
Submit an add transaction and the configuredAddCost fee will be charged from the sender:
Add this to your module
To add fee collection through the bank module, make these changes:- Define a narrow bank keeper interface in
types/expected_keepers.go - Add a
bankKeeperfield to your keeper - Charge the fee inside your keeper business logic
- Add a module account entry in
maccPerms - Pass
app.BankKeeperinto your keeper constructor inapp.go
expected_keepers.go
Rather than importing the bank module directly, the counter module defines the minimal interface it needs:Keeper struct
Fee charging in AddCount
AddCount. The MsgServer stays thin:
AddCount is a named keeper method, it can also be called from BeginBlock, governance hooks, or other modules, not just from the MsgServer.
Module accounts
A module account is an on-chain account owned by a module instead of a user. Modules use module accounts to hold funds, receive fees, or get special permissions like minting or burning. Becausex/counter receives fees from users, it needs a module account entry in app.go:
maccPerms map in app.go. Here, nil means the module account can receive funds but does not get extra permissions like minting or burning.
Sentinel errors
Rather than returning generic errors,x/counter defines named sentinel errors with registered codes. That makes failures easier to understand and easier for clients to match on programmatically.
Where the code lives
x/counter/keeper/errors.godefines the registered module errorsx/counter/keeper/keeper.goreturns those errors from business logic checks
Telemetry
Telemetry records how often the counter is updated so you can observe module activity in an OpenTelemetry-compatible system.Where the code lives
x/counter/keeper/telemetry.godefines the meter and counter metricx/counter/keeper/keeper.gorecords the metric fromAddCount
countMetric.Add(ctx, int64(amount)) in AddCount increments an OpenTelemetry counter every time the module state is updated. This makes module activity visible in any OTel-compatible observability system.
AutoCLI
AutoCLI exposes the module’s queries and transactions as CLI commands. The full module example keeps the same basic AutoCLI setup as the minimal module and adds the recommended setting for custom command integration.Where the code lives
x/counter/autocli.godefines the generated query and tx commands
Try it
These commands come from the AutoCLI configuration.count and add are customized explicitly in autocli.go, and params is still available from the generated query service.
x/counter sets EnhanceCustomCommand: true, which merges any hand-written CLI commands with the auto-generated ones. Since neither module has hand-written commands, it is a no-op here, but it is a good default for fuller modules.
The autocli.go file in x/counter:
Simulation
Simulation lets the SDK generate randomized transactions against the module during fuzz-style testing.Where the code lives
x/counter/simulation/msg_factory.godefines how to generate randomAddmessagesx/counter/module.goregisters those weighted operations
Test it
You can exercise simulation through the repo’s simulation test targets described in the running and testing tutorial.x/counter implements simsx-based simulation, which lets the SDK’s simulation framework generate random Add transactions during fuzz testing:
module.go registers this factory:
BeginBlock and EndBlock
These hooks let a module run code automatically at the start or end of every block. Inx/counter, they are purposefully empty to demonstrate where and how these features can be added.
Where the code lives
x/counter/module.goimplementsBeginBlockandEndBlockapp.goadds the module toSetOrderBeginBlockersandSetOrderEndBlockers
app.go changes
Because the module advertises block hooks,app.go must include countertypes.ModuleName in both blocker order lists.
Add this to your module
To add begin and end blockers to your own module, make two changes:- Implement the hooks in
x/<module>/module.go - Add your module name to
SetOrderBeginBlockersandSetOrderEndBlockersinapp.go
module.go implements HasBeginBlocker and HasEndBlocker:
app.go, the module is added to the blocker order lists like this:
x/counter has no per-block logic, so both methods return nil. They exist to demonstrate the pattern: modules that need per-block execution (staking, distribution) implement real logic here. For example, a counter that auto-increments every block would call k.AddCount(ctx, 1) from BeginBlock instead of exposing a message type.
Unit tests
The full module example includes a real test suite for keeper logic, query behavior, message handling, and bank keeper interactions.Where the code lives
x/counter/keeper/keeper_test.gox/counter/keeper/msg_server_test.gox/counter/keeper/query_server_test.go
Run them
You can run the counter module tests directly with:Add this to your module
Start with keeper, message server, and query server tests. If your module depends on another keeper, use a small mock interface likeMockBankKeeper so you can control success and failure cases in isolation.
x/counter ships a full test suite in x/counter/keeper/:
| File | What it tests |
|---|---|
keeper_test.go | KeeperTestSuite setup, InitGenesis, ExportGenesis, GetCount, AddCount, SetParams |
msg_server_test.go | MsgAdd, event emission, MsgUpdateParams |
query_server_test.go | QueryCount, QueryParams |
KeeperTestSuite struct defined in keeper_test.go, which sets up an isolated in-memory store, a mock bank keeper, and a real keeper instance:
MockBankKeeper lets tests control exactly what the bank keeper returns without needing a real bank module:
SendCoinsFromAccountToModuleFn to simulate success or failure:
Gas
minimum-gas-prices in app.toml sets the minimum fee a node requires before it will accept and relay a transaction. The local dev chain started by make start leaves this empty, so transactions are accepted with no fee beyond the AddCost module parameter.
To require a minimum network fee, set it in app.toml: