blog·2026-03-12·4 min read

Project of the Week: Build a Quota Ledger on Aleo with leo devnode

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.

bash
leo --version

If your local toolchain is old, update it before copying anything from this post.

Step by step

Generate the boilerplate.

bash
leo new quota_ledger
cd quota_ledger

Open program.json. Your manifest controls the program identity. Replace the default content with this.

json
{
  "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.

leo
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.

bash
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.

bash
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.

bash
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.

bash
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.

bash
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.

Sources