Before running any setup, make sure you have completed the one-time steps:
-
Clone and install — see the root README.
-
Environment variables — create
.envas described in Environment variables. -
Which account to run as — identity is an application account (
users.id), not a Telegram chat id. For the local harnesses (make deploy-wallet,make agent) set:APP_USER_ID=1
deploy_wallet.resolve_harness_userreads it, andsmart_wallet_agent.mainuses the same resolution. IfAPP_USER_IDis unset it falls back to translatingTELEGRAM_CHAT_IDthrough theuserstable — which is what keeps a pre-2026-09-10 setup working, since the migration created an account for your old chat id and recorded it asusers.telegram_chat_id. A raw chat id is never used as a key any more, so an unlinked one is an error rather than a silent new account.Telegram is optional. Only the Telegram bot (
make bot) needs a bot from @BotFather — one bot for every network, with groups and inline mode turned off — and a real Telegram account. Its token goes inMITFAH_APIand its username (no @) inMITFAH_USERNAME; each user picks the network inside the chat with/chain. The bot serves only private chats that have been linked to an account through the web app's deep link (POST /api/integrations/telegram/link); an unlinked chat is refused. Its/webcommand links the web app: setWEB_APP_URL(e.g.https://mitfah.com), or it uses the first ofCORS_ORIGINS. Coming from the five per-network bots? See Moving from five Telegram bots to one. For the interactive CLI (make agent), skip all of it and just setAPP_USER_ID. -
Web API secrets —
make apineedsJWT_SECRET(any long random string;python -c "import secrets;print(secrets.token_hex(32))"). The bot's username (MITFAH_USERNAME) is needed only to mint the Telegram deep link and to show the bot in the web app, andCORS_ORIGINS(comma-separated, defaulthttp://localhost:3000) must list your front end since the refresh cookie requires credentialed CORS. SetCOOKIE_SECURE=0for local http development.SIWE_DOMAIN(comma-separated, defaultlocalhost:3000) is the site a sign-in message must name — set it tomitfah.comin production; a message written for any other site is refused, which is what stops a phishing page from replaying a victim's signature to sign in as them. Signing in with the wallet (SIWE) is the only way in: there is no email, password or Google sign-in.The LLM provider is optional too. The agent defaults to DeepSeek (
DEEPSEEK_API_KEY); swap in any other LangChain chat model with a small edit toapp/smart_wallet_agent.py(see docs/app.md) and that key is no longer required.
Every network (Anvil, Ethereum mainnet fork, Sepolia fork, live Sepolia, BSC fork, live BSC) follows the same five steps. The sections below give the exact commands for each network — this is the shape of what's happening and why the order matters:
-
Start the network —
make anvil,make mainnet-fork,make sepolia-fork,make bsc-fork,make arb-forkormake base-fork. This must be running before anything else, since every later step talks to it over RPC. (Live deployments skip this — there's no local node to start.)Each fork has its own port, so several can run at once:
sepolia-fork8545,bsc-fork8546,mainnet-fork8547,arbitrum-fork(make arb-fork) 8548,base-fork8549. Plainanvilandcelo-forkuse 8545 too. The fork targets pass--portto Anvil themselves, andfund/deploypick the port fromARGS, so the commands are the same as before — just always passARGS. The ports live in two places that must agree: the Makefile'sFORK_PORT_*variables and the*-forkrows ofRPCSinapp/seed_data.py(re-runmake dbafter changing them). Their chain IDs differ, so one API process withAPP_FORK_MODE=1serves every running fork. In your browser wallet, point each network at its fork's port.Starting a fork also funds the bundlers. As soon as the fork answers, the target gives every bundler key (
API_BUNDLER_n,TELEGRAM_BUNDLER_n) 100 of the chain's native coin each (the sameanvil_setBalanceasmake fund). If that port already has a node on it, the target refuses to start instead. -
Deploy the shared protocol —
make deploy ARGS="<network>". Runsscript/DeploySHProtocol.s.sol, which deploys the infrastructure shared by all users:EntryPoint, mocks (on Anvil),SHOracle,SHTreasury,SHRegistry,SpendingLimitModule, andSHFactory(with the module set via the factory constructor). Only needs to be re-run when this infra doesn't exist yet on the target chain (e.g. after restarting Anvil, which wipes all chain state). -
Sync the database —
make db. Seeds reference data (token addresses, selectors, RPC URLs, Chainlink feed addresses) and, critically, reads theSHFactoryaddress out of the Forge broadcast file written in step 2 (broadcast/DeploySHProtocol.s.sol/<chain_id>/run-latest.json) into thefactorytable. This step must run after step 2 —deploy_wallet.pyresolves the factory address from the DB, not from the broadcast file directly. Safe to re-run any time; it's idempotent.Watch for stale addresses. Every re-run of
forge script script/DeploySHProtocol.s.sol --broadcaston the same chain overwrites that chain'srun-latest.jsonwith a fresh set of addresses (new nonces → different addresses for every contract, includingSHFactory). If you deploy again without re-runningmake db, the DB keeps pointing at the old (now-wrong) addresses — this can fail in a confusing way, since the old address might still have some contract's bytecode at it (e.g. a previous deployment'sSpendingLimitModule), producing an empty-revert rather than an obvious "no code" error. -
Deploy your wallet —
make deploy-wallet ARGS="<network>". CallsSHFactory.deployWallet(dailyLimitUsd, windowDuration, watchedTokens, sessionKeyX, sessionKeyY, sessionKeyValidUntil, trustedSpenders)to create a per-userSessionHandlerin one transaction:SpendingLimitModuleis installed as a spending-cap hook seeded with a $50k/24h cap over the network's default watched tokens, the harness's passkey is approved for 30 days, and the chain's exchange contracts are granted as trusted spenders (constants.get_default_spenders: Uniswap V3's SwapRouter02 and position manager on Ethereum, Sepolia, Arbitrum and Base, PancakeSwap's V2 router on BSC, none on bare Anvil). It is funded with ETH by the same call (deployWalletispayable).One transaction, not three.
addSessionandaddTrustedSpenderused to be separate owner-signed follow-ups. They are now seeded insideinitialize, sodeploy_wallet.py'sadd_default_session()/trust_default_spenders()are recovery helpers only and are no longer called by__main__.The harness's passkey is a software one. A real user's passkey is made on their phone or laptop, in the web app. The command line has no browser, so
deploy_wallet.pyapproves a P-256 key held in software instead (harness_passkey), derived from the deployer's private key, the user and the chain. It adds no power — whoever holds that private key already owns the wallet — and letsmake agent,make e2e-testandmake agent-smokeapprove quotes the way a phone would.Re-running gives you a NEW wallet, not the same one. The CREATE2 salt is derived from
(deployer, factory.deployCount[deployer])and the factory consumes that counter on every deploy, so eachmake deploy-walletlands on a fresh address with no salt bookkeeping on the Python side. It does not migrate anything:session_handlersis overwritten for thatchat_id, and the previous wallet keeps its prefund, tokens and LP positions with nothing pointing at it. The script warns when it is about to do this — withdraw first if the old wallet holds anything you want.ARGShere must exactly match the network you started in step 1 and deployed to in step 2 ("anvil","mainnet-fork","sepolia-fork","sepolia","bsc-fork", or"bsc") —deploy_wallet.py'sdeploy()dispatcher passes it straight through tonetwork. If it doesn't match, the script will look up the wrong factory address (or none at all) and fail. OmittingARGSdefaults to"anvil", matchingmake deploy's own no-ARGSdefault. -
Start talking to it —
make bot(Telegram) ormake agent(interactive CLI). In the CLI, a quote prints as a card; typeyesto approve it with the harness's passkey. In Telegram, a quote's "Confirm with passkey" opens the web app's approval page, so the bot needsWEB_APP_URLand a passkey made in the web app.The Telegram bot follows
APP_FORK_MODE, like the API. WithAPP_FORK_MODE=1every network it offers in/chainis that network's local fork. A network that isn't running (or answers as another chain) when the bot starts is left out until the next start, with a message saying why — so on forks, start the forks you want first. Plain Anvil is never offered.
Shortcut —
make setup-agent ARGS="<network>". Runs steps 2, 3, 4 and 5 (deploy → fund → db → deploy-wallet → agent) in one command, stopping if any step fails, then drops straight into the interactive CLI agent at the end.fundhere is a balance top-up viaanvil_setBalance(see the Makefile's "Funding Wallets" section) — it automatically skips itself on live networks (sepolia/bsc), where there's no local Anvil node to fund, so this chain is safe to use for all six networks without special-casing. Step 1 (start the network) is not part of this chain — start the network first (skip this for live deployments). The sameARGSvalue is passed through todeploy,fund, anddeploy-walletinternally, so it must be one of the network names listed in step 4 above.The full local setup in two commands:
make <network>-fork # or `make anvil` for plain local Anvil — see step 1 above make setup-agent ARGS="<network>"e.g.
make sepolia-forkfollowed bymake setup-agent ARGS="sepolia-fork". For a live deployment (no local node to start), just runmake setup-agent ARGS="sepolia"(or"bsc") on its own.Two siblings run the identical chain and differ only in what they leave you in:
make setup-bot ARGS="<network>"ends in the Telegram bot instead of the CLI agent, andmake setup-test ARGS="<network>"stops afterdeploy-walletwith no front end, for running tests against a freshly deployed stack.
Celo is not yet supported end-to-end. The Python app layer has scaffolding for it (token list, chain ID, network routing), but
HelperConfig.s.solhas no Celo chain ID branch, so step 2 (make deploy ARGS="celo-fork"or similar) cannot succeed on Celo until that's added on the Solidity side. Note also thatwrap_ethis meaningless on Celo — CELO is natively an ERC-20 with no wrapping step. See docs/app.md for what's already wired up.
Step 1 — Start Anvil:
make anvilStep 2 — Deploy the shared protocol:
make deploy(No ARGS needed — the default NETWORK_ARGS in the Makefile already points at http://127.0.0.1:8545 with the Anvil default account.) This deploys the full mock stack (EntryPoint, ERC20Mocks, MockV3Aggregators, MockIdentityRegistry, MockReputationRegistry, SHOracle, SHTreasury, SHRegistry, SpendingLimitModule, SHFactory).
Step 3 — Sync the database:
make dbStep 4 — Deploy your wallet:
make deploy-wallet(No ARGS needed — deploy_wallet.py defaults to "anvil" when none is given, matching make deploy's own default. Pass ARGS="anvil" explicitly if you'd rather not rely on the default.)
Must be re-run (along with steps 1, 2 and 3) whenever Anvil is restarted — chain state, including the deployed protocol, is wiped on restart.
Shortcut — full setup in two commands:
make anvil make setup-agentRuns steps 2, 3 and 4 above plus a balance top-up (
fund), finishing with the interactive CLI agent below. NoARGSneeded for either command — both default to plain Anvil. See The Setup Sequence above.
Step 5 — No Telegram here. The Telegram bot serves Ethereum, Sepolia, BNB Smart Chain, Arbitrum One and Base (live or forked), not plain Anvil, so it has nothing to offer a wallet on Anvil. Talk to the agent through the interactive CLI below instead.
Step 6 — Chat:
What is my wallet address?
What is my USDC balance?
Send 10 LINK to Sandy
Optional — interactive CLI (no Telegram required):
make agentRequires MAINNET_RPC_URL in .env.
Step 1 — Start a mainnet fork:
make mainnet-forkStep 2 — Deploy the shared protocol:
make deploy ARGS="mainnet-fork"ARGS="mainnet-fork" sends to the mainnet fork's port (8547) and signs with SEPOLIA_ACCOUNT / API_BUNDLER_1. Without it, make deploy targets plain Anvil on 8545 with the Anvil burner key.
Step 3 — Sync the database:
make dbStep 4 — Deploy your wallet:
make deploy-wallet ARGS="mainnet-fork"Step 5 — Start:
make bot # Telegram bot
make agent # Interactive CLIRe-run steps 1, 2 and 3 after any Anvil restart — fork state is wiped on restart.
Shortcut — full setup in two commands:
make mainnet-fork make setup-agent ARGS="mainnet-fork"Runs steps 2, 3 and 4 above plus a balance top-up (
fund), finishing withmake agent(notmake bot) from step 5. See The Setup Sequence above.
Requires SEPOLIA_RPC_URL in .env.
Step 1 — Start a Sepolia fork:
make sepolia-forkStep 2 — Deploy the shared protocol:
make deploy ARGS="sepolia-fork"ARGS="sepolia-fork" tells the Makefile to sign with SEPOLIA_ACCOUNT / API_BUNDLER_1 while still broadcasting to the local fork at http://127.0.0.1:8545.
Step 3 — Sync the database:
make dbStep 4 — Deploy your wallet:
make deploy-wallet ARGS="sepolia-fork"Step 5 — Start:
make bot
make agentUniswap V3 is officially deployed on Sepolia, so the swap and liquidity-position tools work here. The wallet is seeded with a $50k/24h USD cap over the default Sepolia watched tokens (WETH, USDC, LINK) and the harness's passkey, which can drive any of them — see docs/app.md.
Shortcut — full setup in two commands:
make sepolia-fork make setup-agent ARGS="sepolia-fork"Runs steps 2, 3 and 4 above plus a balance top-up (
fund), finishing withmake agent(notmake bot) from step 5. See The Setup Sequence above.
Requires BSC_RPC_URL in .env.
Step 1 — Start a BSC fork:
make bsc-forkStep 2 — Deploy the shared protocol:
make deploy ARGS="bsc-fork"ARGS="bsc-fork" signs with MAINNET_DEPLOYER_PK (the placeholder deployer key baked into HelperConfig.s.sol) and broadcasts with --legacy --skip-simulation — BSC's fee-history data and this fork's baseFeePerGas: 0 blocks both confuse Forge's default gas estimation, so the Makefile routes around it (see the comment above the bsc-fork branch in Makefile for the full explanation). If the deployer needs a balance bump on the fork first, make fund ARGS="bsc-fork" sets it directly via anvil_setBalance.
Step 3 — Sync the database:
make dbStep 4 — Deploy your wallet:
make deploy-wallet ARGS="bsc-fork"Seeds the wallet with a $50k/24h USD cap over the default BSC watched tokens (WBNB, USDC, USDT) and the harness's passkey — see docs/app.md.
Step 5 — Start:
make bot
make agentShortcut — full setup in two commands:
make bsc-fork make setup-agent ARGS="bsc-fork"Runs steps 2, 3 and 4 above plus a balance top-up (
fund), finishing withmake agent(notmake bot) from step 5. See The Setup Sequence above.
Requires SEPOLIA_RPC_URL (Alchemy endpoint), and every API_BUNDLER_n and TELEGRAM_BUNDLER_n key funded with Sepolia ETH — API_BUNDLER_1 deploys, the API's keys bundle for the API, the Telegram ones for the bot.
There's no local node to start for a live deployment, so this flow skips step 1 above.
Step 1 — Deploy the shared protocol to live Sepolia:
make deploy ARGS="sepolia"ARGS="sepolia" broadcasts to the real SEPOLIA_RPC_URL and (if ETHERSCAN_API_KEY is set) verifies contracts on Etherscan.
Step 2 — Sync the database:
make dbStep 3 — Deploy your wallet:
make deploy-wallet ARGS="sepolia"On Sepolia, as everywhere, the app is its own bundler: bundler.py submits UserOps inside handleOps from one of the API_BUNDLER_n keys (the API) or the TELEGRAM_BUNDLER_n keys (the bot), which the EntryPoint repays out of each wallet's prefund. Nothing tops those keys up on a live chain, so keep them funded. If ETHERSCAN_API_KEY is set, both SHOracle and SessionHandler are automatically verified on Etherscan after deployment.
Uniswap V3 is officially deployed on Sepolia, so the swap, quote and liquidity-position tools are available here too. The wallet is seeded with a $50k/24h USD cap over WETH/USDC/LINK and the harness's passkey, which can drive any external call within that cap.
Step 4 — Start:
make bot
make agentShortcut — full setup in one command:
make setup-agent ARGS="sepolia"Since there's no local node to start for a live deployment, this alone covers steps 1, 2 and 3 above, finishing with
make agent(notmake bot) from step 4. The chain'sfundstep (a local-fork-only balance top-up) automatically no-ops on live networks, so it's safe to include here. This broadcasts a real transaction to live Sepolia — make sure that's what you intend before running it.
| Contract | Address |
|---|---|
IdentityRegistry (canonical ERC-8004) |
0x8004A818BFB912233c491871b3d84c89A494BD9e |
ReputationRegistry (canonical ERC-8004) |
0x8004B663056A597Dffe9eCcC1965A193B7388713 |
SessionHandler,SpendingLimitModule, andSHOracleaddresses are deployment-specific and intentionally omitted here — they change on every freshforge script --broadcastrun (different deployer nonces). After runningmake deploy ARGS="sepolia", the fresh addresses are in the Forge broadcast file (broadcast/DeploySHProtocol.s.sol/11155111/run-latest.json) and get synced intowallet.dbbymake db.
Requires BSC_RPC_URL (Alchemy endpoint) and MAINNET_DEPLOYER_PK/MAINNET_DEPLOYER_ADDRESS (see Makefile) funded with real BNB, plus BSC_PRIVATE_KEY for the Python wallet-deployment step.
There's no local node to start for a live deployment, so this flow skips step 1 above.
Step 1 — Deploy the shared protocol to live BSC:
make deploy ARGS="bsc"The
Makefile'sARGSmatching for BSC only has an explicit branch forbsc-fork(which broadcasts to a local fork with--legacy --skip-simulation). A plainARGS="bsc"falls through to the genericNETWORK_ARGSdefault (Anvil's burner key), which is not what you want for a real broadcast — replaceNETWORK_ARGSwith a BSC-specific branch (mirroring thesepoliabranch:--rpc-url $(BSC_RPC_URL) --private-key <funded-bsc-key> --broadcast) before deploying live. This is a real gap in the currentMakefile, not a documentation oversight — double-check the resolved command before broadcasting.
Step 2 — Sync the database:
make dbStep 3 — Deploy your wallet:
make deploy-wallet ARGS="bsc"BSC is self-bundled like every other network: bundler.py signs handleOps with one of the API_BUNDLER_n or TELEGRAM_BUNDLER_n keys, depending on which process is running (see bundler.pick_bundler). On bsc-fork make fund tops them all up; on live BSC they must hold real BNB. Live BSC has not been exercised yet.
Step 4 — Start:
make bot
make agentShortcut — full setup in one command:
make setup-agent ARGS="bsc"Since there's no local node to start for a live deployment, this alone covers steps 1, 2 and 3 above, finishing with
make agent(notmake bot) from step 4. The chain'sfundstep (a local-fork-only balance top-up) automatically no-ops on live networks, so it's safe to include here. This broadcasts a real transaction to live BSC — make sure that's what you intend before running it. Also note themake deploygap flagged in step 1 above: confirmNETWORK_ARGSactually resolves to a funded BSC broadcast before relying on this shortcut for a live BSC deploy.
Until 2026-10-16 each network had a Telegram bot of its own. Now one bot serves every network, and each user picks the network inside the chat with /chain. To move an installation that ran the five bots:
- Make one bot in @BotFather. Turn groups off (
/setjoingroups→ Disable) and leave inline mode off (a new bot starts with it off). The bot answers only private chats either way; this stops it being added to a group at all. - Update
.env. SetMITFAH_APIto the new bot's token andMITFAH_USERNAMEto its username (no @). Delete the ten old variables:MITFAH_ETH_API,MITFAH_SEPOLIA_API,MITFAH_BSC_API,MITFAH_ARB_API,MITFAH_BASE_APIand the five matching…_USERNAMEs. Nothing reads them any more; ifMITFAH_APIis missing while some are still set,make botnames them.TELEGRAM_MAX_CONCURRENT(how many Telegram chat turns run at once, default 64) is optional. - Run
make dbbefore restarting anything. It addsusers.telegram_chain_id, where each account's/chainchoice is kept (migration 6). The bot refuses to start without it, and the API writes it too: unlinking Telegram clears it. - Restart
make apiandmake bot(still exactly one copy of the bot). The API now gives the bot's username in/api/me(telegram_bot) instead of per network in/api/chains, and its link endpoint takes no chain. Deploy the matching web build with it: an older one looks for the bots in/api/chainsand would offer no Telegram link. - Users press Start in the new bot. Their link carries over: in a private chat a user's chat id is the same in every bot, so the chat id already stored for their account (
users.telegram_chat_id) still matches and nobody links again. Until they press Start the bot may not write to them, so they get no daily checks. Each network's conversation is still there and resumes when the user picks that network with/chain. - Retire the five old bots. Nothing answers in them once
make botruns with the new token. Point their users to the new bot (a last message, or each old bot's description in BotFather) before deleting them.