@dfm-fi/agent
v0.2.582
Published
DFM v2 MCP server — drive the full DTF launch → deposit → redeem (+ manage) lifecycle from an AI agent against the live API.
Downloads
3,082
Maintainers
Readme
@dfm-fi/agent — DFM v2 Agent Test Harness (MCP)
An MCP (Model Context Protocol) server that lets an external AI agent drive the
full DFM v2 launch → deposit → redeem DTF flow against the live API
(https://n2-api.dfm.finance/api/v2) programmatically — no web-UI clicking.
This is a test harness for the founder + partner: point your own AI agent (Claude Desktop / Claude Code / any MCP client) at the live closed-mainnet API, authenticate with your own test-wallet keypair loaded locally, and run the whole vault lifecycle — create a DTF, deposit USDC, redeem back to USDC — from the agent.
⚠️ This is a real-money Solana mainnet system. Read the Security section.
Prerequisites
- Node 20+ (ESM build).
- To use it: nothing else —
npx -y @dfm-fi/agentpulls the published, self-contained package (no monorepo clone, nonpm install). - To hack on it from source: this package ships its own
package-lock.json, sonpm ciinsideagent/builds it standalone (that's exactly what the publish CI does). - A dedicated throwaway Solana test wallet (NOT the protocol wallet), funded
with a few dollars of USDC (mainnet mint
EPjFW…) + a little SOL for fees, and allowlisted for the closed alpha (send its pubkey to an admin). - For writes: no client RPC is required — the backend broadcasts your signed
tx (
POST /tx/submit). SetHELIUS_RPC_URLonly if you want to submit through your own RPC instead (optional override; power users + the test harness).
Security model (read first)
- The agent authenticates as its own test wallet, whose keypair is loaded
locally by you from a file path (
DFM_AGENT_KEYPAIR_PATH) or an inline env (DFM_AGENT_KEYPAIR_JSON). The keypair never crosses the MCP boundary — it is never a tool argument, never in a tool result, never logged. Only the public key is ever surfaced. - Never use the protocol wallet (
9GjE…) here. Use a dedicated throwaway test wallet funded with a few dollars of USDC. The harness hard-refuses to load the known protocol pubkey as a tripwire. - The test wallet must be on the closed-alpha access allowlist — otherwise
SIWS sign-in is rejected server-side (403), so it can't reach any protected
endpoint. An admin adds it via the
/opsAccess panel (POST /access/allowlist). - All real-money WRITE tools (deposit / redeem) are gated behind
DFM_AGENT_WRITE_ENABLED=trueand default OFF. With write off, those tools refuse and never touch the chain or load the keypair.
Auth + signing flow (how it works)
SIWS (Sign In With Solana), signed locally, exchanged for a Bearer session:
POST /auth/nonce { wallet }→{ nonce, message }— the server builds the exact message (domain-bound ton2.dfm.finance, nonce embedded).- Sign that exact message locally with the test keypair (Ed25519 → base58).
POST /auth/siws { wallet, signature, message, client: 'native' }→{ accessToken, … }. The server also enforces the access gate here.- Authed calls send
Authorization: Bearer <accessToken>; a 401 triggers one transparent re-auth + retry.
The WRITE flows then follow prepare → sign locally → submit → (confirm/poll):
- launch:
/vaults/create/prepare→ one unsigned v0 tx → sign → submit →/vaults/create/confirm. - deposit / redeem:
/zap-…-v2/open/prepare→ sign → submit → poll/zap-v2/statusuntil the backend orchestrator has cranked all swap legs →/zap-…-v2/close/prepare→ sign → submit. The agent signs only the two user-side boundary transactions; the backend cranks the permissionless legs.
Tools
| Tool | Surface | Endpoint | Notes |
|------|---------|----------|-------|
| whoami | read | /access/status | Test-wallet pubkey, cluster, write flag, gate status. Call first. |
| list_dtfs | read | GET /vaults | Lists vaults (address, symbol, TVL, fees, basket). Public. |
| get_vault | read | GET /vaults/:address | Full vault detail + weights + fee structure. Public. |
| zap_status | read (auth) | GET /vaults/:address/zap-v2/status/:user | Decoded escrow + per-leg crank progress, or null. |
| get_portfolio | read (auth) | GET /portfolio | Session wallet's positions (API reads wallet from JWT). |
| launch_vault | write (gated) | create prepare/confirm | Create a DTF. Basket sums to 10000 bps, ≤15 assets. |
| deposit | write (gated) | zap-in-v2 open/close | Full zap-in lifecycle. RAW 6dp USDC. Resumes an open escrow. |
| redeem | write (gated) | zap-out-v2 open/close | Full zap-out lifecycle. RAW 6dp shares. Resumes an open escrow. |
| zap_v2_cancel | write (gated) | zap-v2/cancel/prepare | Return a stalled escrow's holdings to the wallet, as-is. |
| zap_v2_update_envelope | write (gated) | zap-v2/update-envelope/prepare | Loosen the pinned floors (auto-derived from the live escrow) to unstick a crank. |
| update_vault_assets | write (gated) | assets/prepare + confirm | MANAGER: full-replace a vault's basket (sums to 10000, ≤15, no dupes; manual-mode + 24h timelock). Your own vault only. |
| update_management_fee | write (gated) | POST /fees/:address/update-config | MANAGER: set the management fee (≤2000 bps). Reverts on immutable-fee vaults (which is what this agent creates). |
| transfer_admin | write (gated) | :address/transfer-admin | ⚠️ MANAGER, HIGH-SENSITIVITY: initiate handing vault control to another key (the new admin must ACCEPT). Your own vault only. |
| init_rebalancer | write (gated) | POST /rebalance/:address/init/prepare | MANAGER: enable rebalancing (create the vault's RebalancerConfig, creator pays rent). Bundled into launch_vault automatically — only needed for older/opt-out/oversized vaults. Your own vault only. |
| rebalance | write (gated) | POST /rebalance/:address/execute/prepare | MANAGER: execute a rebalance swap (sell amountIn raw of inputMint → outputMint, both basket assets) — one-signature approve→execute→revoke. On-chain ≤slippage + Pyth NAV-loss guards. Your own vault only. |
Prompt-injection / safety model
The harness is boxed in by design, so a prompt-injected agent (tricked by malicious text it reads while browsing vaults) is bounded:
- No "send funds to an address" tool exists.
redeemalways pays your own wallet;depositpulls from it. There is no way to route money to an attacker. - Every write only touches a vault/wallet the signed-in test wallet OWNS or ADMINS (the API enforces
vault.admin == caller), and amounts/slippage are capped by the on-chain guards (≤10% slippage, per-legmin_outs, the drain-guard, $10 floor, ≤15 assets, immutable fees). - The keypair never enters the agent's context (only the pubkey), so injection can't exfiltrate it; the write-gate (
DFM_AGENT_WRITE_ENABLED) is a hard local kill-switch. - So the worst a hijacked agent can do is make you mis-spend your own money within the contract limits — never the protocol, never other users, never key theft. The two actions to watch — only ever invoke them from an explicit human ask, never inferred from a vault's name/description or other untrusted text:
transfer_admin— hands away control of your own vault.update_vault_assets— full-replaces your own vault's basket with arbitrary (registered, Pyth-mapped) mints + weights; on the next rebalance the vault's value migrates into them. Bounded (own vault, API-validated mints, ≤15 assets, manual-mode + 24h timelock) so it stays inside your own money, but it IS a basket-redirection an injected agent could trigger.
Install
One command — @dfm-fi/agent is live on npm
Published at @dfm-fi/agent. The package is self-contained (no monorepo deps) and ships its built dist/, so npx fetches + runs it — no clone, no build.
The simplest possible install — one command, auto-registers the MCP server:
npx -y @dfm-fi/agent installThat detects the claude CLI and runs the registration for you (and always prints the manual command + Claude Desktop config as a fallback). Add real-money tools with:
npx -y @dfm-fi/agent install --write --keypair /abs/path/test-wallet.json(No Solana RPC key needed — the DFM backend broadcasts your signed tx. --dry-run prints what it would do without registering.)
Read-only:
claude mcp add dfm -- npx -y @dfm-fi/agentFull write-enabled (Claude Code, no RPC key needed):
claude mcp add dfm \
--env DFM_AGENT_KEYPAIR_PATH=/abs/path/test-wallet.json \
--env DFM_AGENT_WRITE_ENABLED=true \
-- npx -y @dfm-fi/agentClaude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"dfm": {
"command": "npx",
"args": ["-y", "@dfm-fi/agent"],
"env": {
"DFM_AGENT_KEYPAIR_PATH": "/abs/path/test-wallet.json",
"DFM_AGENT_WRITE_ENABLED": "true"
}
}
}
}(For read-only, drop DFM_AGENT_WRITE_ENABLED — the read tools need no keypair.
Optional: add --env HELIUS_RPC_URL=https://… to submit through your OWN RPC
instead of the backend.)
Already published at
@dfm-fi/agent. To cut a new version, see Releasing / updating below — a version bump auto-publishes via GitHub, or runagent/scripts/release.shfrom your machine.
From source — works today, no publish needed
cd dfm-v2/agent && npm run build # tsc -b → dist/
# then point your MCP client at the built binary (one line):
claude mcp add dfm -- node /abs/path/dfm-v2/agent/dist/mcp-server.jsReleasing / updating
The published npm version is what npx -y @dfm-fi/agent installs, so updating users = publishing a new version. There is nothing to bump. The version is derived automatically:
version = <major>.<minor>.<git commit count> e.g. 0.2.474where the commit count is the same v.N shown on the web nav and the API /health. So agent v.474 ⇒ @dfm-fi/[email protected] — a 1:1 mapping. (Edit agent/package.json's version only to move the major/minor base line; the patch is always the commit count.)
A) Hands-off — just push, GitHub publishes (preferred)
.github/workflows/publish-agent.yml watches agent/** on dev/main. Every push that touches agent/** auto-publishes the new 0.2.<commitcount> version — only if it isn't already on npm (idempotent: a dev→main sync of the same commit publishes once; a registry blip fails loud, never a silent skip). It builds isolated from the monorepo with --ignore-scripts (no dependency lifecycle code runs on the VPS runner). So updating the package is just your normal flow:
git add agent/… CHANGELOG.md && git commit -m "feat(agent): …"
git push origin dev && git push origin HEAD:main # → @dfm-fi/[email protected].<N> auto-publishesThe workflow runs on the self-hosted VPS runner (GitHub-hosted runners are billing-blocked here). Auto-publish on push has no required-reviewer gate by design — frictionless releases for the closed, team-only dev/main (same posture as the auto-deploy). Forks can't trigger it (push events only).
One-time setup: add a granular npm token (permission Read+Write, scoped to only the
@dfm-fi/agentpackage — minimal blast radius) as the repo secretNPM_TOKEN:gh secret set NPM_TOKEN --repo DFM-Finance/NEXUS2 # paste the token at the prompt (never commit it)or via Settings ▸ Secrets and variables ▸ Actions ▸ New repository secret. Until it's set, the workflow fails loudly at the token step with that instruction (no publish, no side effects).
B) From your machine — one command
agent/scripts/release.sh computes the same 0.2.<commitcount> version, skips if it's already on npm, builds an isolated --ignore-scripts tarball, and publishes the exact built artifact — directly to npm if a local token is present (env NPM_TOKEN, or ~/.config/dfm/npm-token, chmod 600), otherwise it delivers the tarball to the npm-authed laptop (Taildrop → iMessage fallback) and prints the one command to finish there.
agent/scripts/release.shIt never takes the token as an argument or echoes it (a chmod-600 temp .npmrc removed by an EXIT trap). The version tracks committed history, so commit your agent changes first, then run it (or just push and let path A do it — idempotent either way).
Quick start (read-only — safe)
# Run the MCP server against the live API
DFM_API_URL=https://n2-api.dfm.finance \
DFM_AGENT_KEYPAIR_PATH=/abs/path/test-wallet.json \
npm start # = node dist/mcp-server.jsOr exercise the read path without MCP:
DFM_API_URL=https://n2-api.dfm.finance \
DFM_AGENT_KEYPAIR_PATH=/abs/path/test-wallet.json \
npx tsx src/examples/list-and-status.tsLoading the test keypair
Generate a dedicated throwaway wallet and fund it with a little USDC + SOL:
solana-keygen new --no-bip39-passphrase -o ~/dfm-test-wallet.json
solana-keygen pubkey ~/dfm-test-wallet.json # send this pubkey to an admin to allowlistThen either point the agent at the file (preferred):
export DFM_AGENT_KEYPAIR_PATH=$HOME/dfm-test-wallet.json…or inline the JSON array (e.g. in a secret manager):
export DFM_AGENT_KEYPAIR_JSON="$(cat ~/dfm-test-wallet.json)"Enabling the real-money launch → deposit → redeem loop
Only after the parent has reviewed the write path (create-flow.ts,
zap-v2-flow.ts, recovery-flow.ts, signing.ts, submit.ts):
export DFM_API_URL=https://n2-api.dfm.finance
export DFM_AGENT_KEYPAIR_PATH=$HOME/dfm-test-wallet.json # allowlisted + funded
export DFM_AGENT_WRITE_ENABLED=true # the kill-switch
# export HELIUS_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY # OPTIONAL — submit via your own RPC
# End-to-end example: launch a demo DTF, deposit $12, redeem the full position.
# (Runs as a SAFE DRY-RUN — printing the calls it would make — when write is OFF.)
npx tsx src/examples/launch-deposit-redeem.tsCopy-paste walkthrough (in an MCP client / agent)
1. whoami
→ confirm the test-wallet pubkey + that accessGate.allowed is true.
2. launch_vault
{
"name": "My First DTF",
"symbol": "MYDTF",
"assets": [
{ "mint": "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", "symbol": "ETH", "allocationBps": 5000 },
{ "mint": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "symbol": "JUP", "allocationBps": 5000 }
],
"managementFeeBps": 100,
"exitFeeBps": 100
}
→ returns { vault: "<address>", signature, ... }. Note the vault address.
(These two mints are already registered + Pyth-mapped on mainnet. To use
others, list candidates with GET /assets and make sure each is registered.)
3. deposit
{ "vault": "<address from step 2>", "usdcAmount": "12000000" } // $12 raw (6dp)
→ drives open → orchestrator cranks → close; returns when shares are minted.
4. get_portfolio
→ read the resulting raw share balance for that vault.
5. redeem
{ "vault": "<address>", "shares": "<raw shares from step 4>" }
→ drives open → cranks → close; returns net USDC paid (minus the 1% exit fee).
If a deposit/redeem STALLS:
zap_status { vault, user } → inspect per-leg crank progress + failures
zap_v2_update_envelope { vault } → loosen the floors 5% and let the crank retry
zap_v2_cancel { vault } → give up and get the held funds back, as-isNotes:
- Amounts are RAW 6-decimal: $12 =
"12000000", 1 share ="1000000". - The first deposit into a vault must clear ~$12 so it stays above the $10 floor after slippage.
allocationBpsMUST sum to exactly 10000 across the basket; ≤15 assets on mainnet.deposit/redeemare resumable: if a prior open landed but its close never did, calling the same tool again resumes (poll → close) instead of re-opening.
Configure Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"dfm": {
"command": "node",
"args": ["/abs/path/dfm-v2/agent/dist/mcp-server.js"],
"env": {
"DFM_API_URL": "https://n2-api.dfm.finance",
"DFM_AGENT_KEYPAIR_PATH": "/abs/path/dfm-test-wallet.json",
"SOLANA_CLUSTER": "mainnet-beta"
}
}
}
}(Leave DFM_AGENT_WRITE_ENABLED unset for a read-only agent. Add
HELIUS_RPC_URL + DFM_AGENT_WRITE_ENABLED=true only when enabling writes.)
Environment variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| DFM_API_URL | No | https://n2-api.dfm.finance | API base (the /api/v2 prefix is auto-appended). |
| DFM_AGENT_KEYPAIR_PATH | For auth | — | Path to a Solana secret-key JSON file (preferred). The wallet must be allowlisted + funded (USDC + a little SOL). |
| DFM_AGENT_KEYPAIR_JSON | For auth | — | The secret-key JSON array inline (alternative; e.g. injected from a secret manager). |
| DFM_AGENT_WRITE_ENABLED | No | false | Master kill-switch for all write tools (launch/deposit/redeem/cancel/update-envelope). |
| HELIUS_RPC_URL / DFM_RPC_URL | No (optional) | — | OPTIONAL override — submit through your OWN RPC instead of the backend broadcast (POST /tx/submit). Writes need NO client RPC by default; mainnet never uses the rate-limited public RPC. |
| SOLANA_CLUSTER | No | mainnet-beta | mainnet-beta or devnet. |
Architecture
Agent / MCP client
│ stdio (MCP)
▼
MCP server (this package)
├─ read tools ─────────► DFM v2 API (public + authed GET)
└─ write tools (gated) ─► DFM v2 API /prepare ──► unsigned v0 tx(s)
│
sign LOCALLY with test keypair (signing.ts)
│
submit via RPC (submit.ts) — rebroadcast in-window,
│ re-prepare on blockhash expiry
┌─────────────────────┴─────────────────────┐
launch: confirm via /create/confirm deposit/redeem: poll /zap-v2/status
until legs cranked,
then /close → sign → submitThe backend orchestrator (server-side) cranks the permissionless Jupiter swap legs — the agent only signs the two user-side boundary transactions. The agent does not crank legs and does not hold the protocol key.
Source map
| File | Role |
|------|------|
| src/mcp-server.ts | MCP stdio server; registers the 13 tools (5 read + 8 write) + the write gate. |
| src/config.ts | Env → AgentConfig (API URL, cluster, RPC, write flag, keypair source). |
| src/session.ts | SIWS sign-in → Bearer session; in-flight dedup; transparent re-auth on 401. |
| src/signing.ts | The signing boundary — loads the local keypair, signs SIWS + v0 txs. Refuses the protocol wallet; never logs key material. |
| src/submit.ts | RPC submit + confirm; rebroadcast in-window; BlockhashExpiredError / OnChainRevertError. |
| src/api-client.ts | Typed DFM v2 API client (public + authed; create + zap-v2 prepare/confirm). |
| src/create-flow.ts | launch_vault driver: prepare → sign → submit → confirm (+ ghost-vault recovery). |
| src/zap-v2-flow.ts | deposit / redeem lifecycle drivers (resume-aware, close-retry). |
| src/recovery-flow.ts | zap_v2_cancel + zap_v2_update_envelope (floors auto-derived from the live escrow). |
| src/types.ts | Response shapes mirrored against the live API. |
What remains for the parent to review/enable
All write tools are fully implemented but gated OFF (DFM_AGENT_WRITE_ENABLED
unset). Before flipping the gate, review the real-money path (search for the
// REVIEW: real-money markers):
launch_vault(create-flow.ts) — pulls the on-chain creation fee from the test wallet; the test wallet becomes the vault admin. Confirm the basket / fee defaults are what you want.deposit/redeem(zap-v2-flow.ts) — move USDC ↔ shares. They are resume-aware (won't double-open) and retry the close on blockhash expiry.zap_v2_cancel/zap_v2_update_envelope(recovery-flow.ts) — stall recovery.update_envelopeonly ever LOOSENS, with the new floors derived from the live escrow's pinnedminOuts(never hand-supplied).signing.ts/submit.ts— the signing boundary + submit robustness. Confirm the protocol-wallet tripwire + the "secret never leaves this module" invariant.
To roll back at any time: unset DFM_AGENT_WRITE_ENABLED (the write tools then
refuse and never touch the chain or load the keypair).
