Lucidly Pro
An institutional-grade vault execution engine for EVM — operate the same Boring Vault from a UI, an SDK, a CLI, or a natural-language prompt, with identical safety guarantees underneath.
Lucidly Pro is the control surface over Lucidly's on-chain engine (the Se7en-Seas Boring Vault architecture): a minimal asset-holding vault, Merkle-gated strategy execution, an exchange-rate accountant, a multi-asset teller, and queued redemptions. Every operation routes through @lucidly/sdk, so a UI click, a CLI command, an SDK call, and a prompt sentence are the same action with the same checks.
Four surfaces, one engine
The same operation is available four ways (five with the MCP connector), and all of them go through the SDK and the shared safety layer (preflight, the leaf-existence guard, slippage guards):
| Surface | Use it for | Example |
|---|---|---|
| UI | Visual ops, demos, monitoring | Deploy wizard, strategy tree, queue console |
| CLI @lucidly/cli | Scripting, CI, operators | lucidly-cli aave supply USDC 1000 |
| SDK @lucidly/sdk | Apps, bots, backends | client.strategy.aave.supply(...) |
| Prompt / MCP @lucidly/mcp | Single-sentence ops, agents | agent "supply 1000 USDC into Aave" |
Quickstart
From npm install to a live read in under a minute.
Install
# the CLI (provides `lucidly-cli` globally)
npm install -g @lucidly/cli
# or use the SDK directly in your project
npm install @lucidly/sdk
Published packages: @lucidly/sdk · @lucidly/cli · @lucidly/mcp. Requires Node ≥ 18.
Configure
mkdir -p ~/.config/lucidly && cat > ~/.config/lucidly/config.json <<'EOF'
{ "rpc_url": "https://mainnet.base.org", "chain": "base" }
EOF
export LUCIDLY_PK=0xYOURKEY # signer; only needed for write commands
First commands
lucidly-cli # help
lucidly-cli vault preflight params.json # safe: no broadcast, no key needed
lucidly-cli vault view # once a vault is active
lucidly-cli monitor snapshot # read-only NAV/AUM/flows + alerts
~/.config/lucidly/ (override with LUCIDLY_HOME). The active vault, deployment addresses, and enabled-integration descriptor persist in state.json, so later commands rebuild the Merkle tree and proofs automatically.Develop locally (from source)
If you're contributing to the SDK/CLI itself rather than just using it:
git clone https://github.com/lucidlylabs/lucidly-skills.git && cd lucidly-skills
cd lucidly-sdk && npm install && npm run build && npm link
cd ../lucidly-cli && npm link @lucidly/sdk && npm run build && npm link
Concepts
The engine, the safety model, and the vocabulary the rest of the docs use.
The vault engine
Each vault is a stack of audited contracts (pinned Boring Vault commit 0e23e7f, solc 0.8.21):
| Contract | Role |
|---|---|
RolesAuthority | solmate role registry; gates every privileged call |
BoringVault | holds assets & is the ERC20 share token (not ERC4626) |
ManagerWithMerkleVerification | executes strategies that match the Merkle root |
AccountantWithRateProviders | the NAV / exchange rate, with bounds & fees |
TellerWithMultiAssetSupport | deposits (mint) and bulkWithdraw (redeem) |
AtomicQueue + LucidlyRedeemSolver | asynchronous, illiquid-safe redemptions |
DecoderAndSanitizer | one allowed function per selector; constrains address args |
Merkle-gated execution
A strategist (or an agent acting for them) can only execute calls whose leaf is in the vault's manage root. A leaf is:
leaf = keccak256(abi.encodePacked(
decoder, target, valueIsNonZero, selector, packedArgumentAddresses
))
The chain stores only the root; the SDK builds leaves and proofs off-chain and the Manager verifies the proof. The DecoderAndSanitizer for each selector returns the constrained address arguments — forcing recipients and onBehalfOf to the vault — so an approved call can never redirect funds. This is the property that makes single-prompt execution safe: the operator surface physically cannot express a call outside the root.
syUsdSeedMode: "repointed" (the default on mainnets) deploys the vault's own full syUSD decoder and seeds the regenerated syUSD baseline root, so a new vault is useful immediately — no "enable 8 things first."Roles & delegation
Least-privilege via RolesAuthority. Canonical role IDs:
| ID | Role | Can |
|---|---|---|
| 1 | MANAGER | call vault.manage (held by the Manager) |
| 2 / 3 | MINTER / BURNER | mint/burn shares (held by the Teller) |
| 7 | STRATEGIST | execute the manage root |
| 8 | OWNER | govern (handed to an owner address after finalize) |
| 11 | UPDATE_EXCHANGE_RATE | push NAV updates |
| 12 | SOLVER | bulk withdraw & drive redemptions |
NAV & the accountant
The Accountant holds the exchange rate (base-asset units per 1.0 share). Updates are bounded (e.g. ±0.5% per update) with a minimum delay; an out-of-bounds update trips a circuit-breaker pause rather than reverting. AUM = totalSupply × rate / 10**decimals.
Queued redemptions
When a vault holds illiquid positions (Pendle, locked LPs), the instant Teller withdrawal reverts for lack of free liquidity. The AtomicQueue (the same mechanism syUSD uses) lets a holder post a request — offer shares, want an asset, at a limit price, by a deadline — and a SOLVER unwinds positions and fulfills the batch atomically through LucidlyRedeemSolver: shares are burned through the Teller and each holder is paid at their request price, or the whole solve reverts.
Preflight & safety
vault.preflight(params) runs before any deploy (and on demand) and returns blocking errors vs advisory warnings: pinned-commit assertion, owner-is-contract (EIP-7702-aware; EOA ownership is allowed with a warning), un-finalized ownership, strategist==owner, zero/wide rate bounds, zero anti-MEV share-lock, high fees. Mainnet swaps require minOut (a MissingSlippageGuardError otherwise). Finalize renounces the deployer's control to the owner address.
CLI — @lucidly/cli
The operator surface. Write commands need LUCIDLY_PK; reads don't.
Install & configure per the Quickstart. Amounts for aave/teller deposit are human units scaled by the asset's decimals.
Command reference
| Command | Does |
|---|---|
config show | print config + persisted state |
vault preflight <params.json> | pre-mainnet safety check (no broadcast) |
vault deploy <params.json> | deploy the full stack; saves it active |
vault set <address> | select an active vault |
vault finalize <ownerAddress> | hand ownership to an owner address (irreversible) |
vault view | name/symbol/decimals/supply/rate/AUM |
accountant view · set-rate <rate> | read / push the exchange rate |
integration enable <PROTOCOL…> [--assets USDC,WETH] | deploy decoder, build leaves, set root |
delegate grant <addr> [--role STRATEGIST] | grant a role |
roots show [--strategist <addr>] | read the on-chain manage root |
teller deposit <asset> <amount> [--min-mint n] | public deposit (mint shares) |
teller withdraw <asset> <shares> | instant redemption (solver path) |
queue deploy [--operator] [--owner] | AtomicQueue + redeem solver, wired |
queue request <want> <shares> --price <p> [--deadline s] | post a redemption request |
queue solve <want> <user…> [--min-out] [--max-assets] | SOLVER fulfills a batch atomically |
queue status <user> <want> | show a request + validity |
monitor snapshot [--from <block>] | NAV/AUM/flows + alerts (read-only) |
monitor dashboard [--out file.html] | render a self-contained HTML dashboard |
aave supply|withdraw <asset> <amount> | leaf-verified Aave strategy (one tx) |
agent "<prompt>" | single-prompt path with the leaf guard |
SDK — @lucidly/sdk
A viem-based client. Reads work without a signer; writes return { hash, wait() }.
Getting started
import { LucidlyClient } from "@lucidly/sdk";
import { privateKeyToAccount } from "viem/accounts";
const client = new LucidlyClient({
chain: "base", // or 1 | 8453 | 42161 | 84532
rpcUrl: process.env.RPC_URL,
signer: privateKeyToAccount(process.env.LUCIDLY_PK), // omit for read-only
});
client.vault
await client.vault.preflight(params); // PreflightReport { ok, errors[], warnings[] }
const d = await client.vault.deploy(params); // deploys + wires + seeds baseline root
await client.vault.finalize(ownerAddress); // grant OWNER, renounce, transfer RA to the owner address
await client.vault.view(); // { name, symbol, decimals, totalSupply, rate }
await client.vault.aum(); // totalSupply * rate / 10**decimals
Key DeployParams: name, symbol, baseAsset, allowedDepositAssets, initialRate, exchangeRateBounds {upperBps,lowerBps,minUpdateDelay}, shareLockSeconds, owner, strategist, rateUpdater, solver, finalizeOwnership, syUsdSeedMode ("repointed" | "verbatim" | "none").
accountant & teller
await client.accountant.getRate(); // current NAV (bigint)
await (await client.accountant.updateRate(1_003_000n)).wait();
await (await client.teller.deposit("USDC", 1_000_000_000n, { minMint: 1n })).wait();
await (await client.teller.withdraw("USDC", shares)).wait(); // instant (needs free liquidity)
roles & integrations
await (await client.roles.grant(addr, "STRATEGIST")).wait();
await client.roles.hasRole(addr, "OWNER");
await client.roles.getManageRoot(strategist);
// deploy the decoder, build leaves, set the new root
await client.integrations.enable(["AaveV3"], { assets: ["USDC"] }, vault);
client.strategy
// the safety property: hasLeaf() is checked before any tx is built
client.strategy.hasLeaf(vault, { protocol:"AaveV3", action:"supply", asset:"USDC" });
await (await client.strategy.aave.supply(vault, { asset:"USDC", amount })).wait();
await (await client.strategy.aave.withdraw(vault, { asset:"USDC", amount })).wait();
await (await client.strategy.swap(vault, { from:"USDC", to:"WETH", amount, minOut })).wait();
minOut (or explicit allowZeroMinOut) — otherwise MissingSlippageGuardError.client.queue
const { queue, solver } = await client.queue.deployQueue({ operator, owner });
await (await client.queue.request({ want:"USDC", amount: shares, price: 999_000n, deadline })).wait();
await client.queue.getRequest(user, "USDC"); // { deadline, atomicPrice, offerAmount, inSolve }
await client.queue.isRequestValid(user, "USDC");
await (await client.queue.solve({ want:"USDC", users:[user], minAssetsOut, maxAssets })).wait();
price is the holder's limit (want base units per 1.0 whole share) — protection from a bad fill.
client.monitor (read-only)
const snap = await client.monitor.snapshot(); // NAV/AUM/supply + pause/fee/bounds state
const flows = await client.monitor.flows({ fromBlock }); // deposits/withdrawals/queue
const alerts = client.monitor.checkAlerts(snap, prev, { maxRateMoveBps: 100 });
import { renderDashboard } from "@lucidly/sdk";
fs.writeFileSync("dash.html", renderDashboard(snap, flows, alerts));
LucidlyAgent
import { LucidlyAgent } from "@lucidly/sdk";
const agent = new LucidlyAgent(client);
const res = await agent.handle("supply 1000 USDC into Aave", { vault });
// res.ok === false & res.message explains the remedy if no matching leaf exists
The agent parses a prompt to a structured intent and routes it through the same guarded SDK calls — it cannot execute anything outside the vault's root.
Connect Lucidly to your agent
Bring the same deployment + strategy-execution environment into your preferred agent as a connector.
Lucidly is surface-agnostic. The engine and its safety layer (Merkle-leaf guard, preflight, slippage guards) live in @lucidly/sdk; every surface is a thin client over it. So you can drop Lucidly into whatever agent you use and get the identical guarded execution and deploy environment:
| Way to embed | Best for | What the agent gets |
|---|---|---|
| MCP server @lucidly/mcp | Claude, Cursor, Codex, OpenCode, Continue… | 15 typed tools (deploy, strategy, queue, monitor, agent) over stdio |
| Claude plugin / skill | Claude Code & Cowork | the lucidly skill + the bundled MCP connector |
| CLI as a tool | any agent with shell access | lucidly-cli … commands |
| SDK import | custom agents / backends | import { LucidlyClient } from "@lucidly/sdk" |
MCP server (@lucidly/mcp)
A Model Context Protocol server that exposes Lucidly as tools any MCP-capable agent can call. No install required — every MCP client config below uses npx -y @lucidly/mcp, which fetches the latest published version on demand. Reads need no key; writes require LUCIDLY_PK.
# run it standalone (lists tools over stdio)
npx -y @lucidly/mcp
# or install globally
npm install -g @lucidly/mcp && lucidly-mcp
Reads (no signer)
lucidly_vault_view·lucidly_aum·lucidly_accountant_ratelucidly_monitor_snapshot·lucidly_monitor_flowslucidly_strategy_has_leaf·lucidly_roles_has_rolelucidly_queue_get_request·lucidly_preflight
Writes (require LUCIDLY_PK)
lucidly_vault_deploy·lucidly_vault_finalizelucidly_strategy_aave_supplylucidly_queue_request·lucidly_queue_solvelucidly_agent— returns a parse → guard → result reasoning trace
| Env | Notes |
|---|---|
RPC_URL | required — JSON-RPC endpoint |
CHAIN | ethereum|base|arbitrum|baseSepolia or id (default base) |
LUCIDLY_PK | signer — required only for write tools |
LUCIDLY_VAULT / LUCIDLY_DEPLOYMENT | active vault address / deployment-record JSON path |
Connect from Claude / Cursor / Codex / OpenCode
Every MCP client uses the same command + args + env shape. Omit LUCIDLY_PK for a read-only (monitoring) connection.
Claude Desktop / Claude Code
{
"mcpServers": {
"lucidly": {
"command": "npx",
"args": ["-y", "@lucidly/mcp"],
"env": { "RPC_URL": "https://mainnet.base.org", "CHAIN": "base", "LUCIDLY_PK": "0xYOURKEY" }
}
}
}
Cursor — .cursor/mcp.json
{ "mcpServers": { "lucidly": { "command": "npx", "args": ["-y", "@lucidly/mcp"],
"env": { "RPC_URL": "https://mainnet.base.org", "CHAIN": "base" } } } }
OpenCode — opencode.json
{ "mcp": { "lucidly": { "type": "local", "command": ["npx", "-y", "@lucidly/mcp"],
"environment": { "RPC_URL": "https://mainnet.base.org", "CHAIN": "base" } } } }
Codex / Continue and other MCP clients accept the same stdio command.
Claude plugin (ships the connector)
For Claude Code & Cowork, install the lucidly plugin — it bundles the skill and the MCP connector, so one install gives Claude the knowledge and the 15 live tools.
⬇ Download the Lucidly plugin (.plugin) — then drag it into Cowork to install.
| Path in the plugin | What it is |
|---|---|
skills/lucidly/SKILL.md | the skill (workflows + references) |
.mcp.json | auto-registers the lucidly MCP server via npx -y @lucidly/mcp (zero-install, auto-updates) |
env in the plugin's .mcp.json (add LUCIDLY_PK, change RPC_URL/CHAIN). For offline / pinned-version operation, npm install -g @lucidly/mcp and set "command": "lucidly-mcp" in .mcp.json.CLI & SDK as agent tools
lucidly-cli monitor snapshot # read-only, no key
lucidly-cli agent "supply 1000 USDC into Aave" # guarded single-prompt op
import { LucidlyClient, LucidlyAgent } from "@lucidly/sdk";
const client = new LucidlyClient({ chain: "base", rpcUrl, signer });
const res = await new LucidlyAgent(client).handle(userPrompt); // parse → guard → execute
Guides
Step-by-step how-tos with copy-paste commands.
Permissionless deploy via LucidlyDeploymentFactory
The factory at a single canonical address per chain lets anyone deploy a Lucidly vault in one transaction, no approve, no upfront fee — caller pays gas only. The factory CREATE2-deploys the full Boring Vault stack, sets OWNER = Lucidly Treasury and STRATEGIST = msg.sender, applies the 0.05% Accountant platform fee with the Treasury as recipient, and finalises ownership — all atomically. The factory keeps no residual authority. Lucidly's revenue is the streaming mgmt fee on TVL, accrued by the Accountant on every NAV update; never an upfront sticker. Source: lucidly-deploy/script/LucidlyDeploymentFactory.sol.
What's user-controlled (in DeployParams): name, symbol, decimals, starting NAV, rate bounds + min update delay, performance fee, share-lock seconds, allowed deposit assets, salt (for CREATE2-deterministic addressing across chains).
What's enforced by the factory (not user-controlled): owner (= treasury), strategist (= msg.sender), accountant.platformFee (= 5 bps), accountant.platformFeeRecipient (= treasury).
# One transaction per chain — no approve, no fee pull, no Lucidly-side service in the loop.
cast send $LUCIDLY_FACTORY "deployVault((bytes32,string,string,uint8,uint96,uint16,uint16,uint24,uint16,uint64,address,address,address[],bool))" \
'(0xSALT,"Treasury USDC Yield","lucUSDC",6,1000000,10050,9950,3600,0,0,0xRATE_UPDATER,0xSOLVER,[0xUSDC],true)' \
--rpc-url $RPC_URL --private-key $PK
Same address across chains: use the same salt + identical DeployParams on every chain — CREATE2 lands the BoringVault at the same address everywhere. Then wire LayerZero peers on the OFT teller and shares bridge between chains. Admin (factory owner only): setTreasury(addr), setPlatformFeeBps(uint16) (capped at 100 bps), setUsdc/setBalancerVault/setWeth, transferOwnership, sweepStuckToken. These only affect future deploys — existing vaults are immutable.
Deploy the factory itself with script/DeployLucidlyDeploymentFactory.s.sol — defaults LUCIDLY_TREASURY to 0x131d…f636, sets the initial platform fee to 5 bps (0.05%), and owns the contract to FACTORY_OWNER (defaults to the deployer key). One run per chain.
Deploy to an owner address
Use the env-driven runner (preflight → deploy → wire queue → finalize → verify). Rehearse on Base Sepolia first.
cd lucidly-sdk && npm run build
RPC_URL=https://sepolia.base.org LUCIDLY_PK=0xYOURKEY \
PARAMS=../lucidly-deploy/config/live-vault.sdk.json \
OWNER_ADDRESS=0xYourOwner SEED_MODE=none DRY_RUN=1 \
node scripts/deploy-live.mjs # preflight only; drop DRY_RUN to broadcast
On mainnet, drop SEED_MODE (defaults to repointed) and point OWNER_ADDRESS at the address that should own the vault (typically the Lucidly Treasury, or your own governance address). Full runbook: lucidly-deploy/GOLIVE.md.
Enable & run a strategy
lucidly-cli integration enable AaveV3 --assets USDC # deploy decoder, set root
lucidly-cli roots show # confirm the on-chain root
lucidly-cli aave supply USDC 1000 # leaf-verified, one tx
If you skip enable, the strategy is blocked by the leaf-existence guard with the exact remedy — by design.
Queued redemption
lucidly-cli queue deploy # once per vault
# holder (their key): offer shares for USDC at a limit price
lucidly-cli queue request USDC 100 --price 0.999 --deadline 3600
lucidly-cli queue status 0xHolder USDC # valid? read-only
# operator/SOLVER (after any share-lock clears): fulfill atomically
lucidly-cli queue solve USDC 0xHolder --min-out 0.99
Monitoring & dashboard
lucidly-cli monitor snapshot --from <deployBlock> # JSON: snapshot + flows + alerts
lucidly-cli monitor dashboard --out dash.html # self-contained HTML
Single-prompt ops
lucidly-cli agent "deploy a USDC vault on Base called Treasury"
lucidly-cli agent "supply 1000 USDC into Aave" # BLOCKED until you enable AaveV3
lucidly-cli agent "how do holders redeem?"
Every prompt is parsed → guarded (leaf/preflight) → simulated → executed. Blocked actions explain why.
Demos
Runnable and interactive artifacts that show the stack working end-to-end.
Interactive Pro UI
A self-contained app — deploy wizard, strategy tree, queue console, monitor, and the ⌘K prompt console. Reads everything live from chain (no mock data).
Monitoring dashboard
The renderDashboard() output: KPI cards, health badges, alerts, and a flows chart in one self-contained HTML file.
Install the plugin
The packaged plugin — skill + bundled MCP connector, auto-registered on install.
On-chain validations
Fork demos that prove deploy, strategy, queue, monitoring and go-live end-to-end.
See the commands below.
On-chain validation demos (Base fork)
Each script boots an anvil fork of Base and exercises a real flow against the audited engine:
cd lucidly-sdk && npm run build
BASE_RPC_URL=https://mainnet.base.org bash scripts/run-fork-tests.sh
# ├─ deploy + deposit/withdraw round-trip
# ├─ strategy round-trips (Aave supply/withdraw, Uniswap swap)
# ├─ syUSD baseline seeding (verbatim + repointed)
# ├─ pre-mainnet safety guards
# ├─ AtomicQueue queued-redemption round trip (scripts/queue-fork.mjs)
# ├─ monitoring snapshot/flows/alerts + dashboard (scripts/monitor-fork.mjs)
# ├─ evaluations.json capability suite (10/10)
# ├─ go-live dry run (finalize to an owner address)
# └─ live deploy runner rehearsal (scripts/deploy-live.mjs)
The MCP connector ships with stdio smoke tests too: node scripts/smoke.mjs (lists the 15 tools) and node scripts/agent-smoke.mjs (shows the agent reasoning trace).
Using the Pro UI
The visual surface — live, read-only by default, no mock data.
The UI (index.html) reads everything live from chain via read-only RPC. Modules:
- Overview — live KPIs (NAV, AUM, supply, free liquidity), health badges, alerts derived from state, on-chain activity.
- Deploy — a command generator: fill params → get the exact
deploy-live.mjs+ CLI commands (the UI never holds your key). - Strategies — the live manage root with the leaf model; enable-protocol commands.
- Queue — live queue/solver status + a live holder-request lookup; request/solve commands.
- Monitor — live KPIs, pause badges, alerts, and an events flow chart.
- ⌘K prompt console — read queries run live; actions are guarded against the real root and surfaced as exact commands.