Skip to main content

2 posts tagged with "mobile-proving"

View All Tags

From Zero to ZK: Vibe a ZK Mobile App with mopro-ai

· 10 min read
Moven Tsai
Developer on the Mopro Team

TL;DR

  • The problem: Building a mobile app with zero-knowledge (ZK) proofs requires complex environment setup and multiple toolchains. Most developers never get past the setup stage.
  • The tool: mopro-ai is a playbook that lets AI coding agents build ZK mobile apps for you.
  • Demo result: Starting from a single prompt, an AI agent built vibe-app, a Flutter app that generates and verifies ZK proofs entirely on-device.

The Problem: ZK on Mobile Is Still Too Hard

At workshops, the pattern is always the same. Attendees show up excited to build a mobile proof-of-concept. Within the first hour, half the room is stuck on environment setup. Rust is not installed, or the wrong version. CMake is missing. Android NDK paths are misconfigured. Xcode command-line tools point to the wrong directory. The circuit compiles, but the trusted setup artifacts end up in the wrong folder. By the time someone gets to actually calling a prove function from Dart or Swift, the session is over.

The gap between "I want to try ZK on mobile" and "I have a working proof" is surprisingly wide. Here is what stands between a developer and a running mobile ZK app:

  • Environment setup: Rust toolchain (rustup, cargo, cross-compilation targets), platform SDKs (Xcode with command-line tools, Android Studio with NDK and JDK 17+, Flutter SDK), and all the PATH variables that glue them together
  • Circuit knowledge: Circom compiler, snarkjs for trusted setup, circomlib dependencies via npm, and the naming conventions and file structure that make a circuit compile cleanly
  • Mobile knowledge: Native build systems (Gradle, Xcode project settings), FFI wiring (UniFFI bindings, flutter_rust_bridge code generation), and asset bundling for .zkey files
  • Mopro knowledge: The correct sequence of mopro init, mopro build, mopro create, which flags to pass for each platform, and how these commands interact with the layers above

Each layer has its own failure modes and error messages. A developer who is proficient in Flutter but unfamiliar with Rust will hit different walls than one who knows Circom but has never opened Android Studio. The toolchain complexity is the bottleneck, not the ZK concepts themselves.

A ZK Mobile App Playbook for Agents

mopro-ai is not just a plugin. It is a manual with structured knowledge that lets AI agents use mopro accurately and fluently instead of guessing at CLI flags and build sequences.

To install in Claude Code:

/plugin marketplace add zkmopro/mopro-ai
/plugin install mopro
mopro-ai-plugin
Plugin Installation for mopro-ai

The playbook covers the full mopro lifecycle:

CommandWhat it does
/mopro:check-envDiagnose environment and missing tools
/mopro:initInitialize a new mopro project
/mopro:buildBuild ZK bindings (5-15 min, runs in background)
/mopro:createGenerate app template from bindings
/mopro:testRun Rust, FFI, or UI tests
/mopro:deviceManage simulators, emulators, devices
/mopro:newFull workflow: init + build + create

Beyond slash commands, skills also activate automatically based on context. Mention "build for iOS" and the build skill triggers. Say "check my environment" and the env diagnostic runs. The agent does not need to memorize command names. It just needs mopro-ai loaded.

Crucially, mopro-ai includes guardrails that prevent common agent mistakes: never chain build + create in one step (builds take minutes and must be confirmed before proceeding), always pass --platforms flutter for Flutter apps (not --platforms ios, which produces incompatible native-only bindings), always use non-interactive CLI flags to avoid blocking on prompts, and never re-run a build without user confirmation. It also encodes error recovery paths and environment diagnostics.

mopro-ai is built on open standards, specifically Agent Skills for auto-triggered workflows and AGENTS.md for universal agent instructions, so it works across Claude Code, Cursor, VS Code Copilot, Codex CLI, and Gemini CLI.

What We Are Building

The concrete example for this tutorial is vibe-app: a Flutter app that implements challenge-response authentication using zero-knowledge proofs. The user proves they know a secret S such that Poseidon(S, nonce) = expectedHash, without revealing S.

Everything runs on-device. No server-side proving. The "server" in the demo is a mock that provides the challenge (nonce + expected hash), simulating what a real backend would do.

The Prompt That Built vibe-app

Here is the prompt that kicked off the entire project:

"Build a Flutter mobile app that uses mopro for proving knowing a server challenge in Circom. It should use mopro skills."

That is it. One sentence. With mopro-ai loaded, the agent recognized this as a mopro project and produced a 6-step implementation plan:

  1. Initialize a mopro project with Circom adapter
  2. Write the Circom circuit and run trusted setup
  3. Customize the Rust core to register the circuit
  4. Build bindings and create a Flutter app template
  5. Customize the Flutter UI for challenge-response flow
  6. Test end-to-end on a simulator

Each step maps directly to a mopro skill. The agent knew the correct CLI flags, the right file locations, and the build sequence, because mopro-ai told it exactly how mopro works. Without the playbook, the agent would have had to search documentation, guess at flags, and likely hit several dead ends before arriving at a working build. With the playbook, it executed the plan on the first try.

What the Agent Did

Step 1: Initialize the project

mopro init --project_name vibe-app --adapter circom

This scaffolds a Rust project with:

  • Cargo.toml with mopro-ffi, circom-prover, and rust-witness dependencies
  • src/lib.rs with template slots for each proof system
  • test-vectors/ directory for circuit artifacts
  • Config.toml for build configuration

Step 2: Write the circuit and run trusted setup

The agent wrote a Circom circuit implementing Poseidon-based challenge-response:

pragma circom 2.0.0;

include "node_modules/circomlib/circuits/poseidon.circom";

template ChallengeResponse() {
signal input secret; // private
signal input nonce; // public
signal input expectedHash; // public

component hasher = Poseidon(2);
hasher.inputs[0] <== secret;
hasher.inputs[1] <== nonce;

expectedHash === hasher.out;
}

component main {public [nonce, expectedHash]} = ChallengeResponse();

The circuit takes three inputs: a private secret and two public signals (nonce and expectedHash). It computes Poseidon(secret, nonce) and constrains the result to equal expectedHash. A valid proof demonstrates knowledge of the secret without revealing it.

After writing the circuit, the agent compiled it with Circom and ran trusted setup using snarkjs to produce the .zkey and .wasm artifacts, then placed them in test-vectors/circom/.

Step 3: Customize the Rust core

The agent modified src/lib.rs to register the circuit using mopro's macro system:

mod witness {
rust_witness::witness!(challengeresponse);
}

crate::set_circom_circuits! {
("challengeresponse_final.zkey",
circom_prover::witness::WitnessFn::RustWitness(
witness::challengeresponse_witness
)),
}

The rust_witness! macro generates a native Rust witness calculator from the compiled circuit, eliminating the need for WASM-based witness generation at runtime. The set_circom_circuits! macro registers the circuit with its .zkey file so generateCircomProof() knows which proving key to use.

The agent also wrote Rust tests to validate the circuit with known inputs:

#[test]
fn test_challenge_response_valid() {
let circuit_inputs = serde_json::json!({
"secret": "12345",
"nonce": "67890",
"expectedHash": "11344094074881186137859743404234365978119253787583526441303892667757095072923"
}).to_string();

let result = generate_circom_proof(
ZKEY_PATH.to_string(), circuit_inputs, ProofLib::Arkworks
);
assert!(result.is_ok());

let proof = result.unwrap();
let verify = verify_circom_proof(
ZKEY_PATH.to_string(), proof, ProofLib::Arkworks
);
assert!(verify.is_ok());
}

Step 4: Build bindings and create Flutter app

The agent configured Config.toml:

build_mode = "release"
target_adapters = ["circom"]
target_platforms = ["flutter"]
auto_update = false

Then ran the two-step build process:

mopro build --platforms flutter --mode release

This cross-compiles the Rust prover for the target architectures and generates mopro_flutter_bindings/, a Flutter package containing the native libraries and Dart API.

After confirming the build succeeded:

mopro create --framework flutter

This generates a Flutter app template in flutter/ with the bindings already wired up, the .zkey file bundled as an asset, and a working pubspec.yaml.

Step 5: Customize the Flutter UI

The agent built a step-by-step authentication flow in Dart. The key integration points are the proof generation and verification calls:

Generating a proof:

final proofResult = await generateCircomProof(
zkeyPath: zkeyPath,
circuitInputs: '{"secret":"$secret","nonce":"$nonce","expectedHash":"$expectedHash"}',
proofLib: ProofLib.arkworks,
);

Verifying a proof:

final valid = await verifyCircomProof(
zkeyPath: zkeyPath,
proofResult: proofResult,
proofLib: ProofLib.arkworks,
);

These two function calls are the entire ZK integration surface. Everything else (loading the proving key, computing the witness, running the Groth16 prover, serializing the proof) is handled by the mopro bindings underneath.

The UI guides users through four steps: enter a secret, receive a challenge from the (mock) server, generate a ZK proof, and verify it. Each step displays timing information and proof metadata so the user can see exactly what is happening.

The mock server simulates what a real backend would do: given a registered user's secret, it returns a nonce and the pre-computed Poseidon(secret, nonce) hash. In production, the server would never see the secret. It would only store the hash and generate fresh nonces for each authentication attempt. The ZK proof lets the user demonstrate they know the secret that produces the expected hash, without transmitting the secret itself.

Step 6: Test on simulator

The agent started an iOS simulator and ran the app:

cd flutter && flutter run

The flow: enter 12345 as the secret, tap "Request Challenge" to get a nonce and expected hash from the mock server, tap "Generate ZK Proof" to run the Groth16 prover on-device, then tap "Verify Proof" to confirm the proof is valid. Proof generation typically completes in a few seconds on a modern phone or simulator. The secret never leaves the device.

The agent also handled the .zkey asset bundling by copying the proving key into flutter/assets/ and registering it in pubspec.yaml so Flutter can load it at runtime. This is one of those details that is easy to miss manually but that mopro-ai's app skill covers automatically.

What mopro-ai Handled For You

Here is what the developer provided versus what mopro-ai automated:

Developer providedmopro-ai handled
One-sentence prompt describing the appCLI flag selection and build sequencing
Choice of proof system (Circom)Rust project scaffolding and dependency wiring
Choice of platform (Flutter)Cross-compilation for target architectures
Circuit logic (Poseidon challenge-response)FFI binding generation (flutter_rust_bridge)
UI requirements (step-by-step auth flow)Asset bundling (.zkey in Flutter assets)
Macro registration (rust_witness!, set_circom_circuits!)
Build duration management (background execution)
Platform-specific configuration (pubspec.yaml, Gradle)

Without mopro-ai, the agent would have had to guess at mopro's CLI interface, likely passing wrong flags (--platforms ios instead of --platforms flutter), missing the non-interactive flag requirements, or trying to chain build and create in a single step. mopro-ai eliminates that guesswork.

Vibe Your First ZK Mobile App

Building a mobile ZK app no longer requires mastering five different toolchains. With mopro-ai loaded, an AI coding agent can go from a one-sentence description to a working Flutter app with on-device Groth16 proving. It handles the Rust scaffolding, circuit compilation, FFI binding generation, and platform wiring along the way.

The developer's job shifts from fighting build systems to designing circuits and user experiences. That is the gap mopro-ai is designed to close.

Try it yourself: install mopro-ai, describe the ZK app you want to build, and let the agent handle the plumbing.

References

A Developer Experience Retrospective: Integrating MoPro with Anon Aadhaar

· 3 min read
Vikas Rushi
Team Lead, Anon Aadhaar

This is a developer experience blog from the Anon Aadhaar team lead, Vikas integrating MoPro.

For a long time, mobile proving has been tough for developers, especially with zero-knowledge proofs. Developers often spend more time running circuits on mobile than actually building them. A major factor for improving UX will be how quickly we can integrate these proving circuits into mobile apps.

meme

Most mobile proving applications have historically used the Rapidsnark/Witnesscal setup, which is quite messy. It requires a good amount of app development experience, raising questions about how many code changes are needed compared to newer setups like Mopro.

In my past experience, it took me 15 days to figure out how to set up proving circuits on Android. One frustrating part of the Witnesscal/Rapidsnark setup is that you need to write native code for each platform. For binding functions in C++, you still have to write Java for Android and Objective-C for iOS. Recently, React Native has gotten some good updates with Turbo Repo architecture, which I thought could be a solution for this — https://hackmd.io/@0xvikasrushi/BJhNL6g_yl

Integration Time

How long does it take for a developer to integrate and run a circuit on mobile?

  • You still need to write some native code, but it’s minimal. Mopro’s unified Rust bindings are shipped via a Kotlin package, so it’s pretty much plug-and-play. For Android, it’s fairly simple — no need to write any C++ code. :)
  • Developers should focus on writing circuits rather than building an SDK. I was happy to only need to spend 3–4 days putting together a basic SDK that can generate and verify proofs—no need for a fancy frontend.

Code Changes Required

How many files need to be modified? Is the integration minimal or does it require significant changes?

  • Previously, we had a cpp/ folder filled with obscure, hard-to-maintain code — two types of binding, different binaries for witness calculation, and a complicated setup process. Now, that’s all automated, and integration is much smoother.

Cross-Platform Support

Does it require writing platform-specific code (e.g., Java for Android, Objective-C for iOS), or is it abstracted away?

  • Yes, but only a very small amount of platform-specific code is needed.
  • In this particular integration, we were able to go from 700 lines to less than 100 lines of actual code.
  • These ~100 lines are native Kotlin logic used to bind native code to JavaScript, and the implementation is clean and generic enough.

Benchmark Performance

How do Mopro’s binding benchmarks compare to implementations like Anon Aadhaar?

  • There was approximately a ~7% improvement in Execution time
LOG  Mopro Execution time: 3957 milliseconds
LOG Rapidsnark Execution time: 4220 milliseconds

Conclusion

All in all, integrating Mopro with Anon Aadhaar allowed for:

  • Fast proving with latest bindings + RapidSnark: Uses modern proving setups under the hood, so things run fast and smooth.
  • Easy to plug in, great dev experience: Just works out of the box. We didn’t have to fight the SDK—focus stayed on writing circuits.
  • Cross-platform code: Local, browser, or cloud—same code works across all platforms.
  • Less platform-specific code: Code is clean and portable—no hacks or infra-specific tweaks needed.
  • Future scaling: Architecture is solid. Easy to grow, optimize, and plug into bigger systems.