Introduction
Giving an AI agent a compiler is useful. Giving the same agent an unrestricted transaction signer is reckless.
AleoFlow 0.2.0 draws a sensible boundary between those jobs. Its local MCP server exposes project scaffolding, compilation, tests, auditing, queries, local execution, and transaction dry runs. The three tools that can deploy, execute, or send with --broadcast are absent unless the server starts with ALEOFLOW_MCP_ALLOW_BROADCAST=true.
Absent matters. A warning in a tool description asks the model to behave. An omitted tool denies the capability.
Private keys are also excluded from every MCP tool schema. AleoFlow leaves key loading to the underlying Leo process through PRIVATE_KEY or a project .env file. That prevents a model from stuffing a key into a tool call, where it could land in an MCP trace or conversation log.
The boundary is narrower than a full sandbox. If your coding agent also has an unrestricted shell and can read .env, it can still steal whatever is there. Use a fresh, unfunded development account. Do not point a general coding session at a funded production key and expect MCP gating to save you. Lions appreciate fences, but we still check the gate.
What we're building
We will create guarded_payments.aleo, a small payment-note program with two entry points:
preview_paymentcreates an encryptedPaymentrecord without touching public state.create_paymentcreates the same record and increments a public payment counter in afinal { }block.
The receiver, amount, and reference use Leo's default private visibility. Only the aggregate count reaches public storage. An observer can see that another payment was created, but not its recipient or value from this program's state.
AleoFlow's payment template gives us the project shell. We will replace its sample program so every line is visible here and compile it against Leo 4.4 syntax: fn, Final, inline final { }, storage, and a mandatory constructor.
The coding agent will receive these MCP capabilities:
| Tool | Typical schema fields | Authority |
|---|---|---|
aleoflow_init | name, template, optional workspace | Writes local project files |
aleoflow_build | optional path, optional JSON output setting | Compiles locally |
aleoflow_test | optional path, optional JSON output setting | Runs local tests |
aleoflow_audit | path | Reads and checks Leo source |
aleoflow_run | function name, inputs, optional path, network, and endpoint | Runs locally without sending a transaction |
aleoflow_execute_dry_run | function name, inputs, optional path, network, and endpoint | Builds and simulates an execution without --broadcast |
aleoflow_deploy_dry_run | path, optional network and endpoint | Prepares a deployment without sending it |
aleoflow_send_dry_run | recipient, amount, optional network and endpoint | Simulates a credits transfer |
None accepts private_key or a generic bag of CLI flags. That last detail blocks an agent from smuggling --broadcast through a supposedly safe tool.
AleoFlow does not edit source files itself. Your coding agent uses its normal filesystem tools for that, then calls AleoFlow for compiler-backed feedback.
Prerequisites
Install Leo 4.4 and confirm the version before generating anything.
leo --version
Install the exact AleoFlow release used here.
cargo install aleoflow --version 0.2.0
aleoflow --version
aleoflow doctor
You also need an MCP client. The examples use Claude Code, though any client capable of launching a local stdio server can use the same binary.
Keep a development account separate from every funded account. If a dry-run command needs signing context, create an unfunded key locally:
aleoFlow account new --write
If your installed binary is lowercase-only, use the canonical command instead:
aleoflow account new --write
The resulting .env must remain ignored by Git. Check that before inviting an agent into the directory.
git check-ignore .env
Step-by-step
1. Scaffold the project
Create the payment project from AleoFlow's built-in template.
aleoflow init guarded-payments --template payment
cd guarded-payments
AleoFlow preserves the hyphenated folder name and sanitizes the on-chain program identifier to guarded_payments.aleo, since Aleo identifiers cannot contain hyphens.
Replace program.json with the complete manifest below.
{
"program": "guarded_payments.aleo",
"version": "0.1.0",
"description": "Private payment records with a public aggregate counter",
"license": "MIT",
"leo": "4.4.0",
"dependencies": null,
"dev_dependencies": null
}
Pinning the Leo version catches local compiler drift. A different 4.x minor release may parse or lower code differently, so a version warning deserves attention rather than a shrug.
2. Write the payment program
Replace src/main.leo with this complete program.
program guarded_payments.aleo {
@noupgrade
constructor() {}
record Payment {
owner: address,
amount: u64,
reference: field,
}
storage payment_count: u64;
fn preview_payment(
receiver: address,
amount: u64,
reference: field,
) -> Payment {
assert(amount > 0u64);
return Payment {
owner: receiver,
amount: amount,
reference: reference,
};
}
fn create_payment(
receiver: address,
amount: u64,
reference: field,
) -> (Payment, Final) {
assert(amount > 0u64);
let payment: Payment = Payment {
owner: receiver,
amount: amount,
reference: reference,
};
return (
payment,
final {
let current: u64 = payment_count.unwrap_or(0u64);
payment_count = current + 1u64;
},
);
}
}
The @noupgrade constructor fixes the program at edition zero. That is my preferred policy for a tutorial-sized payment primitive because nobody needs an admin key quietly replacing its rules later. A real product may choose @admin or @checksum, but the upgrade authority then becomes part of its security model.
Payment is a record, so Leo encrypts each output for its owner. Record declarations require owner: address; omitting it is a compiler error. The other fields have no public qualifier and are private by default.
payment_count is singleton public storage. A mapping would work, but inventing a dummy key for one counter is needless ceremony. unwrap_or(0u64) handles the undeclared initial value on the first call.
preview_payment is useful for quick local runs. create_payment returns both the private record and a Final handle. Its final block runs on-chain during a real execution and is the only place where the storage assignment is legal.
Notice what the final block does not capture: receiver, amount, or reference. Passing private values into finalization would expose them to public execution. The public counter leaks activity, which is a real tradeoff, but it does not leak the payment payload.
3. Compile before adding AI
Run AleoFlow's wrapper first, then invoke Leo directly so you know the underlying project also works without MCP.
aleoflow build --path .
leo build
Both commands should produce compiled Aleo instructions under build/. AleoFlow delegates compilation to Leo rather than maintaining a second compiler.
Run the private, proof-half preview with a real Aleo address. The address below is the published local development address, not a production identity.
aleoFlow run preview_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field \
--path .
Use the canonical lowercase spelling if required by your shell:
aleoflow run preview_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field \
--path .
Here is the direct Leo equivalent required for a tool-independent check:
leo run preview_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field
leo run evaluates the proof half locally. Use an execution dry run when you want the final block included in the simulation:
aleoflow execute create_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field \
--path .
There is deliberately no --broadcast flag.
4. Add current Leo tests
Create tests/test_guarded_payments.leo.
import guarded_payments.aleo;
@test
fn test_preview_payment() {
let payment: guarded_payments.aleo::Payment =
guarded_payments.aleo::preview_payment(
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px,
125u64,
9field,
);
assert_eq(payment.amount, 125u64);
assert_eq(payment.reference, 9field);
}
@test
@should_fail
fn test_zero_payment_fails() {
let payment: guarded_payments.aleo::Payment =
guarded_payments.aleo::preview_payment(
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px,
0u64,
9field,
);
assert_eq(payment.amount, 0u64);
}
Leo 4.4 tests are top-level @test fn declarations. Imported entry points use the program.aleo::function locator, and imported record types use the same double-colon form. Older test transition examples belong to another compiler generation.
Run the fast test path and then the proving path.
aleoflow test --path .
leo test
leo test --prove
The failing test is intentional. Zero-value records would be valid at the type level, so the assertion in preview_payment supplies the business rule.
5. Audit the source
Run AleoFlow's linter after the compiler tests.
aleoflow audit .
AleoFlow's audit is heuristic. It checks suspicious public record fields, sensitive values written into mappings, shallow private-to-final data flow, and leftover TODO markers. It is useful, especially for catching an accidental finalization leak, but it is not a proof of economic correctness or a substitute for review.
6. Connect the guarded MCP server
Register AleoFlow without setting its broadcast opt-in variable.
which aleoflow
claude mcp add aleoflow -- /absolute/path/to/aleoflow mcp
Restart the client if it does not refresh MCP tools immediately. Ask it to list the AleoFlow tools. You should see the safe and dry-run names, including aleoflow_build, aleoflow_test, aleoflow_audit, aleoflow_run, and aleoflow_execute_dry_run.
You should not see:
aleoflow_deploy_broadcastaleoflow_execute_broadcastaleoflow_send_broadcast
AleoFlow 0.2.0 has dedicated MCP gating tests around this boundary. With the environment flag absent, broadcast tools are omitted from tools/list, and direct calls are refused. With opt-in enabled, the tools appear but still require confirm: true. Safe schemas do not expose a private-key argument or a free-form broadcast flag.
Give the agent a precise task rather than a vague request to handle deployment:
Work only in the guarded-payments project. Use AleoFlow MCP to build, test,
audit, run preview_payment with 125u64 and 9field, then dry-run
create_payment with the same inputs. Fix compiler errors in local files.
Do not request, print, read, or modify any private key. Do not enable or
attempt any broadcast tool. Report raw AleoFlow failures before changing code.
The server returns real command output instead of a model-written summary. That is good architecture. Compiler errors should reach the agent intact, because a friendly paraphrase can remove the exact error code or source location needed for the next edit.
Testing
Run the whole local verification sequence from the project root:
aleoflow build --path .
aleoflow test --path .
aleoflow audit .
aleoflow run preview_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field \
--path .
aleoflow execute create_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
125u64 \
9field \
--path .
Now try the negative case:
aleoflow run preview_payment \
aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px \
0u64 \
9field \
--path .
The run should fail at assert(amount > 0u64). AleoFlow preserves Leo's raw error and may append its best-effort translation. Trust the raw compiler or VM output when the translation disagrees.
Check the MCP boundary separately. Ask the agent to list its AleoFlow tools, then ask whether aleoflow_execute_broadcast exists. Do not set the opt-in flag merely to test the agent's self-control. We are testing capability denial, not manners.
One uncomfortable detail remains: an agent with a general shell could invoke aleoflow execute ... --broadcast directly, bypassing MCP. Disable shell access for the session, apply command allowlists, or run the agent in a container without credentials or network egress. MCP gating controls MCP tools. Nothing more.
What's next
A private payment record is a useful base, though it is not yet money movement. The next version could import credits.aleo, transfer credits, and return the payment record in the same call. Cross-program credit transfers add fee handling and record selection, so I would keep broadcast disabled until those paths have devnode coverage.
For local state iteration, pair this project with Build a Quota Ledger on Aleo with leo devnode. Use the current Leo 4.4 syntax from this tutorial when adapting the workflow.
For more test design, continue with Build a Test-First Allowance Ledger in Leo. Once the program is deployed and a frontend needs to inspect it, Build an ABI-Driven Aleo Transaction Form with the Provable SDK covers the client side.
Keep the broadcast tools off for ordinary coding sessions. When deployment day arrives, use a separate, short-lived session with a low-value account, inspect the dry-run output yourself, and enable only the authority needed for that operation. The agent can write code all week without becoming your wallet.