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.

Supported chains: Ethereum (1), Base (8453), Arbitrum (42161), and Base Sepolia (84532) for rehearsals.

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):

SurfaceUse it forExample
UIVisual ops, demos, monitoringDeploy wizard, strategy tree, queue console
CLI @lucidly/cliScripting, CI, operatorslucidly-cli aave supply USDC 1000
SDK @lucidly/sdkApps, bots, backendsclient.strategy.aave.supply(...)
Prompt / MCP @lucidly/mcpSingle-sentence ops, agentsagent "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 dir is ~/.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):

ContractRole
RolesAuthoritysolmate role registry; gates every privileged call
BoringVaultholds assets & is the ERC20 share token (not ERC4626)
ManagerWithMerkleVerificationexecutes strategies that match the Merkle root
AccountantWithRateProvidersthe NAV / exchange rate, with bounds & fees
TellerWithMultiAssetSupportdeposits (mint) and bulkWithdraw (redeem)
AtomicQueue + LucidlyRedeemSolverasynchronous, illiquid-safe redemptions
DecoderAndSanitizerone 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.

Auto-seeding. Deploying with 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:

IDRoleCan
1MANAGERcall vault.manage (held by the Manager)
2 / 3MINTER / BURNERmint/burn shares (held by the Teller)
7STRATEGISTexecute the manage root
8OWNERgovern (handed to an owner address after finalize)
11UPDATE_EXCHANGE_RATEpush NAV updates
12SOLVERbulk 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

CommandDoes
config showprint 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 viewname/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();
Mainnet swaps require 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 embedBest forWhat the agent gets
MCP server @lucidly/mcpClaude, Cursor, Codex, OpenCode, Continue…15 typed tools (deploy, strategy, queue, monitor, agent) over stdio
Claude plugin / skillClaude Code & Coworkthe lucidly skill + the bundled MCP connector
CLI as a toolany agent with shell accesslucidly-cli … commands
SDK importcustom agents / backendsimport { LucidlyClient } from "@lucidly/sdk"
Same guarantees everywhere. Reads work without a key; writes require a signer and are checked against the vault's on-chain Merkle root + preflight before any transaction is built. An embedded agent cannot execute a call outside the whitelist.

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_rate
  • lucidly_monitor_snapshot · lucidly_monitor_flows
  • lucidly_strategy_has_leaf · lucidly_roles_has_role
  • lucidly_queue_get_request · lucidly_preflight

Writes (require LUCIDLY_PK)

  • lucidly_vault_deploy · lucidly_vault_finalize
  • lucidly_strategy_aave_supply
  • lucidly_queue_request · lucidly_queue_solve
  • lucidly_agent — returns a parse → guard → result reasoning trace
EnvNotes
RPC_URLrequired — JSON-RPC endpoint
CHAINethereum|base|arbitrum|baseSepolia or id (default base)
LUCIDLY_PKsigner — required only for write tools
LUCIDLY_VAULT / LUCIDLY_DEPLOYMENTactive 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 pluginWhat it is
skills/lucidly/SKILL.mdthe skill (workflows + references)
.mcp.jsonauto-registers the lucidly MCP server via npx -y @lucidly/mcp (zero-install, auto-updates)
Installs read-only on Base by default. To enable writes or switch chains, edit the 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).

Open the UI →

Monitoring dashboard

The renderDashboard() output: KPI cards, health badges, alerts, and a flows chart in one self-contained HTML file.

Open the dashboard →

Install the plugin

The packaged plugin — skill + bundled MCP connector, auto-registered on install.

⬇ Download .plugin

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.
Open it in a browser. If a public RPC blocks browser CORS, paste a provider RPC in the banner — the UI never falls back to fake data.

Open the Pro UI →