@lucidly/sdk
v0.3.1
Published
TypeScript SDK for Lucidly tokenized Boring Vaults on EVM. Deploy, Merkle-gated strategies, NAV, roles — plus a streaming LLM research agent (Anthropic / Groq / DeepSeek / OpenAI) for vault + on-chain queries.
Maintainers
Readme
@lucidly/sdk
viem-based TypeScript SDK for deploying and operating tokenized vaults on EVM chains, built on the
Se7en-Seas Boring Vault architecture (pinned commit 0e23e7f, solc 0.8.21).
It mirrors the lucidly skill's documented surface so an agent or CLI can drive it programmatically:
deploy the full stack, whitelist strategies behind a Merkle root, grant strategists, and execute
leaf-verified strategy calls.
Install
npm install @lucidly/sdk viemQuick start
import { LucidlyClient } from "@lucidly/sdk";
import { privateKeyToAccount } from "viem/accounts";
const client = new LucidlyClient({
chain: "base",
rpcUrl: process.env.RPC_URL!,
signer: privateKeyToAccount(process.env.LUCIDLY_PK as `0x${string}`),
});
// Deploy the full Boring Vault stack + wire all roles
const d = await client.vault.deploy({
name: "Lucidly Base USDC", symbol: "lucUSDC", decimals: 6,
baseAsset: "USDC", allowedDepositAssets: ["USDC"], initialRate: 1_000_000n,
strategist: me, rateUpdater: me, solver: me,
});
// Whitelist Aave (deploys decoder, builds leaves, sets the Merkle root)
await client.integrations.enable(["AaveV3"], { assets: ["USDC"] }, d.vault);
// Execute a leaf-verified strategy in one tx (batches approve + supply)
await client.strategy.aave.supply(d.vault, { asset: "USDC", amount: 1_000_000_000n });
// Read NAV for dashboards (no signer required)
const rate = await client.accountant.getRateInQuote("USDC");Modules
| Namespace | Key methods |
|----------------------|------------------------------------------------------------------------|
| client.vault | deploy(), view(), aum(), setActive() |
| client.accountant | getRate(), getRateInQuote(), updateRate() |
| client.teller | deposit(), withdraw(), addAsset(), isAssetSupported() |
| client.roles | grantStrategist(), grant(), revoke(), hasRole(), setManageRoot() |
| client.integrations| enable(), rehydrate(), serialize(), leaves() |
| client.strategy | execute(), aave.supply(), aave.withdraw(), hasLeaf() |
| LucidlyAgent | handle(prompt) — NL routing with a mandatory leaf guard |
| client.researchTools | 10 read-only tools for LLM-driven vault research (see below) |
| ResearchAgent | Streaming tool-use loop over Anthropic / Groq / DeepSeek / OpenAI |
Pure Merkle helpers are exported too: leafHash, leafHashWithPacked, buildTree, proofFor, verify.
Research agent
Drive an LLM through a read-only toolkit over your vault data + on-chain reads + indexer. Four interchangeable providers — bring your own key, swap with one line:
import {
LucidlyClient, ResearchAgent,
AnthropicProvider, GroqProvider, DeepSeekProvider, OpenAIProvider,
} from "@lucidly/sdk";
const client = new LucidlyClient({ chain: "base", rpcUrl: process.env.BASE_RPC! });
// Pick a provider. All four implement the same `LLMProvider` interface.
const provider = new AnthropicProvider({ apiKey: process.env.ANTHROPIC_KEY! });
// const provider = new GroqProvider({ apiKey: process.env.GROQ_KEY! }); // free-tier capable
// const provider = new DeepSeekProvider({ apiKey: process.env.DEEPSEEK_KEY! }); // ~5-10× cheaper
// const provider = new OpenAIProvider({ apiKey: process.env.OPENAI_KEY!, model: "gpt-5-codex" });
const agent = new ResearchAgent(client, { provider });
// Non-streaming (CLI / batch)
const result = await agent.chat("How concentrated is ownership of vault 0xabc…?");
// Streaming (UI / live):
const ac = new AbortController();
await agent.chatStream("Show me delta-neutral policies", {
onTextDelta: (chunk) => process.stdout.write(chunk),
onToolCall: (call) => console.log(`\n[tool] ${call.name}`),
onRateLimit: ({ waitSec }) => console.log(`(waiting ${waitSec}s for rate limit)`),
abortSignal: ac.signal, // user-stop
getStopped: () => false, // polled between iterations and tool calls
});Toolkit (each tool's full description ships in LUCIDLY_RESEARCH_TOOLS):
| Tool | Purpose |
|---|---|
| lucidly.policies | Per-protocol policy status for a vault (decoder/tvlAdapter/oracle clones + enabled leaves) |
| lucidly.holders | Current share-token holders by balance descending |
| lucidly.depositors | Recent deposit ledger entries |
| lucidly.navHistory | NAV/rate update timeline |
| lucidly.policyRequests | Open policy-whitelist requests in the registry |
| morpho.market | Morpho Blue market state by 5-tuple market params |
| morpho.position | Per-account supply/borrow/collateral shares |
| aave.position | Aave V3 user account data (collateral, debt, HF) |
| aave.market | Aave V3 reserve data (APYs, aToken, debt token) |
| chain.tokenInfo | ERC20 metadata for formatting answers |
Resilience: every provider streams via SSE, auto-retries HTTP 429 (parses Retry-After + "try again in Xs" bodies), retries network-blip mid-stream IFF no content has been emitted yet, and honors AbortSignal for user-stop.
Production keys: for browser use, pass dangerousDirectBrowserAccess: true to AnthropicProvider AND use apiBaseUrl to route through your own proxy — otherwise the key is exposed in network requests.
Merkle correctness (the critical invariant)
Leaves are keccak256(abi.encodePacked(decoder, target, valueIsNonZero, selector, packedArgumentAddresses))
and proofs use sorted-pair hashing, matching ManagerWithMerkleVerification + solmate MerkleProofLib
exactly. test-solidity/MerkleParityTest.t.sol proves byte-for-byte parity: it re-derives each leaf the
contract way and verifies the SDK's proofs through the real MerkleProofLib.
Validation
All of the following are validated against an anvil fork of Base mainnet:
scripts/deploy-fork.mjs— full deploy + role wiring + reads (14 checks)scripts/strategy-fork.mjs— enable AaveV3 → leaf-verified approve → real Aave supply/withdraw (7 checks)test-solidity/MerkleParityTest.t.sol— Solidity↔TS Merkle parity (run withforge test)
npm run build
BASE_RPC_URL=https://mainnet.base.org ./scripts/run-fork-tests.shNotes
- Amounts are
bigintbase units (useviem'sparseUnits). - Token params accept a symbol (resolved per active chain) or a raw
0xaddress. - The
vault.deploy()contract artifacts are vendored insrc/artifacts.tsfrom the pinned boring-vault build. - Supported chains: Ethereum, Base, Arbitrum (see
src/chains.ts). Verify addresses before mainnet use.
