Testing stateful applications on Aleo used to be miserable. You had to spin up a full validator node. Proof generation bogged down your machine for every minor logic change. I hated it.
ProvableHQ finally fixed a lot of that pain. The new leo devnode gives you a lightweight local client. Pair it with --skip-execution-proof, and state changes land fast enough that local iteration feels normal again.
What we're building
We need a system that tracks API allowances for users. Our Quota Ledger uses an AdminTicket record to authorize changes, and it stores balances in a public mapping.
I am keeping this example tight on purpose. The mapping is the part that matters, and I would rather show code that actually builds than pad the file with a singleton we do not need yet.
Current Leo syntax also matters here. Use transition and async transition entry points, and put mapping writes in a paired async function. Do not use a constructor here. That is exactly what broke the original example.
Prerequisites
You need a recent Leo release with leo devnode and --skip-execution-proof. Check your version first.
leo --version
If your local toolchain is old, update it before copying anything from this post.
Step by step
Generate the boilerplate.
leo new quota_ledger
cd quota_ledger
Open program.json. Your manifest controls the program identity. Replace the default content with this.
{
"program": "quota_ledger.aleo",
"version": "0.1.0",
"description": "A ledger for managing user quotas",
"license": "MIT"
}
Now open src/main.leo. Delete everything. Paste this version.
program quota_ledger.aleo {
mapping user_quotas: address => u64;
record AdminTicket {
owner: address,
}
transition mint_admin(public receiver: address) -> AdminTicket {
return AdminTicket {
owner: receiver,
};
}
async transition grant_quota(
ticket: AdminTicket,
public user: address,
public amount: u64,
) -> (AdminTicket, Future) {
assert_eq(ticket.owner, self.caller);
let returned_ticket: AdminTicket = AdminTicket {
owner: ticket.owner,
};
return (returned_ticket, finalize_grant_quota(user, amount));
}
async function finalize_grant_quota(user: address, amount: u64) {
let current: u64 = Mapping::get_or_use(user_quotas, user, 0u64);
Mapping::set(user_quotas, user, current + amount);
}
}
The grant_quota path does two separate jobs. The transition consumes the AdminTicket and returns a fresh one to the same owner, while the paired async function performs the public mapping update on-chain.
That split is the current pattern you want to remember. If you try to put mapping writes in a plain transition, or if you reach for a constructor, Leo will push back.
Run the build command now.
leo build
If you copied the file exactly, it should compile cleanly.
Testing with leo devnode
Open a new terminal window and start the local node.
leo devnode start
It will listen on localhost:3030. Leave that terminal alone.
Back in your project terminal, mint the admin ticket. Use --skip-execution-proof so you can test logic without waiting on proof generation every single time.
leo execute mint_admin aleo1youraddress... --broadcast http://localhost:3030 --skip-execution-proof
Replace aleo1youraddress... with your actual Aleo address. Save the returned AdminTicket record from the command output.
Next, grant 500 quota points to a user. Pass the ticket record exactly as it was printed.
leo execute grant_quota "{ owner: aleo1youraddress..., _nonce: 0group }" aleo1useraddress... 500u64 --broadcast http://localhost:3030 --skip-execution-proof
Now query the mapping from the local node.
leo query program quota_ledger.aleo --mapping-value user_quotas aleo1useraddress... --node http://localhost:3030
You should get back 500u64.
What's next
This is the first Aleo local workflow I have used that does not feel like punishment. You can change logic, rebuild, execute, and inspect state without dragging a full proving loop behind every tiny edit.
From here, add revocation, quota burns, or rate-window resets. Just keep the same rule in your head: records move in transitions, public state changes land in paired async functions.