Four secrets, one sandbox, one build.
Every mega-prompt in this repo ships on the same substrate — the EVVM stack on Ethereum Sepolia, plus zkVault when the idea needs shielded notes. Both use one EOA for deployer, fisher, admin, goldenFisher and activator.
Why the sandbox, not your laptop?
The Lovable Linux sandbox already runs the deploy scripts with Nix-provisioned Foundry and reads the four secrets straight from your project settings. Participants never install anything locally; the same scripts run identically inside every remixed project.
The recipe
# 1. Add four secrets to your Lovable project (Settings -> Secrets): METAMASK_PRIVATE_KEY=0x... # Sepolia deployer AND fisher (single-EOA) SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/... ETHERSCAN_API_KEY=... PRIVY_APP_ID=... # Google login + native gas sponsorship # 2. Fund the deployer on Sepolia (0.05 ETH is plenty): # https://cloud.google.com/application/web3/faucet/ethereum/sepolia # 3. From the Lovable sandbox terminal (NOT your laptop): bash scripts/install-foundry.sh # Nix-provisioned Foundry into the sandbox node scripts/deploy-evvm.mjs # deploys the 6-contract EVVM stack + registers evvmId node scripts/deploy-zkvault.mjs # only if you picked a zkVault idea # 4. In the Privy dashboard, enable: # Gas sponsorship -> App pays -> Ethereum Sepolia -> "Allow transactions from the client" # 5. Paste any mega-prompt from this repo into a fresh Lovable message. It is fully # self-contained (EVVM / zkVault standing orders are inline). Ship the demo.
1. Fisher wallet
A single viem wallet client keyed by METAMASK_PRIVATE_KEY. Every fisher route uses this to sign the on-chain call on behalf of the user.
// src/lib/fisher.server.ts — the fisher wallet
import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
export function fisherWallet() {
const raw = process.env.METAMASK_PRIVATE_KEY!;
const pk = (raw.startsWith("0x") ? raw : "0x" + raw) as `0x${string}`;
return createWalletClient({
account: privateKeyToAccount(pk),
transport: http(process.env.SEPOLIA_RPC_URL!),
chain: sepolia,
}).extend(publicActions);
}
2. The EVVM CSV signature
EVVM messages are personal_sign of a CSV string — not abi-encoded. The hashPayload discriminator is the function name; args follow in the exact types the contract's hashDataForX view expects.
// src/lib/evvm-sign.ts — build the EVVM CSV EIP-191 payload
import { encodeAbiParameters, keccak256 } from "viem";
export function csvMessage(args: {
evvmId: number; senderExecutor: string; hashPayload: string;
originExecutor: string; nonce: bigint; isAsyncExec: boolean;
}) {
return [
args.evvmId, args.senderExecutor.toLowerCase(), args.hashPayload,
args.originExecutor.toLowerCase(), args.nonce.toString(),
args.isAsyncExec ? "true" : "false",
].join(",");
}
export function hashPayload(functionName: string, types: readonly any[], values: readonly any[]) {
// NOTE: string discriminator first, then the typed args in the EXACT order
// and types the contract expects. Cross-check with the on-chain hashDataForX view.
return keccak256(encodeAbiParameters(
[{ type: "string" }, ...types] as any,
[functionName, ...values] as any,
));
}
3. zkVault: browser proving only
Noir proofs are generated in the browser with bb.js 4.2.0 and @noir-lang/noir_js 1.0.0-beta.19 (exact pins). Do NOT prove in a TSS server function — Cloudflare workerd crashes on the file-path assumption. The submit route only signs and broadcasts. If you see revert selector 0x59895a53 (ProofLengthWrongWithLogN) the on-chain verifier was generated by a different bb.js version — rerun scripts/deploy-zkvault.mjs.