Fedimint module experiments, developed outside the Fedimint tree and built against a pinned platform branch.
Two module families are developed here, plus a third attached from elsewhere:
| Family | Crates | What it is |
|---|---|---|
modules/amm |
fedimint-amm-{common,server,client,tests} |
A constant-product AMM (Uniswap V2 as reference implementation) trading between the federation's AmountUnits. See modules/amm/fedimint-amm-spec.md. |
modules/usdt |
fedimint-usdt-{common,server,client,tests} |
USDT-on-EVM peg-in/peg-out via threshold ECDSA and ERC-4337. Lifted from the fork at consensus version 0.12. |
multi_sig_stability_pool |
stability-pool-{common,server,client} |
Fedi's stability pool v2 ("multispend"): multisig accounts with threshold transfers, and seeker/provider BTC↔fiat stabilization. Sourced from elsiribot/fedi, branch experimint, not this repo's own modules/. |
bin/fedimintd-experimint |
— | A fedimintd carrying the v2 core modules, meta, both local modules, and multi_sig_stability_pool. |
bin/fedimint-cli-experimint |
— | The matching fedimint-cli, the only client that can drive amm, usdt and multi_sig_stability_pool. See its README. |
bin/fedimintd-experimint is a thin wrapper around the platform branch's
fedimintd::run, supplying this module set: mintv2, walletv2, lnv2,
meta, amm, usdt, multi_sig_stability_pool. Every flag, env var, setup
UI and API endpoint is inherited from upstream.
cargo run -p fedimintd-experimint -- --helpIt deliberately omits the v1 mint/wallet/ln modules that a stock
fedimintd also attaches: this binary targets a multi-unit federation, which is
a v2-only story — the v1 modules predate AmountUnit.
docs/mainnet-deployment.md is the runbook for
standing seven of these up on Bitcoin and Ethereum mainnet: config-gen
commands, the full env var table, secret provisioning, the DKG ceremony, and
how to tell whether the usdt module actually reached BootstrapState::Ready
afterwards.
| Instance | Purpose |
|---|---|
walletv2 |
on-chain BTC peg-in/peg-out |
mintv2 (amount_unit: 0) |
BTC ecash |
mintv2 (amount_unit: 1) |
USDT ecash |
usdt |
USDT-on-EVM peg (issues AmountUnit::new_custom(1)) |
amm |
constant-product market between units 0 and 1 |
lnv2 |
Lightning |
meta |
guardian-published metadata |
multi_sig_stability_pool |
multisig accounts + threshold transfers (Fedi multispend); seeker/provider BTC↔fiat stabilization |
Note the two mintv2 instances, one per asset. Both setup paths express it.
CLI / API. set-local-params takes a repeatable
--module <kind>[=<json>] that builds the instance list directly. Instance ids
are assigned by flag position:
fedimint-cli admin setup set-local-params \
--module walletv2 \
--module 'mintv2={"amount_unit":0}' \
--module 'mintv2={"amount_unit":1}' \
--module lnv2 \
--module 'usdt={"chain_id":1}' \
--module amm \
--module meta \
--module multi_sig_stability_poolThe platform branch has a test (parses_full_deployment_topology) asserting
exactly this shape, two mintv2 instances included.
Setup UI — supported. The form builds the instance list one row at a time:
pick a kind, and for kinds denominated in an asset pick that too. Add a second
mintv2 row, choose "USDT (unit 1)", and the federation runs two mints. Rows
can be added and removed, so the UI expresses the same topologies as --module.
Multiple instances of one kind were always supported by the data model —
ServerModuleConfigGenParamsRegistry is keyed by ModuleInstanceId, and
ConfigGenParams::module_params is the single source of truth for which
instances run. Only the form couldn't express it. (The "Can't insert module of same kind twice" assert is on ModuleInitRegistry, kind → init: one
implementation per kind, unrelated to instance count.)
A mint holds no reserves of its own — its ecash is a claim on whatever backs the unit it is denominated in. Both sides are declared:
| Module | Declares |
|---|---|
walletv2 |
backs Bitcoin (unit 0) |
usdt |
backs USDT (unit 1) |
mintv2 |
requires whichever unit its amount_unit names |
Config generation refuses a topology whose ecash is denominated in an asset nothing backs, and it does so before DKG — the denomination is baked into the module's consensus config and cannot be changed afterwards. The setup UI offers only the declared assets, so the same mistake is unreachable through the form.
Bitcoin is always available and never needs a backing module: it is the federation's native unit, and a lightning-only federation denominates in it with no on-chain wallet enabled.
The setup UI pre-selects only modules whose is_enabled_by_default() is true —
here just lnv2, meta and amm. The three carrying the interesting topology
are opt-in via FM_ENABLE_MODULE_MINTV2, FM_ENABLE_MODULE_WALLETV2 and
FM_ENABLE_MODULE_USDT. multi_sig_stability_pool is opt-in the same way, via
FM_ENABLE_MODULE_SPV2, independent of the asset topology above. They are
still available without those variables; the variables only decide what
starts pre-selected.
bin/fedimint-cli-experimint is the client counterpart: a fedimint-cli with
this module set linked in, which is what makes module amm and module usdt
resolve at all.
cargo run -p fedimint-cli-experimint -- --data-dir /path/to/wallet join fed11qgqp...
cargo run -p fedimint-cli-experimint -- --data-dir /path/to/wallet infobin/fedimint-cli-experimint/README.md
is the usage guide: joining, the instance-id table and why ids are not portable
between federations, the AMM and USDt verbs with their real argument shapes,
denominations per unit, and which upstream subcommands are expected to fail
here.
The one thing to know before reading anything else: module <kind> resolves
to the lowest instance id of that kind, so with two mintv2 instances it
always reaches the BTC mint. Address instances by id.
As of 2026-08-31 a seven-guardian federation runs these modules on Bitcoin
mainnet and Ethereum mainnet, and it holds real funds. usdt is at consensus
version 0.13, amm at 0.0. See docs/handover-2026-08-30.md for the invite
code, topology and operational notes.
This section previously said the opposite, and said that if a deployment ever happened it would be the first thing needing an update — because a good deal of judgement downstream keys off it. So, explicitly:
What has changed. Wire encoding, DB layout and consensus behaviour are no
longer free to change. A breaking change now needs a MODULE_CONSENSUS_VERSION
bump and, for stored records, a migration — not just an edit. The cost of
getting that wrong is already demonstrated: usdt shipped two breaking wire
changes under one version, and the mismatch surfaced as a deposit that hung
forever rather than a clean rejection (see the 0.13 note in
fedimint-usdt-common). fedimint-derive encodes enum variants by positional
index, so deleting or reordering a variant is breaking even when nothing
references it.
What has not been decided. How much of the old freedom to give up is a
judgement call, not a fact, and it is not made here. amm is still at 0.0 and
its pool holds real liquidity; whether that warrants freezing its wire format or
whether this deployment is disposable enough to keep breaking it is the
maintainer's call. Findings previously parked as "not actionable without a
version bump" are still actionable — a bump is now the price, rather than being
free.
Every fedimint-* dependency is git-pinned, by revision, to a single commit:
fedimint-core = { git = "https://github.com/elsiribot/fedimint", rev = "a2d0207702fa13f386b40d41da8133f14fd000ac" }That revision is experimint-v0.11 on the elsiribot/fedimint fork —
Fedimint plus the fork's core/platform changes, with the custom modules
stripped out. Workspace version 0.12.0-alpha.
This is not upstream master, and it is not behind it. The two diverged
from be854220f; experimint-v0.11 is a different API generation, not an
older one. Do not assume an upstream fedimint/fedimint commit is a drop-in
replacement.
Three things force the pin:
fedimint-usdt-serverdepends onfedimint-threshold-ecdsa(crypto/threshold-ecdsa), which exists only on the fork.- The typed per-module
ServerModuleInit::Paramsconfig-gen hook and the leader--moduleCLI are fork-only. - Both module families must resolve to one and the same
fedimint-core. Two copies would compile, but the modules could never be registered into the same federation — which is the point of an AMM that trades USDT.
- Change
revon everyfedimint-*line in the rootCargo.toml. They must all move together — a mismatched rev makes cargo resolve them as distinct source packages and the trait impls silently stop lining up. - Re-seed the lock (see below) and run
cargo check --workspace --all-targets.
branch = "experimint-v0.11" also resolves, but moves under you; prefer the
rev.
Cargo does not inherit a git dependency's lockfile. It re-resolves the whole tree from scratch, which can land on version combinations the platform branch itself never builds. Two have bitten this repo already:
- a fresh resolve picks
bdk_electrum 0.23.2→electrum-client 0.24.1, whilefedimint-ldk-nodealso wantselectrum-client 0.23.1directly; two semver-incompatible copies land in the graph and it fails to compile. fedimint-connectorswantsiroh-next = "=1.0.0"andiroh-mainline-address-lookup ^0.4.0; a fresh resolve picksiroh 1.0.3for the latter and fails to unify.
Cargo.lock is therefore seeded from the platform branch's own lock and
committed. When bumping the pin, seed it again rather than deleting it:
git -C /path/to/fedimint show experimint-v0.11:Cargo.lock > Cargo.lock
cargo check --workspace --all-targetsKeeping third-party versions in [workspace.dependencies] byte-identical to
the platform branch's is part of the same defence — it is what keeps
alloy*, secp256k1, bls12_381 and cggmp21 from unifying into two
copies across the git boundary. Do not upgrade one in isolation.
Everything runs in the Nix dev shell:
nix develop --accept-flake-config
cargo check --workspace --all-targetsOr without entering it:
nix develop --accept-flake-config --command cargo check --workspace --all-targetsThe daemon is also a flake package, which is what a NixOS host consumes:
nix build .#fedimintd-experimint
./result/bin/fedimintd-experimint --versionIt builds only bin/fedimintd-experimint, not the *-tests crates, and
vendors from the committed Cargo.lock rather than re-resolving — see the trap
described under Cargo.lock is seeded from the platform
branch.
The shell exists because several dependencies build C from source and need
specific host tooling: m4 and file for gmp-mpfr-sys (via rug ←
cggmp21), protobuf/cmake/perl for the gateway and aws-lc-sys, and a
fortify-free hardening set for tikv-jemalloc-sys' autoconf probes.
cargo test --workspace # everything that runs hermetically
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --checkThe fedimint-usdt-tests end-to-end suites drive a real EVM chain via anvil,
which the dev shell provides (Foundry). Nothing extra to install.
They skip silently if anvil is missing. spawn_anvil returns Ok(None)
on ErrorKind::NotFound, so an absent binary and a passing test produce
identical output. That is why Foundry is a dev-shell dependency rather than a
CI step — the shell is what CI and developers share, so neither can quietly
lose the coverage. Every other failure mode (bad FM_ANVIL_BASE_EXECUTABLE,
wrong permissions, anvil spawning but never serving RPC) is a hard failure, by
design.
tests/full_topology_e2e.rs deliberately does not take that affordance: it
treats a missing anvil as a hard failure, because its central claim is that
the amm trades real USDt and a silently-skipped USDt leg would make that
claim unfalsifiable.
These suites are the only coverage of the real ERC-4337 UserOp path,
withdrawal batching against a live chain, reorg handling, residual recovery and
non-standard token behaviour. FM_ANVIL_BASE_EXECUTABLE overrides the binary
if you need a specific build. Some devimint tests separately want bitcoind.
Three fedimint-usdt-tests binaries are not part of any test lane:
usdt-e2e-test— devimint-driven end-to-end run.usdt-adversary— joins a live federation from an invite code and runs the no-funds deposit-by-proof attacks against it.capture-deposit-proof-fixtures— one-shot regenerator for the committed mainneteth_getProoffixtures.
*-common and *-client must stay WASM-safe:
cargo check --target wasm32-unknown-unknown \
-p fedimint-usdt-common -p fedimint-usdt-client \
-p fedimint-amm-common -p fedimint-amm-clientThe dev shell carries the target and the cross-compilation environment this
needs (unwrapped clang for the vendored secp256k1-sys C, plus getrandom's
wasm_js backend flag). Do not add non-WASM-safe dependencies to these four
crates.
- The
swapmodule, the other custom module on the fork's2026-07-usdt-walletbranch. - A non-CLI client.
bin/fedimint-cli-experimintis the only thing that drivesammandusdtclient-side; there is no library-shaped wallet SDK, and the*-clientcrates are WASM-safe but nothing consumes them from a browser or a mobile host. - A deployment-shaped run of the full topology. The seven-instance
deployed topology is covered:
fedimint-usdt-tests'tests/full_topology_e2e.rsstands up one in-process federation with all seven instances (bothmintv2s included), pegs BTC in overwalletv2, deposits USDt by proof against a realanvil, seeds anammpool with both legs, swaps each way at the quoted price, withdraws the position and checks every guardian's balance sheet. It does not attachmulti_sig_stability_pool— the module is attachable but not part of this deployment's topology (seedocs/mainnet-deployment.md), so the now-eight-row instance table above is not fully exercised by this test. What is still untested even for the seven it does cover is the deployment shape around it:fedimint-testingtrusted-dealer-generates configs rather than driving the--moduleCLI/UI path, and no Lightning gateway is attached, solnv2boots but never routes.