dfm-agent について
DFM Agent は、Solana 上の DTF 金庫を管理する自律型 AI スキルで、市場調査から設定決定、オンチェーン展開まで行います。作成前のポリシー評価で処理を制約し、Agent Wallet の秘密鍵はユーザーのマシン内に保持されます。
主な機能
- 01市場を調査してトークンを自律選定
- 02金庫名、配分、手数料を決定
- 03作成前のポリシー評価とドライラン
- 04市場指標 API から流動性と取引量を取得
- 05エージェントがオンチェーン取引に署名して送信
- 06秘密鍵をユーザーのマシン内に保持
活用シーン
- 01金庫候補のトークン調査
- 02資産バスケットとポリシーの組み合わせを試行
- 03Solana DTF 金庫の展開と管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dfm-finance/ dfm-agentskills --skill dfm-agentインストール後、Claude Code で/dfm-agentを入力して読み込まれたか確認します。
同梱ファイルreferences/api-reference.md
詳細
DFM Agent
DFM Agent は、Solana 上の DTF(DeFi Token Fund)ボールト管理を行う完全自律型の AI スキルです。エージェントは市場を調査し、名前、シンボル、資産配分、手数料体系、ポリシー設定など、すべてを決定してオンチェーンにデプロイします。人間による確認は介在しません。ポリシーエンジンが安全策であり、人間による承認ステップではありません。
基本理念
エージェントが作成者です。何をローンチするかを完全に決定できます。
+-------------------------------------------------------------------+
| AUTONOMOUS AGENT FLOW |
| |
| 1. RESEARCH Agent analyzes markets, picks candidate tokens |
| via WebSearch. Then GET /market-metrics for |
| authoritative liquidity/volume numbers (the |
| exact values the policy engine enforces against). |
| |
| 2. DECIDE Agent picks vault name, symbol, assets, |
| allocations, fees, and policy thresholds. |
| |
| 3. VALIDATE POST /policy/dry-run (loop) -- simulate the |
| basket+policy against all pre-launch rules. |
| No on-chain cost. If violations, adjust policy |
| OR basket, retry until clean. |
| |
| 4. DEPLOY Two-step vault creation (policy-gated): |
| a) POST /launch-dtf {basket + policy} -> |
| basket-vs-policy validated server-side. |
| Policy committed. Unsigned tx returned. |
| b) Agent signs tx & submits on-chain. |
| c) POST /dtf-create {tx signature + metadata} -> |
| finalize vault (metadata only, no policy). |
| |
| 5. MANAGE Agent monitors, rebalances, distributes fees. |
| |
| GUARDRAILS: Policy is law before creation. /launch-dtf |
| refuses to build a tx for any basket that |
| violates the agent's own declared policy. |
| NO human confirmation step. |
+-------------------------------------------------------------------+
| 原則 | 詳細 |
|---|---|
| 完全自律型 | エージェントが名前、シンボル、資産、配分、ポリシー、手数料をすべて決定します。確認プロンプトはありません。 |
| 作成前にポリシーを確定 | policy オブジェクトは /launch-dtf リクエストボディ内に含めます。バックエンドは tx を構築する前に、バスケットとポリシーに対して evaluatePreCreation を実行します。違反があると、violations[] 配列全体を含む 400 が返され、オンチェーンには何も反映されません。 |
| ドライランによる事前確認ループ | POST /policy/dry-run は、何もコミットせずに同じ評価結果を返します。/launch-dtf を呼び出す前に、ポリシーとバスケットの組み合わせを無料で試行するために使用します。 |
| メトリクスの信頼できる情報源 | GET /market-metrics は、ポリシーエンジンが適用する正確な liquidity_usd / volume_24h_usd の値を返します。min_amm_liquidity_usd / min_24h_volume_usd を選択する際は、これらの値を使用してください(ウェブから取得したアグリゲーターの数値ではなく)。 |
| 2 段階のボールト作成 | POST /launch-dtf はポリシーを検証・コミットし、未署名の tx を構築します。エージェントが署名して送信します。POST /dtf-create はオンチェーンのメタデータを DB に保存します(ポリシーは不要です。すでにコミットされており、チェーンイベントパイプラインによって自動的に関連付けられます)。 |
| 非カストディアル | Agent Wallet の秘密鍵がユーザーのマシンから外部に出ることはありません。バックエンドが秘密鍵を受け取ることもありません。 |
| エージェント = オンチェーンの権限者 | Agent Wallet は、デプロイするすべてのボールトの永続的なオンチェーン作成者/管理者になります。 |
機密データに関するルール(必須 -- 最初に読むこと)
次の環境変数の値を、ターミナル出力に表示、エコー、記録、または出力してはいけません。
DFM_AUTH_TOKEN-- JWT 認証トークンDFM_AGENT_KEYPAIR-- base58 秘密鍵- 秘密鍵、認証トークン
環境変数が設定されているか確認する場合は、長さのみを確認してください。
node -e 'console.log(process.env.DFM_AUTH_TOKEN ? "set" : "not set")'
次のことは絶対に行わないでください。
# WRONG - exposes token in terminal output
echo $DFM_AUTH_TOKEN
export DFM_AUTH_TOKEN="eyJ..." # WRONG - token visible in bash command
curl -H "Authorization: Bearer eyJ..." ... # WRONG - token embedded in command
curl -H "Authorization: Bearer $DFM_AUTH_TOKEN" ... # WRONG - shell expands and displays it
**すべての API 呼び出しでは、環境変数を process.env 経由で内部的に読み取るインラインの node -e スクリプトを必ず使用してください。**これにより、機密情報が bash コマンド自体に表示されるのを防げます。
API 呼び出しの正しいパターン:
node -e '
const http = require("http");
const https = require("https");
const url = new URL(process.env.DFM_API_URL + "/api/v2/agent/dtf/SYMBOL/state");
const client = url.protocol === "https:" ? https : http;
const req = client.get(url, { headers: { "Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN } }, (res) => {
let data = "";
res.on("data", (chunk) => data += chunk);
res.on("end", () => console.log(data));
});
req.on("error", (e) => console.error("Error:", e.message));
'
**API 呼び出しに curl を使用しないでください。**トークンや鍵がコマンド文字列に表示されないよう、常に process.env 参照を使った node -e を使用してください。
**重要:API 呼び出しにタイムアウトを設定しないでください。**API 呼び出しを行う Bash コマンドを実行するときは、必ず timeout: 600000(10 分)を設定するか、run_in_background: true を使用してください。デフォルトの Bash タイムアウトは 2 分ですが、ボールト作成、リバランス、手数料分配などのオンチェーン操作には短すぎます。タイムアウトによって API 呼び出しが強制終了されないようにしてください。
認証の事前確認(必須)
**API 呼び出しを行う前に、この確認を必ず完了してください。**この手順を省略しないでください。
手順 1:.claude/settings.json に環境変数があることを確認する
Claude Code は、~/.zshrc や ~/.bashrc を読み込まない非対話型のサブプロセスで bash を実行します。ユーザーのターミナルで export を使って設定した環境変数は、Claude Code の bash コマンドからは利用できません。Claude Code に環境変数を渡す信頼できる唯一の方法は、.claude/settings.json を使用することです。
事前確認のたびに、次のスクリプトを実行して ~/.zshrc の環境変数を .claude/settings.json に同期してください。
node -e '
const fs = require("fs");
const path = require("path");
const os = require("os");
const settingsPath = path.join(process.cwd(), ".claude", "settings.json");
let settings = {};
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch {}
if (!settings.env) settings.env = {};
// Reject sentinel/placeholder values that should never be honoured.
const isInvalid = (v) => {
if (!v || typeof v !== "string") return true;
const t = v.trim();
if (!t) return true;
if (t === "+token+" || t === "<token>" || t.startsWith("+")) return true;
if (t.startsWith("\"") || t.endsWith("\"")) return true; // stray quotes from bad templating
return false;
};
let zshrc = "";
try { zshrc = fs.readFileSync(path.join(os.homedir(), ".zshrc"), "utf8"); } catch {}
const envVars = ["DFM_API_URL", "DFM_AUTH_TOKEN", "DFM_AGENT_KEYPAIR", "SOLANA_RPC_URL", "AGENT_WALLET_PATH"];
for (const v of envVars) {
// 1) Keep settings.json value if already valid (it is updated in-process by refresh / launch scripts).
if (!isInvalid(settings.env[v])) continue;
// 2) Pull from ~/.zshrc. Use a global regex and take the LAST match — newest export wins
// when the file accumulates multiple `export VAR=...` lines.
const re = new RegExp("export\\s+" + v + "=[\"\\047]?([^\"\\047\\n]+)[\"\\047]?", "g");
let lastMatch = null, m;
while ((m = re.exec(zshrc)) !== null) lastMatch = m;
if (lastMatch && !isInvalid(lastMatch[1])) {
settings.env[v] = lastMatch[1];
continue;
}
// 3) Final fallback: current process env
if (!isInvalid(process.env[v])) settings.env[v] = process.env[v];
else delete settings.env[v]; // ensure no garbage placeholder lingers
}
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
for (const v of envVars) {
console.log(v + "=" + (!isInvalid(settings.env[v]) ? "set" : "NOT SET"));
}
'
DFM_AUTH_TOKEN が設定されているにもかかわらず、API 呼び出しで 401(Unauthorized/トークンの期限切れ)が返される場合:
- 以下のトークン更新スクリプトを実行します。このスクリプトは
DFM_AGENT_KEYPAIRからエージェントのウォレットアドレスを導出し(ユーザーへの確認は不要です)、エージェントのウォレットアドレスを指定してPOST {DFM_API_URL}/api/v2/agent/token/refresh-by-walletを呼び出し、新しい JWT を.claude/settings.jsonに書き込み、~/.zshrcにある既存のexport DFM_AUTH_TOKEN=行を置き換えます(追記は決してしません。追記すると古いトークンが蓄積し、プリフライト処理で最初に取得される可能性があります)。トークンの値が出力されることはありません。 - スクリプトが
STATUS=successを報告したら、同じセッションで元の操作を再試行します。Claude Code は次の bash 呼び出し時に.claude/settings.jsonを読み込むため、再起動は不要です。
更新方法を独自に考えて実行しないでください。 以前、独自に試した方法で settings.json に文字どおりのプレースホルダー文字列(例:+token+)が書き込まれたことがあります。必ずこのスクリプトをそのまま使用してください。
スクリプトを .claude/refresh-token.js に一度だけ書き込み、その後 node .claude/refresh-token.js で実行します。
const http = require("http");
const https = require("https");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { Keypair } = require("@solana/web3.js");
const bs58 = require("bs58").default || require("bs58");
const apiUrl = process.env.DFM_API_URL;
const agentSecret = process.env.DFM_AGENT_KEYPAIR;
if (!apiUrl) { console.log("ERROR: DFM_API_URL not set"); process.exit(1); }
if (!agentSecret) {
console.log("ERROR: DFM_AGENT_KEYPAIR not set — cannot derive agent wallet for refresh");
process.exit(1);
}
// Derive the agent's on-chain wallet address from the keypair env var.
const agentWalletAddress = Keypair.fromSecretKey(bs58.decode(agentSecret)).publicKey.toBase58();
const payload = JSON.stringify({ agentWalletAddress });
const url = new URL(apiUrl + "/api/v2/agent/token/refresh-by-wallet");
const client = url.protocol === "https:" ? https : http;
const req = client.request(url, {
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
}, (res) => {
let data = "";
res.on("data", (c) => data += c);
res.on("end", () => {
try {
const json = JSON.parse(data);
// Backend response shape: { status, message, data: { token, expires, expiresPrettyPrint, expiresAt } }
const token = json.data?.token || json.token;
if (!token || typeof token !== "string" || token.startsWith("+")) {
console.log("ERROR: No valid token in response: " + data);
process.exit(1);
}
// (1) Update .claude/settings.json
const sp = path.join(process.cwd(), ".claude", "settings.json");
let s = {}; try { s = JSON.parse(fs.readFileSync(sp, "utf8")); } catch {}
if (!s.env) s.env = {};
s.env.DFM_AUTH_TOKEN = token;
fs.mkdirSync(path.dirname(sp), { recursive: true });
fs.writeFileSync(sp, JSON.stringify(s, null, 2));
// (2) REPLACE any existing DFM_AUTH_TOKEN export in ~/.zshrc; never append duplicates.
const zshrcPath = path.join(os.homedir(), ".zshrc");
let zshrc = "";
try { zshrc = fs.readFileSync(zshrcPath, "utf8"); } catch {}
const newLine = "export DFM_AUTH_TOKEN=\"" + token + "\"";
const lineRe = /^\s*export\s+DFM_AUTH_TOKEN=.*$/gm;
if (lineRe.test(zshrc)) {
zshrc = zshrc.replace(lineRe, newLine);
} else {
if (zshrc.length && !zshrc.endsWith("\n")) zshrc += "\n";
zshrc += newLine + "\n";
}
fs.writeFileSync(zshrcPath, zshrc);
// Output ONLY safe info — NEVER the token
console.log("STATUS=success");
console.log("AGENT_WALLET=" + agentWalletAddress);
console.log("DFM_AUTH_TOKEN=set");
} catch (e) {
console.log("ERROR: " + e.message);
process.exit(1);
}
});
});
req.on("error", (e) => { console.log("ERROR: " + e.message); process.exit(1); });
req.write(payload);
req.end();
このスクリプトの実行後も DFM_AUTH_TOKEN が設定されていない場合は、自動エージェントプロフィール作成フローを実行します。
-
ユーザーにDFM に登録したウォレットアドレス(DFM Dashboard でのサインアップ時に使用した Solana の公開鍵)を尋ねます。
DFM Agent を設定するには、DFM Dashboard(https://qa.dfm.finance)に登録した**Solana ウォレットアドレス**が必要です。 ウォレットの公開鍵を貼り付けてください。
-
エージェントのウォレットが存在することを確認します。プロフィールの作成には、エージェントのウォレットの公開鍵が必要です。
DFM_AGENT_KEYPAIRがすでに設定されている場合は、そこから公開鍵を導出します(新しいものを生成してはいけません)。AGENT_WALLET_PATH(デフォルトは~/.dfm/agent-wallet.json)にキーペアファイルが存在する場合は、それを読み込み、公開鍵を導出します。- どちらも存在しない場合に限り、新しいキーペアを生成します。下記の「Agent Wallet -- Keypair Generation」セクションを参照してください。
-
ユーザーからウォレットアドレスが提供され、エージェントのキーペアが存在することを確認したら、エージェントのプロフィール名とユーザー名を自動生成します。以下のルールをよく読んでください。以前のセッションでは名前がごく少数の候補(「Vault Navigator」、「DeFi Navigator」、「Alpha Sentinel」)に偏っていましたが、ユーザー体験を大きく損ないます。
表示名のルール(必須):
- 起動ごとに、本当に異なる語彙から選んだ2語の表示名を付けてください。最大の失敗パターンは、少量の例を読んだモデルが、その例や類似の名前に引きずられることです。その対策として、幅広く意図的に混ぜた候補群を使ってください。下の表を出発点にしつつ、この会話ですでに出てきた語は避けて、表にない新しい組み合わせを自由に考えてください。
- 使用禁止の名前の語幹(どちらの語にも使用しないこと): Navigator、Sentinel、Agent、Bot、Vault、DeFi、Crypto、Solana、Token。これらは以前の例で使われすぎており、名前の候補として飽和しています。どれかを使いたくなったら、やめて表の別の分野から選んでください。
- 使用禁止のフルネーム(すでに使われすぎているため、再使用しないこと): Vault Navigator、DeFi Navigator、Alpha Sentinel、Momentum Agent、Crypto Bot。
- 表示名に16進数/ランダムな接尾辞/数字/
_xxのマーカーを追加しないでください。 それらを使うのはユーザー名だけです(スクリプトが処理します)。表示名は、2語でできたすっきりしたフレーズにしてください。"Vault Navigator A08E 🦞" は、あらゆる点で誤りです。再試行用の接尾辞がユーザーに見えるフィールドに漏れています。"Sapphire Tide" が正しい例です。 - エージェントのウォレットの先頭3〜4文字を創造的なヒントにしてください。 そうすることで、異なるウォレットから確実に異なるテーマが選ばれます。例:エージェントのウォレットが
3UzVで始まる場合、3/U/V を連想させる名前を選びます(例:Triton Vault……いや、Vault は使用禁止なので、Triton Verse や Ultraviolet Drift を試してください)。これはハッシュ化の仕組みではなく、モデルが自然に同じ名前を繰り返そうとする傾向を崩すためのヒントです。 - 起動ごとに、異なる名前を付けてください。 この会話の前回の実行で「Sapphire Tide」が使われた場合、次は位相的に大きく異なる名前を選んでください。Quasar Forge、Boreal Compass などです。Sapphire Drift や Sapphire Reef のような名前は避けてください。
候補群(組み合わせて使用してください。各列から1語ずつ選ぶか、同じ雰囲気で新しい語を考えてください):
神話/宇宙 市場/動き 素材/自然 抽象/概念 Aurora、Orion、Helios、Nyx、Atlas、Perseus、Vega、Lyra、Triton、Pegasus Drift、Tide、Pulse、Surge、Cascade、Compass、Gradient、Vector、Ledger、Index Obsidian、Quartz、Sapphire、Granite、Ember、Amber、Cinder、Boreal、Zephyr、Silt Cipher、Lattice、Horizon、Atlas、Forge、Codex、Verse、Prism、Quasar、Echo Obsidian Pulse、Aurora Compass、Quasar Forge、Sapphire Tide、Boreal Index、Cinder Cipher、Pegasus Lattice のような組み合わせは、いずれもこの方針に沿っています。これらの完全一致の組み合わせは再使用しないでください。 これらは例であり、使用してよい名前の一覧ではありません。近い雰囲気の新しい名前を作ってください。
ユーザー名の生成(別の処理です。重複の回避はスクリプトが行います):
- ユーザー名は、表示名を小文字にして単語の間をアンダースコアでつないだものです(例:Sapphire Tide →
sapphire_tide)。 - ユーザー名にあらかじめ接尾辞を付けたり、ランダム化したりしないでください。
profile-launch.jsスクリプトは409 "Username is already taken"が返された場合、ユーザー名に4桁の16進文字の接尾辞を追加して、最大5回まで自動で再試行します。そのままのベース名を渡し、重複への対処はスクリプトに任せてください。 再試行中も表示名は維持され(変更されるのはユーザー名だけです)、ユーザーには、ユーザー名がsapphire_tide_a08eで再試行された場合でも「Sapphire Tide 🦞」と表示されます。
-
1つのスクリプトでプロフィールを作成し、トークンを保存してください。 API 呼び出し、トークンの抽出、環境変数への書き込みはすべて1つのスクリプト内で行い、トークンがターミナル出力に表示されないようにしてください。また、このスクリプトはユーザー名の重複による409エラーが発生した場合、ユーザー名にランダムな接尾辞を追加して最大5回自動で再試行します。そのため、エージェントに新しい名前を再度尋ねる必要はありません。スクリプトファイルを作成して実行してください。
.claude/profile-launch.jsという名前のファイルに次の内容を書き込み、node .claude/profile-launch.jsで実行してください。const http = require("http"); const https = require("https"); const fs = require("fs"); const path = require("path"); const os = require("os"); const crypto = require("crypto"); const { Keypair } = require("@solana/web3.js"); const bs58 = require("bs58").default || require("bs58"); const apiUrl = process.env.DFM_API_URL; const walletAddress = process.argv[2]; const baseName = process.argv[3]; const baseUsername = process.argv[4]; if (!apiUrl) { console.log("ERROR: DFM_API_URL not set"); process.exit(1); } if (!walletAddress || !baseName || !baseUsername) { console.log("ERROR: usage: node profile-launch.js <walletAddress> <name> <username>"); process.exit(1); } // Derive agent wallet public key from DFM_AGENT_KEYPAIR const agentKeypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR)); const agentWalletAddress = agentKeypair.publicKey.toBase58(); const MAX_ATTEMPTS = 5; const UNAME_RX = /username/i; // disambiguates duplicate-username vs. wallet-already-has-agent // Sanitize base username to allowed charset (alphanumeric + underscore) const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, ""); const cleanBase = sanitize(baseUsername) || "agent"; // Suffix generator: deterministic-looking but unique. 4 hex chars = 65k combos. const suffix = () => crypto.randomBytes(2).toString("hex"); function attempt(usernameToTry, agentNameToTry, n) { const payload = JSON.stringify({ userPublicKey: walletAddress, agentWalletAddress: agentWalletAddress, name: agentNameToTry, username: usernameToTry, metadata: [{ key: "created_by", value: "dfm-agent-skill" }] }); const url = new URL(apiUrl + "/api/v2/agent/profile-launch"); const client = url.protocol === "https:" ? https : http; const req = client.request(url, { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } }, (res) => { let data = ""; res.on("data", (chunk) => data += chunk); res.on("end", () => { let json = null; try { json = JSON.parse(data); } catch { /* fall through */ } const message = json?.message || json?.error || data; // 409 + duplicate-username -> regenerate username, retry. Do NOT retry on // "agent profile already exists for this wallet" — that is unrecoverable here. if (res.statusCode === 409 && UNAME_RX.test(String(message)) && !/wallet/i.test(String(message))) { if (n >= MAX_ATTEMPTS) { console.log("ERROR: username conflicts after " + MAX_ATTEMPTS + " attempts. Last tried: " + usernameToTry); process.exit(1); } const nextUsername = cleanBase + "_" + suffix(); const nextName = baseName; // keep display name; only username needs to be unique console.log("RETRY=username_taken attempt=" + (n + 1) + " next_username=" + nextUsername); return attempt(nextUsername, nextName, n + 1); } if (res.statusCode === 409) { console.log("ERROR: 409 Conflict (not username-related): " + message); process.exit(1); } if (res.statusCode < 200 || res.statusCode >= 300) { console.log("ERROR: HTTP " + res.statusCode + ": " + message); process.exit(1); } const token = json?.data?.token?.token; if (!token) { console.log("ERROR: No token in response. Response: " + data); process.exit(1); } // Write to .claude/settings.json const sp = path.join(process.cwd(), ".claude", "settings.json"); let s = {}; try { s = JSON.parse(fs.readFileSync(sp, "utf8")); } catch {} if (!s.env) s.env = {}; s.env.DFM_AUTH_TOKEN = token; fs.writeFileSync(sp, JSON.stringify(s, null, 2)); // REPLACE any existing DFM_AUTH_TOKEN export in ~/.zshrc; never append duplicates. const zshrcPath = path.join(os.homedir(), ".zshrc"); let zshrc = ""; try { zshrc = fs.readFileSync(zshrcPath, "utf8"); } catch {} const newLine = "export DFM_AUTH_TOKEN=\"" + token + "\""; const lineRe = /^\s*export\s+DFM_AUTH_TOKEN=.*$/gm; if (lineRe.test(zshrc)) { zshrc = zshrc.replace(lineRe, newLine); } else { if (zshrc.length && !zshrc.endsWith("\n")) zshrc += "\n"; zshrc += newLine + "\n"; } fs.writeFileSync(zshrcPath, zshrc); // Only output safe info — NEVER the token const profileName = json.data?.agentProfile?.name || agentNameToTry; const profileUsername = json.data?.agentProfile?.username || usernameToTry; console.log("STATUS=success"); console.log("AGENT_NAME=" + profileName); console.log("AGENT_USERNAME=" + profileUsername); console.log("AGENT_WALLET=" + agentWalletAddress); console.log("ATTEMPTS=" + n); console.log("DFM_AUTH_TOKEN=set"); }); }); req.on("error", (e) => { console.log("ERROR: " + e.message); process.exit(1); }); req.write(payload); req.end(); } attempt(cleanBase, baseName, 1);次のように実行してください:
node .claude/profile-launch.js <WALLET_ADDRESS> "<AGENT_NAME>" "<AGENT_USERNAME>"このスクリプトはDFM_AGENT_KEYPAIRからagentWalletAddressを自動的に導出し、ペイロードに含めます。出力されるのはSTATUS=success、AGENT_NAME=...、AGENT_USERNAME=...、AGENT_WALLET=...、ATTEMPTS=<n>、DFM_AUTH_TOKEN=setのみです。実際のトークン値は.claude/settings.jsonと~/.zshrcに直接書き込まれ、ターミナル出力には絶対に表示されません。ユーザー名の競合時の再試行動作:
- HTTP
409のメッセージに「username」への言及がある場合(例:"Username is already taken")、スクリプトはサニタイズ済みのベースユーザー名に4桁の16進数のサフィックスを追加し、POST /profile-launchを再実行します。最大5回試行します。 - 再試行中、スクリプトがログに出力するのは
RETRY=username_taken attempt=<n> next_username=<new>のみです。これにより、シークレットを漏らさずにエージェント(および状況を確認しているユーザー)が進捗を確認できます。 - 表示名の
nameは再試行の間も保持され、変更されるのはusernameのみです。 - HTTP
409のメッセージに「wallet」への言及がある場合(例:"An agent profile already exists for this wallet address")、スクリプトは再試行しません。これはこのフローでは復旧できない状態です。代わりに、エージェントは/token/refresh-by-walletを呼び出してください。 - 5回すべての試行で競合した場合、スクリプトは明確な
ERROR:行を出力して終了します。エージェントは競合についてユーザーに伝える必要があります。
- HTTP
-
ユーザーに「エージェントのプロフィールを作成しました!認証トークンを読み込むためにAIエージェントを再起動すれば、すぐに使い始められます。」と伝えます。
DFM_AGENT_KEYPAIRが設定されておらず、署名が必要な操作(launch-dtf、distribute-fees)を行う場合は、ウォレットを自動生成します(下記の「Agent Wallet -- Keypair Generation」セクションを参照)。ユーザーに尋ねず、そのまま生成してください。
ステップ2:続行する
必要なすべての変数が設定されている場合は、要求された操作をただちに実行します。確認を求めないでください。
重要:.claude/settings.jsonに初めて書き込んだ後、環境変数を読み込むためにClaude Codeを再起動するようユーザーに伝えてください。2回目以降は設定ファイルがすでに存在し、環境変数をすぐに使用できます。
使用する場面
以下の場合にこのスキルを使用します:
- ユーザーが DTF ボールトの起動、作成、デプロイを依頼した場合
- ユーザーがトークン/市場を調査してファンドを作成するよう依頼した場合
- ユーザーがボールトの状態、ポリシー、リバランス状況の確認を希望した場合
- ユーザーがボールトのリバランスや手数料の分配を依頼した場合
- ユーザーが特定の資産の市場データを尋ねた場合 — 「SOL の流動性を確認して」、「BONK の24時間取引高は?」、「JUP の価格は?」、「ORCA の保有者数は?」、「この資産はローンチに十分な厚みがある?」 —
GET /market-metricsにルーティングします(下記「プロンプトのルーティング」を参照)。このエンドポイントはポリシーエンジンが照会するものと同じ Jupiter パイプラインを使用するため、数値はローンチ/リバランス時に適用されるものと完全に一致します。 - ユーザーが資産のバスケット/ウォッチリストを監視したい場合 — 「[SOL, JUP, BONK] を見ておいて」、「この中で下限を下回りそうなものはある?」、「バスケットの流動性の変動を監視して」 — 複数資産のクエリで
GET /market-metricsにルーティングし、表を表示します。(一定間隔で継続的にポーリングする場合、エージェントは各ターンで必要に応じて呼び出しを実行します。サーバー側のサブスクリプションはありません。) - ユーザーがボールトへの USDC の入金またはボールトトークンの償還を依頼した場合(資金の入出金はエージェントに紐づいています。「ステップ 7:資金の入出金」を参照)
- ユーザーが**「
<vault>のシェアをいくつ保有している?」または「<vault>での自分のポジションは?」*と尋ねた場合 —GET /vaults/:symbol/sharesでオンチェーンの ATA 残高を読み取ります。また、ユーザーが「自分の<vault>をすべて償還して」*と言った場合にも、事前確認としてこの呼び出しを行います。 - ユーザーが**「プラットフォーム上のすべての DTF」/「DFM 上のすべてのボールト」/「利用可能な DTF は?」を尋ねた場合 — プラットフォーム全体の一覧取得フローを実行します(ステップ 5:管理 →「プラットフォーム上のすべての DTF を一覧表示」):すべてのページを対象に、ボールトタイプごとの取得を2回行い、注目/非注目の情報を付加して、1つの表にまとめます。これは「自分のボールト」**とは異なります。プラットフォーム全体に関する表現を
/vaults/userにルーティングしないでください。このエンドポイントはユーザーごとのスコープです。 - ユーザーが詳細付きのプラットフォーム DTF の**「一覧」**を求めた場合 — 「すべての DTF の一覧を見せて」、「すべてのボールトの詳細データを教えて」、「すべての情報付きで各ボールトを一覧にして」 — 同じプラットフォーム全体の一覧取得フローを実行しますが、詳細モードで表示します(ステップ 5:管理 →「詳細な一覧表示モード」):上部に概要表を表示し、続けてすべてのボールトについて詳細ブロック(概要+構成)を表示します。「一覧」に「詳細/完全/すべて」といった条件が付いている場合は、デフォルトで詳細モードにします。「表示して」「閲覧して」「何が利用可能?」といった表現だけの場合は、コンパクトな概要表をデフォルトとします。
- ユーザーがエージェント認証(ウォレットの生成、トークン管理)を設定する必要がある場合
プロンプトのルーティング — 意図 → エンドポイント
作業を始める前に、以下の表に照らしてユーザーのプロンプトを分類します。行を必ず1つだけ選び、そのワークフローを実行してください。複数のツールを同時に使用したり、ファーストパーティ API で回答できる質問に対して、安易に WebSearch を使ったりしないでください。ポリシーエンジンが適用する数値はアグリゲーターではなく /market-metrics に基づいているため、特定のアセットに関する市場データ形式の質問には、オープンウェブではなく必ず API を使用してください。そうしないと、エージェントがプラットフォームの適用対象にならない数値を引用することになります。
| ユーザーのプロンプトの形式 | 主なツール | 追加/フォールバック | 出力形式 |
|---|---|---|---|
| 「<asset> の価格/流動性/24時間取引高/保有者」、「SOL の市場データを確認して」、「BONK はローンチに十分な流動性がある?」、「[SOL, JUP, BONK] の指標を見せて」 | GET /market-metrics?symbols=...&names=...&mints=... | なし — これがアセット指標の正規の取得方法です | Step 1: Research の列スキーマ(Symbol、Name、Price、Liquidity、24h Volume、Holders)に従った Markdown テーブル |
| 「自分のバスケットを監視して」、「これらのアセットをウォッチして」、「流動性が50万を下回るものがあれば知らせて」、「この中に下限を下回りそうなものはある?」 | GET /market-metrics(複数アセット) | スナップショットには追加なし。ユーザーが定期的な監視を求めた場合、エージェントは各ターンでチェックを実行することを伝えます — サーバー側のサブスクリプションはありません。 | アセットごとに1行の Markdown テーブル。ユーザーがしきい値を指定した場合は、テーブルの下に「⚠ X が <threshold> を下回っています」という1行の概要を追加します |
| 「市場センチメントは?」、「<asset> に何が起きている?」、「X を取り巻くニュース/材料/ナラティブ」、「今日、市場が下落している理由は?」、「今週トレンドの Solana トークン」 | WebSearch + WebFetch | ユーザーが具体的な数値を尋ねた場合に限り、追加で /market-metrics を使用 | 簡潔な説明。ユーザーが相場の方向性を判断しようとしている場合は、出典を2~4件示します |
| 「自分の <symbol> ボールトはどうなっている?」、「<symbol> の状態/ポートフォリオ/NAV/シェア価格を見せて」、「<symbol> の自分のポジション」 | GET /dtf/:symbol/state | シェア残高の生データを取得する場合は GET /vaults/:symbol/shares | Step 5: Manage に従った複数セクションの Markdown テーブル(ボールト概要テーブル + ポートフォリオテーブル + 保有資産テーブル + 履歴) |
| 「<symbol> をリバランスすべき?」、「<symbol> はリバランスの時期?」、「リバランスの準備状況を確認して」 | GET /dtf/:symbol/rebalance/check | ユーザーが求めた場合に限り、コンテキストとして /dtf/:symbol/state を使用 | 平易な言葉での準備状況の判定 + suggestion.suggestedActions[] に基づく提案テーブル |
| 「自分のボールトを一覧表示して」、「自分の DTF」、「自分が作成したボールト」、「ボールト全体のポートフォリオ」 | GET /vaults/user?vaultType=dtf および GET /vaults/user?vaultType=yield_dtf | なし | Step 5: Manage に従った統合 Markdown テーブル |
| 「プラットフォーム上のすべての DTF」、「DFM の全ボールト」、「注目のボールトを見て」、「厳選されたものは?」 | GET /vaults/featured/list(ページネーションあり) ± クロスリスト用の /vaults/user | なし | 統合 Markdown テーブル — Step 5 → 「プラットフォーム上のすべての DTF の一覧表示」を参照 |
| 「<n> 件の DTF を下書き/提案して」、「提案を出して」、「ボールトのアイデア」 | アセットの調査には WebSearch + WebFetch を使用し、その後、各候補の流動性/取引高を検証するために GET /market-metrics を使用 | ユーザーが1つ選んだ後に POST /policy/dry-run | Step 2: Design → 「提案の出力形式」に従った3つのテーブル形式(バスケット、ポリシー、ホワイトリストのバッファー) |
| 「DTF をローンチ/作成/デプロイして」(テーマの指定の有無は問いません) | 以下の Step 1~4 に従った自律型ローンチフロー全体 | なし | 会話形式の進捗報告 + Step 4 に従った最終的な成功メッセージ |
| 「<n> USDC を入金して」、「<n> シェアを償還して」、「<symbol> を購入/売却して」 | Step 7 の資金フロースクリプト | なし | スクリプト形式のフェーズ表示 + 最終的な DEPOSIT_OK/REDEEM_OK の概要テーブル |
リアルタイム監視 — 実際に可能なこと
サーバー側のサブスクリプション/ストリーミングエンドポイントはありません。 このスキルにおける「リアルタイム」とは、ユーザーが依頼するたびにエージェントが /market-metrics(および/または /dtf/:symbol/state)を呼び出し、スナップショットを表示することを意味します。ユーザーが 「今後1時間、SOLの流動性を監視して、600Mを下回ったら教えて」 と言った場合、エージェントは次のように対応してください。
- 今すぐスナップショットを実行し、現在の値を伝えます。
- 呼び出されたときにしか確認しないこと、バックグラウンドでのポーリングはないことを、ユーザーに明確に伝えます。ユーザーの希望する頻度(例:「15分後にもう一度聞いてください」)で再度依頼するよう提案します。
- 継続的に監視していると偽ってはいけません。 「代わりに監視しておきます」、「下がったら通知します」、「今ポーリングしています」 などの表現は禁止です。これらは、実際には存在しないバックグラウンド処理を行っているかのように示唆します。
実際にスケジュールを設定した確認を行う場合、ユーザーは外部から /schedule スキルの呼び出しを設定できます。これはこのスキルの範囲外であり、尋ねられた場合はその旨を明示してください。
/market-metrics が取得する情報と取得しない情報
| 返されるフィールド | 用途 | 使用しない用途 |
|---|---|---|
liquidity_usd, volume_24h_usd | ポリシーの調整、深度の確認、「このアセットをローンチできるか」の判断、監視値の変化の確認 | 長期トレンドの分析(履歴データがないため) |
price_usd | 現在のスポット価格の参照、アグリゲーターのデータの妥当性確認 | チャート、ローソク足、経時的な変化率 |
holder_count | 分布のヒューリスティック、ミームトークン/新規トークンの審査 | ウォレットの特定、クジラの追跡 |
policyRelevant.{liquidity_usd, volume_24h_usd} | 評価器が比較する数値 | その他すべて — トップレベルのフィールドと重複しています |
このエンドポイントが返すのはアセットごとのスナップショットのみです。トレンド、センチメント、ニュース、その他のナラティブ情報については、上記の表に従って WebSearch/WebFetch に振り分けてください。
DTF の自律的なローンチ -- 仕組み
ユーザーから DTF のローンチを依頼された場合(例:「優良銘柄の Solana ファンドを作成して」や「ミームトークンの DTF をローンチして」)、次の自律的なワークフローに従ってください。
ステップ 1:調査(エージェントが判断)
WebSearch と WebFetch を使ってトークンを発見します。価格、時価総額、ミントアドレス、トレンド資産、マクロ経済の状況を調べます。その後、GET /market-metrics を、ポリシーエンジンが実際に適用する 2 つの数値(ルール 2 の流動性、ルール 3 の 24 時間取引高)の信頼できる情報源として使います。アグリゲーターのスクレイピング値はバックエンドが確認する値と食い違うことがよくあります。/market-metrics は評価器と同じパイプラインを通して取得した Jupiter のデータを返すため、数値が正確に一致します。
WebSearch を使って次を探します。
- 時価総額、取引高、値動きで上位の Solana トークン
- 現在の市場状況、トレンド、センチメント
- 利回り型 DTF の場合:Solana の LST(リキッドステーキングトークン)と利回りを生むトークン(mSOL、jitoSOL、bSOL、INF、hSOL、stSOL)および現在の APY、TVL、ステーキング利回り
WebFetch を使って、次のデータを取得します。
- トークンデータアグリゲーター(CoinGecko、CoinMarketCap、Jupiter aggregator、Birdeye、DexScreener)
- Solana のトークンリストと、ミントアドレスを確認できる検証済みレジストリ
- 利回り型 DTF の場合:ステーキング利回りアグリゲーター、LST プロトコルのサイト(Marinade、Jito、BlazeStake、Sanctum)、DeFi 利回りダッシュボード
次に、候補資産(ミント、シンボル、または名称)を指定して GET {DFM_API_URL}/api/v2/agent/market-metrics を呼び出し、ポリシーに関係する正確な数値を取得します。
GET /api/v2/agent/market-metrics?symbols=SOL,JUP&names=Bonk
Authorization: Bearer <DFM_AUTH_TOKEN>
応答:
{
"metrics": [
{
"mintAddress": "So11111111111111111111111111111111111111112",
"symbol": "SOL",
"name": "Wrapped SOL",
"liquidity_usd": 691807448.19,
"volume_24h_usd": 14648499808.98,
"price_usd": 85.33,
"holder_count": 3820662,
"policyRelevant": {
"liquidity_usd": 691807448.19,
"volume_24h_usd": 14648499808.98
}
}
],
"unresolved": []
}
これらの数値を使って、次のようにします。
- 戦略の下限を満たさない候補を除外します(例:積極的なファンドの場合、
liquidity_usd < 50000の資産を除外します)。 - ポリシー内の
min_amm_liquidity_usdとmin_24h_volume_usdは、最も弱い選定資産の/market-metricsの数値より 30~50%低く設定し、安全マージンを設けます。Jupiter のスナップショットは日中に変動するため、下限を最も弱い資産の現在値ちょうどに設定すると、悪いスナップショットで資産が下限を下回り、バスケット内で「ロック」されます(ロックされた資産を含む中間バスケットは取引高の条件を満たせなくなるため、エージェントは/update-assets-txでその資産を削除できなくなります)。具体的なルールは、floor = floor(weakest_asset_snapshot × 0.5) とし、切り下げてきりのよい数値にします。 policyRelevantの値が null の場合は、Jupiter データ取得の一時的な失敗を示します。キャッシュはミントごとに保持されるため、キャッシュが温まった後に再試行します。
market-metrics の結果をユーザーに提示する際は、表形式で表示します。箇条書きにはしないでください。mintAddress(ユーザーから求められていない限り、列が増えるだけです)と、重複する policyRelevant ブロック(Liquidity 列と 24h Volume 列ですでに示されています)は省略します。
✅ RIGHT (market-metrics output):
| Symbol | Name | Price (USD) | Liquidity | 24h Volume | Holders |
| ------ | -------------- | ----------- | ------------- | ------------- | ----------- |
| SOL | Wrapped SOL | $85.33 | $691,807,448 | $14,648,499,808 | 3,820,662 |
| JUP | Jupiter | $0.642 | $48,210,005 | $912,440,011 | 612,408 |
| BONK | Bonk | $0.0000231 | $11,002,341 | $74,509,123 | 891,210 |
列の仕様:
| 列 | 取得元 | 形式 |
|---|---|---|
| シンボル | metrics[].symbol | そのまま |
| 名称 | metrics[].name | そのまま |
| 価格(USD) | metrics[].price_usd | $<n>(桁の大きさに応じて有効数字 4~6 桁)。1 セント未満のトークンは $0.0000231 の形式。null の場合は非表示。 |
| 流動性 | metrics[].liquidity_usd | $<n>。3 桁区切りを使い、$1M を超える値はドル単位の整数に丸めます。 |
| 24 時間取引高 | metrics[].volume_24h_usd | 流動性と同じ形式。 |
| 保有者数 | metrics[].holder_count | 整数。3 桁区切りを使います。すべての行が null の場合は列全体を非表示にします。 |
応答に unresolved[] リスト(バックエンドが見つけられなかった資産)が含まれる場合は、表の下に 1 行の注記を追加します。例:"Couldn't resolve: BONK (no Jupiter price), XYZ (mint not found)." unresolved を独立した表として表示しないでください。
次に、以下を判断します。
- ユーザーの意図または戦略に基づいて候補トークンを特定します
- 利回り型 DTF の場合:APY が最も高く、流動性が最も深い LST と利回り型資産を優先します
- 最終的なバスケットを選び、配分を決定します
- 信頼できる情報源(公式ドキュメント、検証済みトークンリスト、エクスプローラー、大手データプロバイダー)から、各トークンの Solana
mintAddressを自動的に特定します underlyingAssetsに含める前に、複数の情報源でミントアドレスを照合します- 未検証、情報が食い違う、または信頼度が低いミントの対応付けを却下し、検証済み資産に置き換えます
ステップ 2:設計(エージェントが決定)
調査に基づいて、次の項目を自律的に決定します。
- Vault の種類 — 標準的な分散型トークンファンドには
"DTF"、利回り付き/LST に重点を置いたファンドには"YIELD_DTF"を指定します。dtf-createペイロードに設定します。 - Vault 名 — 戦略を表す、特徴的で覚えやすい名前
- Vault シンボル — 短く(最大 10 文字)、一意で覚えやすいもの
- 基礎資産 — 資産の
symbolまたはname(推奨)と、ベーシスポイント単位の配分を指定します(合計は 10000 である必要があります)。バックエンドがasset-allocationからmintAddressを解決します。- DTF の場合:標準トークン(SOL、JUP、Bonk、RAY など)
- YIELD_DTF の場合:LST および利回り付きトークン(mSOL、jitoSOL、bSOL、INF など)
- 管理手数料 — ベーシスポイント単位(例:200 = 2%)
- ポリシー設定 — 資産上限、リバランス頻度、ステーブルコインの最低比率など
- タグ — 検索可能なカテゴリ(利回り DTF には「Yield」「LST」「Staking」を含めます)
- 説明 — 戦略の概要
- ローンチメディアのフィールド — DTF ローンチペイロードでは、
logoUrl、bannerUrl、metadataUriを空文字列に設定します。
提案の出力形式(ユーザーが直接ローンチではなく、DTF の 提案 を求めた場合)
ローンチ前に検討する提案/選択肢/候補 Vault(「DTF の提案をいくつかください」、「バスケットをいくつか提案してください」、「何をローンチできますか」、*「Vault のアイデアを 3 つ作成してください」*などの依頼)が求められた場合、エージェントは各提案を厳密な Markdown テーブル形式で提示する必要があります。生の JSON は使いません。policy オブジェクトの JSON 出力は、ユーザーが読む必要のないフィールド名を露出させ、有用な情報を画面外に押しやり、API のデバッグ出力のように見えるため、提案の出力では明示的に禁止されています。ユーザーはすでに、提案に対する JSON 出力は「不要で不適切」だと伝えています。この方針に逆戻りしないでください。
各提案では、1 行の提案タイトル(例:### 1) Solana LST Yield Basket — curated LST exposure)の後に、必ず 3 つのテーブルを出力します。各提案の末尾には、バスケットとポリシーの組み合わせがポリシーエンジンを通過する理由を要約した、1~2 文の 根拠 の段落を任意で付けられます。それ以外は何も付けません。JSON のコードフェンス、ポリシーフィールドの箇条書き、根拠の段落と重複する「これが機能する理由」のリストは含めません。
テーブル 1 — バスケットの配分(ホワイトリストモードにバッファが含まれていても、優先資産のみ):
| シンボル | 配分 | バスケットに含める理由 |
|---|---|---|
| SOL | 25% | Solana で最も流動性と取引量が大きく、バスケットの基盤となる |
| JUP | 15% | DEX アグリゲーターで、現物取引量はトップ 3 |
| ... | ... | ... |
配分の列には pct_bps ではなく、パーセンテージ(25%)を表示します。これはユーザー向けの値です。パーセンテージの合計は 100% とし、ローンチ時に underlyingAssets[].mintBps に設定される内容と一致させます。
テーブル 2 — ポリシーパラメーター(/launch-dtf の policy サブオブジェクトに設定される値):
| パラメーター | 値 | 制御する内容 |
|---|---|---|
| 資産モード | WHITELIST_ONLY | リストにある Mint だけがバスケットに含まれます |
| 最小 AMM 流動性 | $180,000 | 対象資産のうち、スナップショット時の値が最も低いものに対して 50% の余裕を確保 |
| 最小 24 時間取引量 | $1,200,000 | 対象資産のうち、スナップショット時の値が最も低いものに対して 50% の余裕を確保 |
| 最小/最大資産数 | 4 / 10 | ローンチ時のバスケットサイズに対して ±2 の余裕を確保 |
| 最小/最大資産比率 | 5% / 40% | 単一ポジションが 40% を超えないようにし、最低 5% とすることでごく小さなポジションを防止 |
| 1 回のトランザクションあたりの最大リバランス量 | 60% | 資産の完全な入れ替えを可能にする(最大資産比率の 2 倍以上) |
| 最小リバランス間隔 | 12 h | 利回り型ファンドに適した慎重な頻度 |
| 1 日・1 週間あたりの最大リバランス回数 | 1 · 5 | 不必要な売買を抑制 |
| ローンチ後のリバランス禁止期間 | 24 h | ローンチ後 24 時間はリバランスを行いません |
| 手数料のロック | はい | ローンチ時に管理手数料を確定します |
上記の行は必ずすべて含めてください。USD の値には桁区切りを入れた $<n>、パーセンテージには <n>%、時間/件数のフィールドには整数、ブール値には yes / no を使ってください(true / false は使わないでください)。フィールドが本当に該当しない場合に限り、その行を省略してください(例:値が 0% の Min stablecoin floor は、情報量を抑えるために省略できます)。 |
表 3 — ホワイトリスト一覧(asset_mode が WHITELIST_ONLY または WHITELIST_BLACKLIST の場合のみ):
この表は5列です — Asset、Role、Liquidity、24h Volume、Floor check。指標の列は任意ではなく、必須です。ユーザーがボールトのローンチ前に、すべてのバッファミントがポリシーの最低基準を実際に満たしていることをひと目で確認できるようにするためです。この情報がないと、ローテーション時まで使用不能なバッファに気づけません。
| Asset | Role | Liquidity | 24h Volume | Floor check (vs $X liq / $Y vol) |
|---|---|---|---|---|
| jitoSOL | Priority — launch | $850,000,000 | $42,000,000 | ✓ pass |
| mSOL | Priority — launch | $620,000,000 | $28,000,000 | ✓ pass |
| bSOL | Priority — launch | $180,000,000 | $9,500,000 | ✓ pass |
| INF | Priority — launch | $95,000,000 | $5,200,000 | ✓ pass (最も弱い優先ミント — $47.5M / $2.6M の最低基準を設定) |
| hSOL | Buffer — reserve | $62,000,000 | $3,400,000 | ✓ pass |
| jucySOL | Buffer — reserve | $55,000,000 | $2,800,000 | ✓ pass |
| picoSOL | Buffer — reserve | $48,000,000 | $2,650,000 | ✓ pass(余裕度:1.0× — 境界値。Rationale で注意喚起) |
表 3 の厳守ルール:
- 列見出しの
$X liq / $Y volを、実際に提案するmin_amm_liquidity_usdとmin_24h_volume_usdに置き換えてください(例:Floor check (vs $1,500,000 liq / $1,300,000 vol))。 - Liquidity と 24h Volume の値には、エージェントがすでに取得した
/market-metricsのスナップショットを使用します。ホワイトリスト内のミントにスナップショットが1件でもない場合、ローンチに進まないでください — 取得するか、そのミントをホワイトリストから削除してください。この表の空欄はローンチを阻止する要因です。 - Floor check 列には、
liquidity ≥ floor_liq AND volume ≥ floor_volの場合は✓ pass、それ以外の場合は✗ FAIL — <reason>と表示してください(例:✗ FAIL — liquidity $361K below $1.5M floor)。✗ FAILの行はローンチを阻止する要因です。 エージェントは、該当するミントをホワイトリストから除外する(推奨)か、min(priority ∪ buffer)を基準に最低基準を再計算して表を再生成する必要があります。✗ FAILの行がある提案をユーザーに提示しないでください — それを防ぐことが目的です。 - 優先ミントも最低基準を満たす必要があります。優先ミントが独自の最低基準を満たさない場合、最低基準が高すぎるということなので、再計算してください。
- Rationale の段落では、ホワイトリスト全体で最も弱いミント(最低基準を設定するミント)と、最低基準をわずかに上回るバッファミントの余裕度を明記してください。最低基準を30%未満の余裕でしか満たさないバッファミントがある場合は、キャッチ22リスクとして明記してください(Future-proofing checklist のルール #5) — スナップショットが悪化した際、最初に最低基準を下回るミントになります。
各ホワイトリストミントに、underlyingAssets に含めてローンチする Priority — launch、または今後の /update-assets-tx によるローテーションをポリシー上許可するためだけにホワイトリストに含める Buffer — reserve のいずれかを付けてください。ローンチ時のバスケットはホワイトリスト全体ではなく優先ミントのサブセットであることが、ユーザーにひと目でわかる必要があります。ユーザーから明示的に求められない限り、この表に完全な base58 ミントアドレスを表示しないでください。シンボルと名前で十分です。
提案の出力で禁止されている項目(過去の不適切な出力で確認されたすべての項目):
- ポリシーオブジェクトを生の JSON コードフェンスで出力すること —
{ "asset_mode": "...", ... }。Table 2 の行スキーマで完全に置き換えてください。 - ユーザー向けの列で
pct_bpsの整数を使うこと(%を使用してください)。 - JSON からコピーした
min_amm_liquidity_usd: 179000形式のキーと値の行。 - 優先枠とバッファを区別せず、ミントアドレスのホワイトリストを単純な配列として列挙すること。
- Table 2 の内容を言い換えただけの、マーケティング調の箇条書き「これが有効な理由」セクション。
- Liquidity / 24h Volume / Floor check 列がない Table 3 — 3 列形式(Asset、Role、Notes)では使用できないバッファが見えなくなり、ローテーションできないバッファのバグの根本原因になります。必ず 5 列形式で表示してください。
- 提示する提案の Table 3 に
✗ FAILの行を含めること — これは、エージェントが修正せずに使用できないバッファを含む提案をユーザーに提示したことを意味します。該当するミントを除外するか、提案を表示する前にフロアを再計算してください。 /market-metricsのスナップショットがないバッファミントを列挙すること。 スナップショットを取得していない場合、そのバッファは未検証です。未検証のバッファを提案に含めてはいけません。
ユーザーが提案を選択したら、選択されたバスケットとポリシーをそのまま使って Step 3(/policy/dry-run)に進みます。dry-run で使用するのは優先資産のみです(バッファ資産はバスケットではなく、ポリシーの一部です)。
Step 3:検証(デプロイ前の dry-run)
/launch-dtf を呼び出す前に、提案するバスケットとポリシーを POST /policy/dry-run に渡して検証します。これは無料(DB への書き込みもオンチェーン費用もありません)で、すべての違反を一度に返すため、エージェントはまとめて修正できます。ok: true または violations: [] になるまで繰り返してください。
POST {DFM_API_URL}/api/v2/agent/policy/dry-run
Authorization: Bearer <DFM_AUTH_TOKEN>
{
"underlyingAssets": [
{ "symbol": "SOL", "pct_bps": 4000 },
{ "symbol": "JUP", "pct_bps": 3000 },
{ "name": "Bonk", "pct_bps": 3000 }
],
"policy": {
"asset_mode": "OPEN",
"min_amm_liquidity_usd": 100000,
"min_24h_volume_usd": 500000,
"min_assets": 3,
"max_assets": 12,
"max_asset_pct": 4000,
"min_asset_pct": 500
}
}
正常なレスポンス(Step 4 に進みます):
{ "ok": true, "policyCheck": { "ok": true, "violations": [] } }
違反が検出されたレスポンス — 返されたすべての違反:
{
"ok": false,
"policyCheck": {
"ok": false,
"violations": [
{
"violationCode": "rule2MinAmmLiquidity",
"message": "Mint Bonk... has $30000 AMM liquidity; policy requires at least $100000.",
"details": { "mint": "Dez...", "observedUsd": 30000, "minUsd": 100000 }
},
{
"violationCode": "rule4MinMaxAssetCount",
"message": "Proposed 3 distinct assets; policy requires between 5 and 12.",
"details": { "distinctAssetCount": 3, "minAllowed": 5, "maxAllowed": 12 }
}
]
}
}
修正方法(優先順位順):
rule2MinAmmLiquidity/rule3Min24hVolume→(その資産が戦略上不可欠な場合は)ポリシーのしきい値を下げるか、資産を入れ替えます。含まれる資産のいずれについても、/market-metricsの報告値を超えるしきい値を設定しないでください。rule4MinMaxAssetCount→ バスケットのサイズ、またはmin_assets/max_assetsの範囲を調整します。rule5MaxPctPerAsset/rule6MinPctPerAssetIfHeld→pct_bpsの配分をリバランスします。rule1WhitelistBlacklist/assetModeViolation→asset_mode、asset_whitelist、またはasset_blacklistを修正します。
修正のたびに dry-run を再実行してください。 dry-run が問題なしを返した場合にのみ、/launch-dtf に進みます。実際には最大約 3 回の反復が目安です。収束できない場合は、オンチェーン費用が発生する前に阻害要因をユーザーに伝えてください。
Step 4:デプロイ(ポリシーで制御する 2 段階フロー)
4a. 未署名トランザクションを構築する(ポリシーを含む)
ボールト作成ペイロードとポリシーを含めて、POST {DFM_API_URL}/api/v2/agent/launch-dtf を送信します。バックエンドは /policy/dry-run と同じ作成前評価を実行し、ポリシーをコミット(未リンクの状態で、vault_name + vault_symbol をキーとして保存)して、未署名トランザクションを返します。いずれかのルールに違反した場合は、すべての violations[] を含む 400 を返します。トランザクションは構築されず、オンチェーンにも何も書き込まれないため、エージェントは修正して再試行できます。
{
"signerPublicKey": "<public key derived from DFM_AGENT_KEYPAIR>",
"vaultName": "Solana Blue Chips",
"vaultSymbol": "SOLBC",
"underlyingAssets": [
{ "symbol": "SOL", "mintBps": 4000 },
{ "symbol": "JUP", "mintBps": 3000 },
{ "name": "Bonk", "mintBps": 3000 }
],
"managementFees": 200,
"category": 0,
"threshold": 500,
"policy": {
"asset_mode": "OPEN",
"asset_whitelist": [],
"asset_blacklist": [],
"min_amm_liquidity_usd": 100000,
"min_24h_volume_usd": 500000,
"min_assets": 2,
"max_assets": 8,
"max_asset_pct": 4000,
"min_asset_pct": 500,
"min_stablecoin_pct": 0,
"max_rebalance_pct": 7500,
"min_rebalance_interval_hours": 4,
"max_rebalances_per_day": 3,
"max_rebalances_per_week": 14,
"launch_blackout_hours": 24,
"fee_locked": true,
"notes": "Auto-generated policy for blue chip strategy"
}
}
余裕幅を設ける理由(値をそのままコピーしないでください):ローンチ時のポリシーでは、エージェントが将来実行する必要のあるバスケット移行を許可する必要があります。以下の「将来に備えるためのチェックリスト」を参照してください。エージェントは /launch-dtf を送信する前に、提案するポリシーに対してこのチェックリストを実行する必要があります。
成功レスポンス(グローバルな ResponseMiddleware エンベロープでラップされています。トップレベルに pagination フィールドがない成功したエージェントエンドポイントはすべて、このラップが適用されます):
{
"status": "success",
"message": "OK",
"data": {
"onChain": {
"transaction": "base64-encoded-unsigned-versioned-transaction...",
"vaultIndex": 42,
"vaultPda": "7Xk...def",
"vaultMintPda": "9Rm...ghi"
},
"policyId": "665c..."
}
}
**未署名トランザクションは body.onChain.transaction ではなく、body.data.onChain.transaction から読み取ってください。**同じく、vaultIndex、vaultPda、vaultMintPda、policyId も body.data.* から読み取ります。body.onChain.transaction を読み取ると undefined が返され、実際には問題のないレスポンスに対してエージェントがローンチ失敗と報告します。壊れているのは読み取り方法だけです。
ポリシー違反レスポンス(400)— GlobalExceptionFilter によってラップされます。スローされた BadRequestException の本文は error の下に格納されます。violations[] 配列は body.violations ではなく、body.error.violations にあります。
{
"statusCode": 400,
"path": "/api/v2/agent/launch-dtf",
"timestamp": "2026-05-06T12:34:56.789Z",
"error": {
"message": "Proposed basket violates the supplied constitutional policy. Adjust the policy or the basket and try again.",
"ok": false,
"violationCode": "rule2MinAmmLiquidity",
"violations": [
{ "violationCode": "rule2MinAmmLiquidity", "message": "...", "details": {...} }
],
"error": "Bad Request",
"statusCode": 400
}
}
違反の読み取り方:body.error.violations — 各エントリーの形式は /policy/dry-run の policyCheck.violations と同じです(そのため、違反コードの翻訳テーブルを再利用できます)。body.violations は undefined として扱ってください。このパスはラップされていない dry-run の形式であり、例外エンベロープではありません。
4b. トランザクションに署名してオンチェーンに送信する
エージェントのローカルキーペアを使用して、返されたトランザクションに署名し、Solana に送信します。
import { Keypair, VersionedTransaction, Connection } from "@solana/web3.js";
const bs58 = require("bs58").default || require("bs58");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR!));
const connection = new Connection(process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com");
// Deserialize the unsigned transaction from the API response.
// `response` here is the parsed JSON body — the tx lives under data.onChain
// because the global ResponseMiddleware wraps the controller return value
// as { status, message, data: { onChain, policyId } }.
const txBytes = Buffer.from(response.data.onChain.transaction, "base64");
const tx = VersionedTransaction.deserialize(txBytes);
// Sign with the agent's keypair
tx.sign([keypair]);
// Submit on-chain
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
});
await connection.confirmTransaction(signature, "confirmed");
4c. ボールトのメタデータを DB に保存する
オンチェーントランザクションの確定後、トランザクション署名とメタデータのみを含めて、POST {DFM_API_URL}/api/v2/agent/dtf-create を送信します。ポリシーは /launch-dtf の実行中にすでにコミットされており、チェーンイベントパイプラインによって新しいボールトに自動的にリンクされます。ここではポリシーフィールドを送信しないでください。送信すると拒否されます。
{
"transactionSignature": "<signature from step 4b>",
"vaultName": "Solana Blue Chips",
"vaultSymbol": "SOLBC",
"vaultType": "DTF",
"description": "Top-tier Solana ecosystem tokens",
"tags": ["Blue Chip", "Solana", "DeFi"],
"logoUrl": "",
"bannerUrl": "",
"noRebalance": false
}
成功レスポンス:
{
"vault": [
{
"eventType": "VaultCreated",
"vault": {
"vaultName": "Solana Blue Chips",
"vaultSymbol": "SOLBC",
"vaultIndex": 42,
"status": "active"
}
}
],
"policyId": "665c..."
}
例:WHITELIST モード(厳選した LST 利回りファンド向け) — ポリシーフィールドは /launch-dtf に移動します。/dtf-create には引き続きメタデータのみを含めます。**underlyingAssets(ローンチ時に組み込む優先資産 3 つ)と asset_whitelist(6 つのミント。優先資産 3 つと、今後の /update-assets-tx による入れ替え用に確保するバッファ 3 つ)の非対称性に注意してください。**バッファ用ミントはポリシー上の対象ですが、ローンチ時のバスケットには含まれません。優先資産とバッファのルール全体については、ポリシーフィールド決定ガイドの「WHITELIST_ONLY」を参照してください。
ステップ 4a /launch-dtf の本文(厳選した LST ファンドのポリシー):
{
"signerPublicKey": "<agent pubkey>",
"vaultName": "Solana LST Yield",
"vaultSymbol": "SLSTY",
"underlyingAssets": [
{ "symbol": "mSOL", "mintBps": 4000 },
{ "symbol": "jitoSOL", "mintBps": 3500 },
{ "symbol": "bSOL", "mintBps": 2500 }
],
"managementFees": 150,
"category": 0,
"policy": {
"asset_mode": "WHITELIST_ONLY",
"asset_whitelist": [
"mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
"J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",
"bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",
"5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",
"he1iusmfkpAdwvxLNGV8Y1iSbj4rUy6yMhEA3fotn9A",
"BqYCcAd1ZtrtdwoAxmqLtXMSrW1DHPV6LWLjB2TQrtyT"
],
"asset_blacklist": [],
"min_amm_liquidity_usd": 500000,
"min_24h_volume_usd": 500000,
"min_assets": 2,
"max_assets": 8,
"max_asset_pct": 5000,
"min_asset_pct": 1000,
"min_stablecoin_pct": 0,
"max_rebalance_pct": 2000,
"min_rebalance_interval_hours": 12,
"max_rebalances_per_day": 1,
"max_rebalances_per_week": 5,
"launch_blackout_hours": 24,
"fee_locked": true,
"notes": "Whitelisted LST-only yield fund (3 priority + 3 buffer mints)"
}
}
ステップ 4c /dtf-create の本文(メタデータのみ):
{
"transactionSignature": "<signature>",
"vaultName": "Solana LST Yield",
"vaultSymbol": "SLSTY",
"vaultType": "YIELD_DTF",
"description": "Diversified Solana liquid staking token yield fund",
"tags": ["Yield", "LST", "Staking", "Solana"],
"logoUrl": "",
"bannerUrl": "",
"noRebalance": false
}
ポリシーフィールド決定ガイド
エージェントは、ボールト戦略に基づいてすべてのポリシー値を決定する必要があります。**これらの値は /dtf-create ではなく、/launch-dtf(ステップ 4a)の policy サブオブジェクトに設定します。**すべてのしきい値は、将来のバスケット移行に備えて余裕を持たせて調整してください。自身のポリシーによってバスケットが変更不可能になったボールトは、エージェントでも救済できません。/launch-dtf を送信する前に、以下の「将来に備えるためのチェックリスト」を参照してください。
| 戦略タイプ | max_asset_pct | min_asset_pct | min_amm_liquidity_usd | min_24h_volume_usd | max_rebalances_per_day | min_rebalance_interval_hours |
|---|---|---|---|---|---|---|
| 保守的(優良銘柄、インデックス) | 3000-4000 | 500-1000 | 500000 | 1000000 | 2 | 6 |
| 中程度(混合型、エコシステム) | 4000-5000 | 500 | 100000 | 500000 | 3 | 4 |
| 積極的(ミーム、トレンド) | 5000-6000 | 300 | 50000 | 100000 | 4 | 2 |
| 利回り(LST、ステーキング、利回り) | 4000-5000 | 500-1000 | 500000 | 500000 | 1 | 12 |
上記の流動性/取引量の下限は、目標値ではなく、戦略上の上限値です。必ず、最も弱い対象資産の実際の /market-metrics スナップショットに対する 0.5×の安全バッファルールも適用してください(戦略テーブルの値と weakest_asset_snapshot × 0.5 のうち、低い方を選びます)。 |
必ず次のように設定してください。
asset_mode:ボールト戦略に基づいて選択します。-
"OPEN"— あらゆる資産を追加できます。制限はありません。幅広い市場/インデックス/アグレッシブな戦略で使用します。 -
"WHITELIST_ONLY"—asset_whitelistに含まれる資産のみ使用できます。厳選ファンド(例:「優良銘柄のみ」「LST のみ」)で使用します。**ホワイトリストには、ローンチ時のバスケットを厳密に上回る範囲の資産を含める必要があります。つまり、優先資産(underlyingAssetsに入る 4/5/6 個の選定資産)に加えて、エージェントが後から/update-assets-txによるポリシー更新で入れ替えられる、カテゴリ適合の予備資産を 3~4 個含めます。**予備資産は初期ローンチには決して含まれず、underlyingAssetsに投入されるのは優先資産のみです。予備資産を用意するのは、将来のバスケット移行に(禁止されている)ポリシーの拡張が不要になるようにするためです。**重要:下限値は、優先バスケット単独ではなく、優先資産と予備資産を合わせたホワイトリスト全体の中で最も弱い資産を基準に調整します。**このスキルで最も間違いやすいルールです。以前の提案では、下限値を priority_weakest × 0.5 に設定しながら、その下限値を下回る流動性のミントを予備資産に含めていました。その結果、ボールトのローンチ直後から予備ミントがポリシー上使えなくなります。
asset_whitelistには含まれている(そのためルール 1 は満たす)ものの、バスケットに組み込もうとするたびにルール 2/ルール 3 に失敗し、確認はできても入れ替えられない状態になります。ボールトでは、ローンチ時に含めた優先資産の間でしか入れ替えられなくなります。正しい手順(必ずこのとおりに実行してください):
- 優先資産を選び、それぞれについて
/market-metricsを取得します。 - 予備候補(同じカテゴリから 3~4 個のミント)を選び、それぞれについて
/market-metricsを取得します。**この取得を省略しないでください。**スナップショットのない予備ミントは検証できないため、ホワイトリストに追加してはいけません。 weakest_liquidity = min(liquidity_usd over priority ∪ buffer)およびweakest_volume = min(volume_24h_usd over priority ∪ buffer)を計算します。「priority ∪ buffer」全体が対象である点に注意してください。両方のグループを含めます。min_amm_liquidity_usd = floor(weakest_liquidity × 0.5)およびmin_24h_volume_usd = floor(weakest_volume × 0.5)を設定し、切り下げて扱いやすい数値に丸めます。- **確認します。**すべての予備ミントが、設定した下限値をクリアしている必要があります(この手順で計算すればクリアできるはずです。失敗するものがあれば、計算が間違っているため、再計算してください)。
予備候補のスナップショットが優先資産より著しく弱く、手順 3 によって下限値が無意味なほど低くなる場合は、下限値を引き下げるのではなく、その候補を予備資産から外してください。予備資産は、実際に入れ替えられる場合にのみ役立ちます。優先資産の中で最も弱い資産の半分を下回る水準まで下限値を引き下げる「予備」ミントは、ボールトに存在しない深さがあるかのように見せるおとりです。下限値を大幅に希薄化し、エージェントが後からポリシー更新で追加する可能性のある資産を判定する基準まで弱くしてしまいます。 具体的な数値設定: 優先資産数とバッファ資産数の合計を
max_assets以下にします。バッファ資産数は3以上にします(実際にローテーションできる選択肢を確保するため)。ホワイトリストに登録するすべてのミントが、優先資産と同じ0.5倍の安全バッファを適用したうえで、提案されているmin_amm_liquidity_usdとmin_24h_volume_usdの下限を満たす必要があります。 - 優先資産を選び、それぞれについて
-
"OPEN_BLACKLIST"—asset_blacklistに登録された資産を除き、すべての資産を許可します。特定のリスク資産を除外したい場合に使用します。除外するミントアドレスをasset_blacklistに設定します。 -
"WHITELIST_BLACKLIST"— ホワイトリストに登録された資産のみを許可し、さらにブラックリストに登録された資産を除外します。明示的な除外条件を設けた、厳選型ファンドに使用します。asset_whitelistにはWHITELIST_ONLYと同じ優先資産+3~4バッファのルールを適用し、asset_blacklistは明示的なカテゴリ除外にのみ使用します。ローンチ時に含めるのは優先資産のみです。 -
判断基準: ユーザーが特定のカテゴリのファンド(例:「LST ファンド」「ブルーチップのみ」「DeFi トークン上位5銘柄」)を求めている場合は、
WHITELIST_ONLYを使用します。幅広いエクスポージャーを求めている場合は、OPENを使用します。「ミームコインを除外」などの指定がある場合は、OPEN_BLACKLISTを使用します。
-
min_assets:max(1, launch_basket_count − 1)。min_assets == launch_basket_countには設定しないでください。将来の移行時に資産を1つも減らせなくなり、バスケットがそのサイズに固定されるためです。(TOP3S はmin_assets: 3, max_assets: 3でローンチしたため、3資産のバスケットに永久に固定され、1資産だけの入れ替えでも更新ごとの上限を超えてしまいます。)max_assets:min(12, launch_basket_count + 3)とし、ハード上限は12です。バスケットの資産数を2~3増やせる余地を必ず残してください。max_rebalance_pct:資産数が4以下のバスケットでは5000~7500(50~75%) にします。3000~5000が許容されるのは、個々のウェイトがすでに小さい、資産数が6以上のバスケットに限られます。2 × max_asset_pctの値を下回る設定は絶対にしないでください。 資産を1つ入れ替えると、入れ替え前の資産のウェイトと入れ替え後の資産のウェイトの両方が移動するため、合計の変動は入れ替えるウェイトのおよそ2倍になります。max_rebalance_pct < 2 × max_asset_pctに設定すると、資産を1つ入れ替えることが構造上不可能になります。(TOP3S はmax_rebalance_pct: 2500、max_asset_pct: 5500でローンチしたため、JUP から ETH への入れ替えはすべて数学的にブロックされます。)max_rebalances_per_week:max_rebalances_per_day * 7以下launch_blackout_hours:24(最初の24時間はリバランスを防止します)fee_locked:運用手数料の約束が商品の一部であるインデックス型、利回り型、厳選型の運用方針ではtrueにします。TVL の増加に応じて手数料を調整する必要がある場合はfalseにします(ポリシーの更新で後からロックすることはできますが、ローンチ時にロックすると解除できません)。notes:運用戦略とポリシーの根拠を簡潔に記述します
将来への備えチェックリスト(/launch-dtf を実行する前に)
エージェントは、提案する policy とバスケットについて、以下の項目をすべて頭の中で確認する必要があります。1つでも満たさない場合は、ポリシーを調整してドライランを再実行してください。このチェックリストを満たさないボルトは決して起動しないでください。誤った設定の代償は致命的です。憲法ポリシーはエージェントが緩和できず、移行の際にボルトを実質的に使用不能にします。
- 資産数の余裕 —
max_assets ≥ launch_basket_count + 2かつmin_assets ≤ launch_basket_count − 1ですか? いいえの場合 → 上限と下限の幅を広げてください。 - 単一スワップの実行可能性 —
max_rebalance_pct ≥ 2 × max_asset_pctですか? 言い換えると、明日、最もウェイトの大きい資産を入れ替える必要が生じた場合、配分の変動はmax_rebalance_pctの範囲内に収まりますか? いいえの場合 →max_rebalance_pctを引き上げてください(推奨)。またはmax_asset_pctを引き下げてください。 - 流動性/出来高のバッファー(優先資産) — ローンチ時のバスケットに含まれるすべての資産について、
min_amm_liquidity_usd ≤ asset.liquidity_usd × 0.5かつmin_24h_volume_usd ≤ asset.volume_24h_usd × 0.5ですか? いいえの場合 → 下限値を半分にしてください。スナップショットは変動するため、資産をロックしないよう、下限値は日中の50%下落を吸収できる必要があります。 - 除外可能性 — 現在バスケットに含まれているすべての資産について、その資産を除いたバスケットをシミュレーションしてください。シミュレーション後のバスケットは
min_assets、max_asset_pct、min_asset_pctを引き続き満たしますか? 除外できない資産が1つでもある場合、そのボルトは出来高スナップショットが一度悪化するだけで永久に動かせなくなる可能性があります。 - 行き詰まり防止策 — 含まれる資産の
/market-metricsスナップショットが、提案するmin_24h_volume_usdまたはmin_amm_liquidity_usdの30%以内にある場合は、下限値をさらに引き下げるか、資産を入れ替えてください。下限値付近を推移する資産は、いずれその値を下回ります。そうなると、その資産を含む中間バスケットはどれも出来高ゲートを通過できず、移行全体がブロックされます。 - バッファーの入れ替え可能性(WHITELIST モードのみ) — バスケットに含まれていない
asset_whitelist内のすべてのミント(つまり、すべてのバッファー — 予備ミント)について、そのミントの/market-metricsスナップショットは、提案するmin_amm_liquidity_usdとmin_24h_volume_usdの下限を、優先資産と同じ0.5倍の余裕をもって満たしていますか? バッファーのミントが1つでも満たさない場合、出来高がわずか1ドル足りないだけでも、ポリシーによって行き詰まります。入れ替え可能に見えてもホワイトリスト内に留まり、Rule 2/Rule 3 によって、そのミントをバスケットに加えようとする/update-assets-txはすべてブロックされます。基準を満たさないバッファーのミントを削除してください(推奨 — 厳しい下限値を維持できます)。または、その特定のミントを将来の選択肢としてどうしても残す必要がある場合に限り、0.5倍の余裕をもって基準を満たすよう、両方の下限値を引き下げてください(これにより、今後追加するすべてのバッファー候補の基準も下がるため、慎重に検討してください)。この同じミスによる過去の現場での失敗例:流動性下限を150万ドルに設定した Solana-DeFi ボルトで、ORCA(36万1,000ドル)/RENDER(54万ドル)をバッファーに加えたため、起動初日からどちらも入れ替え不能になりました。出来高下限を7万ドルに設定したミームベータのボルトでは、DRIFT(4万9,000ドル)をバッファーに加えたため、入れ替え不能になりました。
ユーザーが示した戦略(例:「厳選した3銘柄のみ」)が厳格なポリシーを求めているように見えても、ポリシーの範囲を狭めて厳格さを設定しないでください。戦略の意図はバスケットと notes に反映し、ポリシーの範囲はボルトを管理しやすく保つのに十分な広さにしてください。
バックエンドのペイロードルール
/launch-dtf ペイロードの場合:
- エージェントが作成するボールトでは、
category: 0(Manual)を指定します。 underlyingAssetsにはsymbolまたはname(推奨)を送信します。バックエンドがasset-allocationからmintAddressを解決します。- 厳格な制限:
underlyingAssetsに USDC(symbol: "USDC"またはname: "USD Coin")を絶対に含めないでください。 - 候補リストに USDC が含まれている場合は、削除し、
launch-dtfを送信する前に、対象となる別の USDC 以外のアセットに置き換えてください。 - アセット数:
underlyingAssetsのアセット数は最小 1、最大 12 です。範囲外のペイロードはバックエンドによって拒否されます。 policyオブジェクトは必須です。 エージェントが適用したいフィールドをすべて含めてください。省略されたフィールドは 0/無効がデフォルトになります。underlyingAssetsのバスケットは、サーバー側でpolicyに照らして評価されます(/policy/dry-runと同じルールです)。いずれかのルールに違反した場合、/launch-dtfはviolations[]を含む400を返し、トランザクションは作成されません。
/dtf-create ペイロードの場合:
- メタデータのみ。 ポリシーフィールド(
asset_mode、asset_whitelist、min_amm_liquidity_usdなど)は送信しないでください。ポリシーは/launch-dtfの実行時にすでにコミットされ、自動的に関連付けられています。 - 次の項目のみを残します:
transactionSignature、vaultName、vaultSymbol、vaultType、description、tags、logoUrl、bannerUrl、noRebalance。 vaultTypeは、標準ファンドの場合は"DTF"、イールド/LST ファンドの場合は"YIELD_DTF"に設定します。logoUrlとbannerUrlは空文字列に設定します。vaultNameとvaultSymbolは、launch-dtfで使用した値と一致させる必要があります。
未署名トランザクションを返す API 呼び出し用の署名ヘルパー
launch-dtf と distribute-fees は、Base64 エンコードされた未署名の VersionedTransaction を返します。署名して送信するには、次のパターンを使用します。
import { Keypair, VersionedTransaction, Connection } from "@solana/web3.js";
const bs58 = require("bs58").default || require("bs58");
async function signAndSendTransaction(
base64Tx: string,
keypair: Keypair,
connection: Connection
): Promise<string> {
const txBytes = Buffer.from(base64Tx, "base64");
const tx = VersionedTransaction.deserialize(txBytes);
tx.sign([keypair]);
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
preflightCommitment: "confirmed",
});
await connection.confirmTransaction(signature, "confirmed");
return signature;
}
// Usage — note the `data.onChain.transaction` path. The /launch-dtf and /distribute-fees
// responses are wrapped by the global ResponseMiddleware as { status, message, data: { ... } },
// so the unsigned tx string lives under `body.data.onChain.transaction`. Reading
// `body.onChain.transaction` returns undefined and the call to Buffer.from will throw
// "first argument must be of type string" — that error is the misread, not a backend bug.
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR!));
const connection = new Connection(process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com");
const sig = await signAndSendTransaction(response.data.onChain.transaction, keypair, connection);
ボールトのデプロイに関する重大なエラー処理ルール:
-
トランザクションに署名してオンチェーンで送信した後、
launch-dtfを再度呼び出さないでください。 オンチェーンでのボールト作成は取り消せず、USDC が必要です。dtf-createが失敗した場合は、同じトランザクション署名、ボールト名、シンボルを使用してdtf-createのみ再試行してください。新しい名前/シンボルを生成したり、launch-dtfを再度呼び出したりしないでください。 -
launch-dtfがポリシーのviolations[]を含む 400 を返した場合(オンチェーン送信前):ポリシーはコミットされておらず、トランザクションも作成されていないため、オンチェーンの費用は発生していません。違反コードに従ってバスケットまたはポリシーを修正し(Step 3 のドライランと同じガイダンスです)、launch-dtfを再試行してください。低コストで試行錯誤するため、先に/policy/dry-runを実行することを検討してください。 -
launch-dtfがほかの理由で失敗した場合(例:検証エラー、アセットが見つからない、USDC 不足):ペイロードを修正してlaunch-dtfを再試行してください。トランザクションはまだ作成されていないため、オンチェーンの費用は引き続き発生していません。 -
署名/送信に失敗した場合:新しい未署名トランザクションを取得するために、
launch-dtfを再試行してもかまいません。最初の呼び出しですでにポリシーはコミットされています。未関連付けの既存ポリシーが取得されるよう、まったく同じvaultNameとvaultSymbolを再利用してください(これらを変更すると、異なる名前のポリシーが新たにコミットされます)。 -
dtf-createが失敗した場合(オンチェーン送信の成功後):まったく同じtransactionSignature、vaultName、vaultSymbolを使用してdtf-createのみを再試行してください。これらの値は絶対に変更しないでください。launch-dtfを再度呼び出さないでください。ポリシーはすでにコミットされており、いずれにしてもチェーンイベントパイプラインによって関連付けられます。 -
トランザクション署名を保持してください:オンチェーン送信が成功した後は、署名を保存し、
dtf-createのすべての再試行で再利用してください。これはオンチェーンのボールトとデータベースレコードを結び付けるものです。
Step 5:管理(継続的、自律的)
起動後、エージェントは次の処理を自律的に行います。
GET {DFM_API_URL}/api/v2/agent/vaults/user?page=1&limit=10&vaultType=dtf&includeTvl=trueでユーザーのボールトを一覧表示します(ページネーション対応。利回りファンドの場合はvaultType=yield_dtfに切り替えます)GET {DFM_API_URL}/api/v2/agent/vaults/featured/list?page=1&limit=10&vaultType=dtf&includeTvl=trueでプラットフォームのおすすめボールトを閲覧しますGET {DFM_API_URL}/api/v2/agent/dtf/:symbol/stateでボールトの状態を監視しますGET {DFM_API_URL}/api/v2/agent/dtf/:symbol/rebalance/checkでリバランスの準備状況を確認しますPOST {DFM_API_URL}/api/v2/agent/dtf/:symbol/rebalanceでリバランスを実行します(管理者ウォレットがバックグラウンドで実行します)POST {DFM_API_URL}/api/v2/agent/dtf/:symbol/distribute-feesで発生した手数料を分配します(エージェントが署名する未署名のトランザクションを返します)
ボールトの一覧表示(/vaults/user、/vaults/featured/list): どちらのエンドポイントも同じ4つのクエリパラメーターを受け取ります。page、limit、vaultType(dtf | yield_dtf)、includeTvl です。/vaults/user は、ページネーションと種類のフィルタリングを使って呼び出し元自身のボールトを列挙するために使用します(ページネーションに対応していない旧式の /dtf/my-vaults よりも推奨)。/vaults/featured/list はプラットフォーム全体のおすすめボールトを表示するために使用します。必ず includeTvl=true を送信してください。どちらのエンドポイントも totalValueLocked と sharePrice を返す必要があります。pagination.hasNext が false になるまで(または pagination.page >= pagination.totalPages になるまで)page を進めてください。
自然言語のリクエストを page に対応付ける: ユーザーの表現をそのまま page クエリパラメーターに反映します。
| ユーザーの発言 | 送信する値 |
|---|---|
| "自分のボールトを見せて" / "自分のDTFを一覧表示して" | ?page=1&limit=10&...(必ず1ページ目から開始) |
| "2ページ目を見せて" / "2ページ" | ?page=2&limit=10&... |
| "おすすめボールトの5ページ目" | ?page=5&limit=10&...(/vaults/featured/list に対して) |
| "次のページ" / "もっと見せて" | ?page=<lastShown + 1>&...。前回の応答で pagination.hasNext === false だった場合は拒否し、「最後のページです」とユーザーに伝えます。 |
| "前のページ" / "戻って" | ?page=<lastShown - 1>&...。lastShown がすでに1の場合は、同じ1ページ目の結果を返します(page を1未満にしないでください)。 |
| "最初のページ" / "最初に戻って" | ?page=1&... |
| "最後のページ" | まず page=1 を呼び出して pagination.totalPages を読み取り、その後 ?page=<totalPages>&... を呼び出します |
| "1ページあたり25件を見せて" / "一度に50件取得して" | 指定されたとおりに limit を渡します(過剰な値の場合は100など妥当な上限に制限)。limit が変わった場合は page を1にリセットします。 |
ターンをまたいでページネーションの状態を記憶する: このスキルはステートレスであり、「現在のページ」を記憶する機能は組み込まれていません。「次のページ」や「前のページ」のリクエストを正しく解決できるよう、返した直近の pagination.page と pagination.totalPages を会話のコンテキスト内で管理してください。ユーザーがフィルター(vaultType、search、limit)を切り替えた場合は、結果セットが変わり、以前のページ番号が同じデータを指さなくなるため、page=1 にリセットしてください。
ページを取得した後は、ナビゲーション用のフッターをユーザーに表示します。例:「5ページ中2ページ目です。続けて見るには『次のページ』、ページを指定するには『Nページ』と言ってください。」どの操作を案内するかは、pagination.hasNext / pagination.hasPrev を使って判断してください。ページネーションをユーザーに隠さないでください。pagination.totalPages > 1 の場合、さらにページがあることを知らせる必要があります。
応答の形式(どちらのエンドポイントもページネーション対応で、エンベロープにはラップされません):
{
"data": [/* array of Vault objects */],
"pagination": { "page": 1, "limit": 10, "total": 5, "totalPages": 1, "hasNext": false, "hasPrev": false }
}
ボールトのリストは body.data にあります。data の直下にあるフラットな配列です。ボールトを順に処理するには body.data.map(...) を使い、ナビゲーションには body.pagination を参照してください。
よくある間違い — 次のように読み取らないでください:
| 誤ったアクセス方法 | 失敗する理由 | 正しいアクセス方法 |
|---|---|---|
body.data.vaults | 配列は body.data 自体です。ここにはネストされた vaults キーはありません。(レガシーの /dtf/my-vaults の形式と混同しています。こちらは { vaults: [...], total } でラップされています。) | body.data |
body.data.data | これらの2つのエンドポイントには pagination フィールドが含まれるため、グローバルな ResponseMiddleware は { status, message, data: ... } でラップしません。二重の data はありません。 | body.data |
body.vaults | これらのエンドポイントにはトップレベルの vaults フィールドはありません。 | body.data |
body.results / body.items | 汎用 API の感覚ではありますが、このコントラクトには当てはまりません。 | body.data |
レスポンスがラップされていない理由: グローバルな ResponseMiddleware インターセプターは、トップレベルに pagination フィールドがあるレスポンスをそのまま通します({ status, message, data } のエンベロープはありません)。このスキルの call() ヘルパーがこれを自動的に処理し、ページネーション付きのレスポンスは変更しません。リストエンドポイントで body.status === "success" が見つかった場合、上流で何か問題が起きています。形式が不正なレスポンスとして扱い、ユーザーに伝えてください。
Vault 項目のフィールド: body.data 内の各 Vault には、vaultName、vaultSymbol、vaultAddress、description、vaultIndex、tags[]、feeConfig.managementFeeBps、underlyingAssets[](それぞれにネストされた assetAllocation: { name, symbol, logoUrl } と pct_bps を含む)、creator(詳細なプロフィールオブジェクト。name、walletAddress、avatar、twitter_username を含む)、category: { name }、totalValueLocked、sharePrice、vaultApy、performance7d、および文字列型の nav と totalSupply が含まれます。すべてのフィールドの一覧は references/api-reference.md § 12a を参照してください。
エンドポイントとレスポンス形式の早見表(これらの違いは、「vault listing」に関する不具合の最も一般的な原因です):
| エンドポイント | レスポンス形式 | ステータス |
|---|---|---|
GET /vaults/featured/list | { data: Vault[], pagination: {...} } | こちらを使用 — ページネーション対応、現行。 |
GET /vaults/user | { data: Vault[], pagination: {...} } | こちらを使用 — ページネーション対応、現行。 |
GET /dtf/my-vaults | { vaults: Vault[], total: number } | レガシー/非推奨 — 形式が異なります。明示的に求められない限り、エージェントからこちらを読み取らないでください。「自分のVault」という表現はすべて /vaults/user にルーティングしてください。 |
Vault リストをユーザーに表示するときは、読みやすいフィールドを提示してください — vaultName / vaultSymbol、description、カンマ区切りの symbol pct% 形式で表した資産バスケット(pct_bps を100で割る)、USD 形式にした totalValueLocked、パーセンテージで表した performance7d、手数料率としての feeConfig.managementFeeBps / 100、および作成者としての creator.name(または creator.twitter_username)。_id、id、生の pct_bps、内部 Mongo フィールド、daoconfig: null は省略してください。 nav と totalSupply は小数の精度を保つ文字列です。計算を行う前に Number() でパースし、新しい Vault では "0"、null の vaultApy、null の performance7d を「まだデータなし」として扱ってください。
誤った例と正しい例 — ボールト一覧の出力
ユーザーは 「自分の DTF を一覧表示して」 に対して、次の不適切な出力を受け取りました。以下の括弧内の記述/フッターの断片はすべて禁止されています。
❌ WRONG:
Here are the DTFs currently available from your DFM account context
(/vaults/user, paginated across all pages):
DTF (9)
AIPIN — Solana AI + DePIN Infrastructure
TASR — TA Structure Rotation
…
Yield DTF (4)
YLST — Solana Yield LST
…
Total: 13 vaults (9 DTF + 4 Yield DTF)
If you want, I can also return this with TVL/share price sorting
(highest TVL first) or only "platform featured" vaults.
上記の禁止表現:
- 「DFM アカウントのコンテキスト(/vaults/user、すべてのページをページネーションで取得)」 — エンドポイントのパスとページネーションの内部処理が漏れています。「ユーザー向けの出力にエンドポイントのパスを含めない」というルールで禁止されています。
- 「(TVL の高い順)または『プラットフォームの注目』ボールトのみ」 — 求められていない、推測に基づく追加提案です。ユーザーが知る必要のない、別の「注目ボールト」一覧の存在を明かしています。
同じ一覧を正しく出力する場合は、必ず Markdown の表にします。短い導入文を 1 文、その後に表を続け、さらにページがある場合に限ってページネーションのフッターを付けます。ボールト一覧に箇条書きを使うことは明示的に禁止されています(「行動ガイドライン」の「出力形式」ルールを参照)。
✅ RIGHT:
Here are your vaults — 13 in total (9 DTFs and 4 Yield DTFs).
| Type | Symbol | Name | TVL (USD) | 7d % | Fee |
| --------- | ---------- | --------------------------------- | --------- | ------ | ----- |
| DTF | AIPIN | Solana AI + DePIN Infrastructure | $1,240.50 | +3.41% | 1.50% |
| DTF | TASR | TA Structure Rotation | $890.10 | -0.12% | 2.00% |
| DTF | OBLETH | Open Blacklist ETH | $0.00 | — | 1.50% |
| DTF | WHL3 | TriGuard White | $0.00 | — | 1.50% |
| DTF | POP-DTF | Popeye Index | $14.80 | +13.01%| 1.50% |
| DTF | EXECX | Execution Stack X | $0.00 | — | 2.00% |
| DTF | BARBL | Barbell Degen | $0.00 | — | 2.00% |
| DTF | AGE-DTF | Agentonomy | $14.80 | +13.01%| 1.50% |
| DTF | ICM-DTF | ICM Prime | $0.00 | — | 1.50% |
| Yield DTF | YLST | Solana Yield LST | $0.00 | — | 1.00% |
| Yield DTF | NX6-YDTF | NEXUS-6 | $0.00 | — | 2.00% |
| Yield DTF | REA-YDTF | Real Bastion | $0.00 | — | 2.00% |
| Yield DTF | AUR-YDTF | Aurum Stake | $0.00 | — | 2.00% |
ボールト一覧の列スキーマ(必須):
| 列 | ソース | 形式 |
|---|---|---|
Type | vaultType(「dtf」→「DTF」、「yield_dtf」→「Yield DTF」) | タイトルケース。生の enum は使用しない |
Symbol | vaultSymbol | そのまま |
Name | vaultName | そのまま |
TVL (USD) | totalValueLocked | $<n>.<2dp>。桁区切りを付け、0 の場合は $0.00 |
7d % | performance7d | +<n>.<2dp>% / -<n>.<2dp>%。null の場合は —(新しいボールトに「0.00%」を表示しない) |
Fee | feeConfig.managementFeeBps / 100 | <n>.<2dp>% |
任意の列 — ユーザーから求められた場合、または文脈上関連する場合にのみ追加します:Author(creator.name、または @ を付けた creator.twitter_username)、APY(vaultApy。null の場合は —)、Basket(pct_bps の上位 3 資産を SYM 40% / SYM 30% / SYM 30% の形式で表示)。
誤った出力からの変更点:
- 厳格な表形式 — すべてのボールトを行として表示します。一覧の形式を箇条書き、番号付きリスト、段落からエージェントが選ぶことはできません。Markdown の表のみを使用します。
- エンドポイントのパスは記載しません。 「DFM アカウントのコンテキスト(/vaults/user、…から)」ではなく、「あなたのボールト」とします。
- 「すべてのページをページネーションで取得」は記載しません — ページネーションは内部処理です。回答をまとめるために複数ページを取得しても、それについて何も言いません。
totalPages > 1で、1 ページ目しか取得していない場合に限り、表の下に 1 行のフッターを追加します:「N ページ中 1 ページ目です。続きは『次のページ』と言ってください。」 ページネーションに言及してよいのはこの場合のみで、関連性があるときに限ります。 - 求められていない追加提案はしません。 「ご希望であれば、…の形式でもお返しできます」 で終わらせないでください。
- 新しいボールトは TVL = $0.00 / 7d =
—— 数値を捏造しません。null/"0"は$0.00/—として表示します。
プラットフォーム上のすべての DTF を一覧表示する(両方のボールトタイプ、全ページ、注目と非注目)
ユーザーがプラットフォーム上のすべての DTF を尋ねた場合 — 「DFM の DTF をすべて一覧表示して」、「プラットフォーム上のすべてのボールトを見せて」、「DFM ではどんな DTF を利用できる?」、「DFM で利用可能なボールトの一覧を表示して」、「すべての DTF を見て」 — エージェントは、2 つの別個のエンドポイントを組み合わせたプラットフォーム全体の一覧表示フローを実行します(プラットフォームではこれらを別の一覧として公開しています。isFeaturedVault クエリフィルターはありません。捏造しないでください)。
| エンドポイント | 返される内容 | 用途 |
|---|---|---|
GET /vaults/user?vaultType=<type> | 指定したタイプの、プラットフォーム上のすべてのボールト。 | 「すべての DTF」の基準となる一覧。 |
GET /vaults/featured/list?vaultType=<type> | 注目のみ — プラットフォームの注目セクションに掲載された、厳選サブセット。 | 基準となる一覧と照合し、注目対象の行を示す。 |
どちらのエンドポイントも、同じ page / limit / vaultType / includeTvl クエリパラメーターを受け取り、同じページネーション付き { data: Vault[], pagination: {...} } 形式を返します(エンベロープによるラップはありません)。エージェントはそれぞれを個別に取得し、最後までページネーションしてから、会話のコンテキスト内で統合します。
アルゴリズム:
- エンドポイントごとに2種類の vault type で実行 —
vaultType=dtfとvaultType=yield_dtfで個別に実行します。エンドポイントはそれぞれ1種類のタイプのみでフィルタリングします。 - 各パスで最後までページネーションします。
page=1、limit=50(リクエスト回数を減らすため、大きめの上限)から開始します。各呼び出し後にpagination.hasNextを確認し、trueの場合はpageをインクリメントして繰り返します。hasNext === falseになったら終了します。必ずincludeTvl=trueを送信してください。 - Featured 列用に照合します —
/vaults/featured/listの結果からvaultSymbolの値を集めてSetを作成します。/vaults/userの各 vault について、シンボルがそのセットに含まれていれば_featured = true、そうでなければ_featured = falseに設定します。 - 1つの出力テーブルに統合します — vault type 列と featured フラグ列を含め、
/vaults/userから取得した一意の vault ごとに1行を表示します。
どちらのエンドポイントにも isFeaturedVault クエリパラメーターはありません。 &isFeaturedVault=true や &isFeaturedVault=false は追加しないでください。プラットフォームでは、全件と featured のみを2つのエンドポイントに分けて提供しています。
インラインの node -e の例 — 合計4回のフェッチパス(universal × 2種類の vault type と、featured × 2種類の vault type)を実行し、統合して、ユーザー向けテーブル用の JSON を1つ出力します。
node -e '
const http = require("http");
const https = require("https");
// Generic paginate-to-completion against either listing endpoint.
async function fetchAll(path, vaultType) {
const all = [];
let page = 1;
while (true) {
const url = new URL(process.env.DFM_API_URL
+ path
+ `?page=${page}&limit=50&includeTvl=true&vaultType=${vaultType}`);
const client = url.protocol === "https:" ? https : http;
const r = await new Promise((resolve, reject) => {
const req = client.get(url, {
headers: { "Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN }
}, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", () => {
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, body: data }); }
});
});
req.on("error", reject);
});
if (r.status !== 200) {
console.log("ERROR " + r.status + " from " + path + ": " + JSON.stringify(r.body));
return all;
}
all.push(...(r.body.data || []));
if (!r.body.pagination?.hasNext) break;
page += 1;
}
return all;
}
(async () => {
// Universal — every vault on the platform.
const dtfAll = await fetchAll("/api/v2/agent/vaults/user", "dtf");
const ydtfAll = await fetchAll("/api/v2/agent/vaults/user", "yield_dtf");
// Featured — curated subset surfaced on the platform.
const dtfFeatured = await fetchAll("/api/v2/agent/vaults/featured/list", "dtf");
const ydtfFeatured = await fetchAll("/api/v2/agent/vaults/featured/list", "yield_dtf");
// Build the featured-symbol set for the Featured column.
const featuredSymbols = new Set([
...dtfFeatured.map(v => v.vaultSymbol),
...ydtfFeatured.map(v => v.vaultSymbol),
]);
const rows = [
...dtfAll.map(v => ({ ...v, _type: "DTF", _featured: featuredSymbols.has(v.vaultSymbol) })),
...ydtfAll.map(v => ({ ...v, _type: "Yield DTF", _featured: featuredSymbols.has(v.vaultSymbol) })),
];
console.log(JSON.stringify({
totalDtf: dtfAll.length,
totalYieldDtf: ydtfAll.length,
featuredCount: featuredSymbols.size,
rows: rows.map(v => ({
type: v._type,
featured: v._featured,
symbol: v.vaultSymbol,
name: v.vaultName,
tvl: v.totalValueLocked,
perf7d: v.performance7d,
feeBps: v.feeConfig?.managementFeeBps,
apy: v.vaultApy,
})),
}));
})().catch(e => console.log("FATAL " + (e?.message || String(e))));
'
featured のみの一覧(ユーザーが 「featured vaults を表示して」、「featured のみ」、「プラットフォームでキュレーションされているものは?」 と言った場合)は universal のパスを完全に省略し、/vaults/featured/list?vaultType=dtf と /vaults/featured/list?vaultType=yield_dtf のみを呼び出します。それぞれをページネーションし、同じ統合テーブル形式で表示します。この出力の全行は定義上 featured です(Featured 列は不要です。視覚的な一貫性のために、全行に ⭐ を付けた列を表示してもかまいません)。
出力テンプレート(必須 — 統合テーブル1つのみ):
✅ RIGHT — platform-wide DTF listing:
Here are all DTFs on the platform — <total> in total (<dtfCount> DTFs and <yieldCount> Yield DTFs, of which <featuredCount> are platform-featured).
| Type | Featured | Symbol | Name | TVL (USD) | 7d % | Fee |
| --------- | -------- | ---------- | --------------------------------- | ---------- | ------- | ----- |
| DTF | ⭐ | AGE-DTF | Agentonomy | $14.80 | +13.01% | 1.50% |
| DTF | ⭐ | ICM-DTF | ICM Prime | $0.00 | — | 1.50% |
| DTF | | AIPIN | Solana AI + DePIN Infrastructure | $1,240.50 | +3.41% | 1.50% |
| DTF | | TASR | TA Structure Rotation | $890.10 | -0.12% | 2.00% |
| DTF | | POP-DTF | Popeye Index | $14.80 | +13.01% | 1.50% |
| DTF | | EXECX | Execution Stack X | $0.00 | — | 2.00% |
| DTF | | BARBL | Barbell Degen | $0.00 | — | 2.00% |
| DTF | | OBLETH | Open Blacklist ETH | $0.00 | — | 1.50% |
| DTF | | WHL3 | TriGuard White | $0.00 | — | 1.50% |
| Yield DTF | ⭐ | NX6-YDTF | NEXUS-6 | $0.00 | — | 2.00% |
| Yield DTF | ⭐ | REA-YDTF | Real Bastion | $0.00 | — | 2.00% |
| Yield DTF | ⭐ | AUR-YDTF | Aurum Stake | $0.00 | — | 2.00% |
| Yield DTF | | YLST | Solana Yield LST | $0.00 | — | 1.00% |
列のスキーマ(プラットフォーム全体の一覧):
| 列 | ソース | 形式 |
|---|---|---|
| Type | 派生値(vaultType → "DTF" / "Yield DTF") | タイトルケース。 |
| Featured | vault のシンボルが /vaults/featured/list の結果セットに含まれる場合は true、それ以外は false。(レスポンスのフィールドではなく、照合による値です。) | featured の場合は ⭐、それ以外は空欄(— や false は不可)。 |
| Symbol | vaultSymbol | そのまま |
| Name | vaultName | そのまま |
| TVL (USD) | totalValueLocked | 千単位の区切りを付けた $<n>.<2dp>。0 の場合は $0.00。 |
| 7d % | performance7d | +/-<n>.<2dp>%。null の場合は —。 |
| Fee | feeConfig.managementFeeBps / 100 | <n>.<2dp>% |
並べ替え順(必須): まずタイプごとにグループ化(DTF を先、Yield DTF を後)し、次に各グループ内で featured の行を非 featured の行より前に並べ、その後 totalValueLocked の降順にします。これにより、各グループで重要度の高い vault が先頭に表示されます。
ページネーションのフッター: プラットフォーム全体の一覧には絶対に表示しないでください。エージェントはすでに両方の vault type について最後までページネーションしています。ユーザーの「すべての DTF」という意図は、1つの応答で満たされます。統合した一覧が扱いきれないほど長い場合(たとえば50行以上)は、フィルターを提案する1行を追加します。「プラットフォーム上のすべてを表示しました。featured のみ、DTF のみ、または TVL で絞り込みますか?」
この出力で禁止されること:
- 「
/vaults/featured/listから」など、エンドポイントへの言及。 - 「全ページをページネーションしました」—データの取得方法をユーザーに知らせる必要はありません。
- featured と非 featured を別々のテーブルに分けること — Featured 列で区別し、1つのテーブルにまとめます。
- ユーザーが「すべて」と言った場合に、featured vault のみを一覧表示すること — ユーザーは両方を明示的に求めています。
ユーザーが後からフィルター(「featured のみ」/「DTF のみ」/「TVL が最も高いもの」)を指定した場合: 該当する行でフィルタリングした同じ統合テーブルを再表示します。すでに会話のコンテキストに全件があるため、再取得はしないでください。
詳細一覧モード — レスポンスのすべてのフィールドを表示
ユーザーがプラットフォームの DTF について、各ボールトの完全なデータを求めていることが伝わる形で**「一覧」を求めた場合 — 「すべての DTF の一覧をください」、「DFM 上のすべてのボールトの詳細データをください」、「詳細情報付きですべてのボールトを一覧表示して」、「すべてのボールトの完全な情報を表示して」、「利用可能な DTF は何ですか — 完全なデータをください」 など — 簡潔なサマリーテーブルではなく、詳細形式を表示します。詳細形式は置き換えではなく追加**です。同じ簡潔なサマリーテーブルを先頭に表示し、その下にボールトごとの詳細ブロックを表示します。
詳細モードを選択する表現(上記のサマリーのみモードとの比較):
| 表現 | モード |
|---|---|
| 「すべての DTF を表示して」、「利用可能な DTF は何ですか」、「ボールトを閲覧」、「DTF を一覧表示して」 | サマリーテーブルのみ(上記の §「プラットフォームのすべての DTF を一覧表示する」フロー)。 |
| 「一覧をください」、「リストをください」、「詳細データをください」、「詳細付きで一覧表示して」、「すべてのボールトの完全なデータを表示して」、「各ボールトについてわかることをすべて教えて」、「すべての DTF の詳細な一覧」 | 詳細モード — 先頭にサマリーテーブルを表示し、その後にボールトごとの詳細ブロックを表示します。 |
表現が曖昧な場合は、詳細モードを優先します。「一覧」を求めるユーザーは、通常、シンボルや TVL だけでなく、プラットフォームが各ボールトについて持つデータも求めています。
アルゴリズム: サマリーのみのフローと同じです(fetchAll を 4 回実行し、featuredSymbols セットを作成してマージ)。異なるのは表示方法です。まずサマリーテーブルを出力し、次にマージした行を同じソート順で反復し、ボールトごとに 1 つの詳細ブロックを表示します。
出力テンプレート(詳細モード — サマリーテーブル+ボールトごとの詳細ブロック):
✅ RIGHT — detailed platform-wide DTF listing:
Here are all DTFs on the DFM platform — <total> in total (<dtfCount> DTFs and <yieldCount> Yield DTFs, of which <featuredCount> are platform-featured). Summary at the top, full details for each vault below.
| Type | Featured | Symbol | Name | TVL (USD) | 7d % | Fee |
| --------- | -------- | ---------- | --------------------------------- | ---------- | ------- | ----- |
| DTF | ⭐ | AGE-DTF | Agentonomy | $19.34 | 0.00% | 1.50% |
| DTF | ⭐ | ICM-DTF | ICM Prime | $11.97 | 0.00% | 1.50% |
| DTF | | TASR | TA Structure Rotation | $11.17 | 0.00% | 2.00% |
| ... | | ... | ... | ... | ... | ... |
---
**1. AGE-DTF — Agentonomy** ⭐
*AI agent tokens basket designed for narrative exposure to autonomous agents, agent launchpads, and "agent-driven markets". This is intentionally higher volatility and more reflexive than DeFi.*
Tags: AI-agents, agent-economy, high-beta, narrative-index, experimental
Overview:
| Field | Value |
| ------------- | ----------------- |
| Symbol | AGE-DTF |
| Type | DTF |
| Category | Manual |
| Vault index | 53 |
| Vault address | FdxU…tp8B |
| TVL | $19.34 |
| Share price | $1.7615 |
| NAV | $19.34 |
| Total supply | 10.981699 |
| Mgmt fee | 1.50% |
| 7d perf | 0.00% |
| APY | — |
| Creator | Tops (HzgvD…rUrM) |
Composition (7 assets):
| Symbol | Asset | Allocation |
| ------- | ----------------- | ---------- |
| VIRTUAL | Virtual Protocol | 40.00% |
| ZEREBRO | zerebro | 25.00% |
| RENDER | Render Token | 15.00% |
| NOS | Nosana | 12.00% |
| GRASS | Grass | 6.00% |
| IO | IO | 1.00% |
| DITH | Dither | 1.00% |
---
**2. ICM-DTF — ICM Prime** ⭐
*Solana's Internet Capital Markets stack in one index: issuance and attention markets plus the core rails for trading, leverage, and settlement.*
Tags: ICM, capital-markets, Solana, trading-rails, infra, high-beta
Overview:
| Field | Value |
| ------------- | ----------------- |
| Symbol | ICM-DTF |
| Type | DTF |
| Category | Manual |
| Vault index | 47 |
| Vault address | 3eH1…Vuqx |
| TVL | $11.97 |
| Share price | $0.9882 |
| NAV | $11.97 |
| Total supply | 12.111534 |
| Mgmt fee | 1.50% |
| 7d perf | 0.00% |
| APY | — |
| Creator | Tops (HzgvD…rUrM) |
Composition (9 assets):
| Symbol | Asset | Allocation |
| ------ | ------------- | ---------- |
| JUP | Jupiter | 18.00% |
| RAY | Raydium | 16.00% |
| PYTH | Pyth Network | 15.00% |
| ORCA | Orca | 12.00% |
| PUMP | Pump | 12.00% |
| DRIFT | Drift | 10.00% |
| KMNO | Kamino | 10.00% |
| META | MetaDAO | 5.00% |
| ORE | ORE | 2.00% |
---
(...repeat one detail block per vault, in the same sort order as the summary table...)
ボールトごとの詳細ブロック — フィールドスキーマ:
| ブロック要素 | ソース | 形式/ルール |
|---|---|---|
| 見出し | vaultSymbol + vaultName | **<n>. <symbol> — <name>**。ボールトが注目対象の場合は ⭐ を付加します。<n> はマージ後のソート順における行インデックスです。 |
| 説明行 | description | イタリック体で 1 行として表示します。description が null/空の場合は行全体を省略します。 |
| タグ行 | tags[] | Tags: <a>, <b>, <c>。tags が空/存在しない場合は行全体を省略します。 |
| 概要 → シンボル | vaultSymbol | そのまま |
| 概要 → 種類 | 派生値(dtf → DTF、yield_dtf → Yield DTF) | タイトルケース。 |
| 概要 → カテゴリー | category.name | そのまま。null の場合は — を表示します。 |
| 概要 → ボールトインデックス | vaultIndex | 整数。存在しない場合は —。 |
| 概要 → ボールトアドレス | vaultAddress | <先頭4文字>…<末尾4文字> に切り詰めます(例:FdxU…tp8B)。完全なアドレスは絶対に表示しないでください。 |
| 概要 → TVL | totalValueLocked | 千位区切り付きの $<n>.<小数点以下2桁>。0/null の場合は $0.00。 |
| 概要 → シェア価格 | sharePrice | $<n>.<小数点以下4桁>。null の場合は —。 |
| 概要 → NAV | nav(小数を表す文字列 — Number() で解析) | $<n>.<小数点以下2桁>。null の場合は —。 |
| 概要 → 総供給量 | totalSupply(小数を表す文字列) | <n>.<小数点以下6桁>。null の場合は —。 |
| 概要 → 運用手数料 | feeConfig.managementFeeBps / 100 | <n>.<小数点以下2桁>%。 |
| 概要 → 7 日間のパフォーマンス | performance7d | +/-<n>.<小数点以下2桁>%。null の場合は —。0 の場合は +0.00% ではなく 0.00% と表示します。 |
| 概要 → APY | vaultApy | <n>.<小数点以下2桁>%。null/0 の場合は —。 |
| 概要 → 作成者 | creator.name + creator.walletAddress | <name> (<先頭4文字>…<末尾4文字>)。creator が null の場合は — を表示します。ウォレット全体、作成者の _id、メールアドレス、アバター URL、ソーシャルリンク、Twitter ハンドルは内部情報のため、絶対に表示しないでください。 |
| 構成の見出し | underlyingAssets.length | 構成(<N> 資産): |
| 構成行 → シンボル | underlyingAssets[].assetAllocation.symbol | そのまま |
| 構成行 → 資産 | underlyingAssets[].assetAllocation.name | そのまま |
| 構成行 → 割合 | underlyingAssets[].pct_bps / 100 | <n>.<小数点以下2桁>%。行は pct_bps の降順でソートします。 |
ボールト間の区切り: --- を単独の行に表示します。最後のボールトの後には区切りを表示しません。
詳細出力で禁止される項目(要約モードで禁止される項目に加えて):
- 内部 Mongo の
_idフィールド(vault、asset allocation、category、creator)は、絶対に表示しないでください。 - ウォレットアドレスまたはvaultアドレスの全体は表示しないでください。必ず
<first4>…<last4>の形式に短縮してください。 logoUrl、avatar、twitter_username、email、socialLinks、useAvatarImage、useTwitterImageは内部情報/個人情報です。チャットには絶対に表示しないでください。- レスポンス内の
daoconfig、_id、重複したidはバックエンドの生成物です。 pct_bpsを生の整数(「2600 bps」)として表示しないでください。必ずパーセントに変換してください。nav/totalSupply/sharePriceを変換せず、生の6桁小数文字列(「960675」、「1.000016」)として表示しないでください。必ず上記のスキーマに従って解析し、書式を整えてください。- 「これはプラットフォームのデータベースからの情報です」/「/vaults/user から取得しました」といった表現や、その他のバックエンド内部に関する表現は使わないでください。
- 要約テーブルを省略して、詳細ブロックだけを表示しないでください。必ず要約を先頭に配置してください。
空またはデータが少ないレスポンス: 両方のエンドポイントで pagination.total === 0(DTF が1つもない)の場合は、テーブルや詳細ブロックを表示せず、次の1行だけを返してください:"DFM プラットフォームには、まだ DTF がありません。" 片方の vault タイプだけに行がある場合は、要約ヘッダー行からデータのないタイプを省略してください("<dtfCount> DTFs and 0 Yield DTFs" → "<dtfCount> DTFs")。
長さ制限: 結合した一覧に vault が20件を超えて含まれる場合は、全行を要約テーブルに表示しますが、詳細ブロックは上位20件(並べ替え順)までに制限し、末尾に次の1行を追加してください:"上位20件の vault の詳細を表示しています。シンボルまたはフィルター(only featured、only Yield DTFs、top 5 by TVL)を指定すると、任意の範囲を表示します。" vault が20件以下の場合は、すべての詳細ブロックを表示してください。
/dtf/:symbol/state の表示 — 複数テーブル形式
vault-state レスポンスには、vault、portfolio、userHoldings、rebalanceHistory.data[] の4つのセクションが含まれます。それぞれを、明確な見出しを付けた独立した Markdown テーブルにしてください。最初に vault 名を示す1行のヘッダーを配置します。データが空または null のセクションは省略してください。
✅ RIGHT (full state output):
**AIPIN — Solana AI + DePIN Infrastructure**
Vault overview:
| Field | Value |
| ------------ | --------------- |
| Symbol | AIPIN |
| Type | DTF |
| Status | active |
| TVL | $1,240.50 |
| Share price | $1.0823 |
| NAV | $1,240.50 |
| Total tokens | 1,146.231410 |
| Mgmt fee | 1.50% |
| 7d perf | +3.41% |
| APY | — |
Portfolio (current holdings):
| Symbol | Allocation | Balance | Value (USD) |
| ------- | ---------- | ------------ | ----------- |
| ZEREBRO | 35.00% | 142.318 | $434.18 |
| RENDER | 30.00% | 88.512 | $372.15 |
| ORCA | 20.00% | 211.044 | $248.10 |
| HNT | 15.00% | 67.901 | $186.07 |
Your holdings:
| Field | Value |
| ------------- | --------------- |
| Wallet | 5HvU…A7B8 |
| Token balance | 2.543171 AIPIN |
| Value | $2.7536 |
| Share | 0.22% |
Recent rebalances:
| Date (UTC) | Status | USDC in | USDC out | Duration |
| ------------------- | ---------- | --------- | --------- | -------- |
| 2026-04-21 14:02 | completed | $1,184.32 | $1,182.10 | 47s |
| 2026-04-08 09:17 | completed | $1,021.05 | $1,019.88 | 39s |
Vault 概要テーブル — 列のスキーマ:
| フィールド | ソース | 書式 |
|---|---|---|
| シンボル | vault.vaultSymbol | そのまま |
| タイプ | vault.vaultType | "dtf" → "DTF"、"yield_dtf" → "Yield DTF" |
| ステータス | vault.status | そのまま(小文字) |
| TVL | vault.totalValueLocked | $<n>.<2dp>、桁区切り付き |
| シェア価格 | vault.sharePrice | $<n>.<4dp>(TVL より精度を高くする) |
| NAV | vault.nav(10進数を文字列で保持 — 最初に Number() で解析) | $<n>.<2dp> |
| トークン総数 | vault.totalTokens(10進数を文字列で保持) | <n>.<6dp> |
| 管理手数料 | vault.feeConfig.managementFeeBps / 100 | <n>.<2dp>% |
| 7日間のパフォーマンス | vault.performance7d | +/-<n>.<2dp>%、null の場合は — |
| APY | vault.apy | <n>.<2dp>%、null/0 の場合は — |
ポートフォリオテーブル — portfolio.assets[] 内のアセットごとに1行:
| 列 | ソース | 書式 |
|---|---|---|
| シンボル | assets[].symbol | そのまま。ここでは生の mintAddress を絶対に表示しないでください |
| 割り当て比率 | assets[].allocationBps / 100 | <n>.<2dp>% |
| 残高 | assets[].balance(生の u64 — 10^decimals で割る) | <n>.<6dp> |
| 金額(USD) | assets[].valueUsd | $<n>.<2dp> |
allocationBps の降順に行を並べ、最大のポジションを先頭にしてください。
保有状況テーブル — userHoldings.tokenBalance > 0 の場合にのみ表示してください。walletAddress は <first4>…<last4> の形式(例:5HvU…A7B8)に短縮してください。ユーザー向けの出力でアドレス全体を表示しないでください。
直近のリバランスのテーブル — rebalanceHistory.data[] が空でない場合にのみ表示し、直近の行を最大5件まで表示してください。startTime は UTC の YYYY-MM-DD HH:mm に変換してください。Duration 列は executionDurationMs / 1000 を四捨五入し、末尾に s を付けてください。_id、ヘッダーにすでに含まれる vaultSymbol、sellPhaseTransactions および buyPhaseTransactions の配列は非表示にしてください。これらは要約には細かすぎるため、ユーザーが「どのアセットがスワップされましたか」と尋ねた場合にのみ表示してください。
すべての管理操作は単一の API 呼び出しで完了します。確認は不要です。
ステップ 6:基盤資産を更新する(4 フェーズのフロー)
ユーザーが 「基盤資産を更新して」、「バスケットを変更して」、「資産を入れ替えて」、「X にリバランスして」、またはボルトの underlyingAssets の配分を置き換えることを意味する表現を使った場合は、この 4 フェーズのフローを実行します。4 つのフェーズすべてが必須であり、各フェーズでユーザーの確認を求めずに自律的に実行します。 フェーズ 4(リバランス)は、例外なく資産更新フローの最後に必ず実行します。リバランスを行わずにバスケットだけを変更すると、ボルトのオンチェーン上の目標と実際の保有資産が一致しなくなるため、リバランスは操作の一部であり、任意のフォローアップではありません。
┌──────────────────────────────────────────────────────────────────────────┐
│ UPDATE UNDERLYING — four-phase flow │
│ │
│ Phase 1: DECIDE THE NEW BASKET │
│ - WebSearch / WebFetch + GET /market-metrics → candidate assets │
│ - Match against the vault's existing constitutional policy │
│ (GET /dtf/:symbol/policy → asset_mode, whitelist, min liquidity) │
│ - Choose `mintBps` allocations summing to 10000 │
│ - Prefer `symbol` / `name` identifiers — backend resolves to mints │
│ │
│ Phase 2: BUILD ON-CHAIN TX (POLICY-GATED, SERVER-SIDE) │
│ POST /api/v2/agent/vaults/:symbol/update-assets-tx │
│ { signerPublicKey, underlyingAssets: [{ symbol, mintBps }, ...] } │
│ │
│ ├─ 400 + violations[] ─▶ Phase 1 with adjustments (loop) │
│ └─ 201 + base64 tx ─▶ Phase 3 │
│ │
│ Sign locally with DFM_AGENT_KEYPAIR → submit on-chain → confirm │
│ │
│ Phase 3: SYNC DB │
│ PATCH /api/v2/agent/vaults/:symbol/underlying-assets-by-mint │
│ { underlyingAssets: [{ mintAddress, pct_bps }, ...] } │
│ (Note: pct_bps, NOT mintBps — different field name) │
│ Auto-flushes agent:vaults:* caches. │
│ │
│ Phase 4: REBALANCE (STRICTLY MANDATORY — RUNS AUTOMATICALLY) │
│ POST /api/v2/agent/dtf/:symbol/rebalance │
│ { signerPublicKey } │
│ Admin wallet executes swaps server-side. No user confirmation gate │
│ — this phase ALWAYS runs after Phase 3 completes. A basket change │
│ without the rebalance leaves the vault drifting (on-chain target ≠ │
│ actual holdings), so the rebalance is part of the operation. │
└──────────────────────────────────────────────────────────────────────────┘
フェーズ 1 — 新しいバスケットを決める
ユーザーの意図に基づいて、追加、削除、リバランスする資産を特定します。ローンチ時と同じリサーチツールを使用します。
- トークンの発掘には、
WebSearch/WebFetchを使用します(価格動向、ナラティブ、時価総額)。 - Jupiter の流動性/24 時間取引量については、
GET /market-metrics?symbols=A,B,Cを使用します。この値は、ポリシーエンジンがルール 2/3 の適用に使用する正式な値と同じです。 GET /dtf/:symbol/policyを使用して、ボルトの既存のasset_mode、asset_whitelist、asset_blacklist、min_amm_liquidity_usd、min_24h_volume_usd、max_asset_pct、min_asset_pct、min_stablecoin_pctを読み取ります。フェーズ 2 のポリシーゲートで拒否されないよう、新しいバスケットを調整します。このフローで編集できるのはバスケットのみで、ポリシーは固定です。
各資産の mintBps を選び、すべての値の合計が正確に 10000 になるようにします。必要に応じてゼロから離れる方向に丸め、余りは最大の配分に加算します。
mintAddress の代わりに symbol または name を渡すこともできます。バックエンドは asset-allocation コレクションを介してそれらを解決します(/launch-dtf と同じ経路です)。サーバー側で解決するほうがコストが低く、エージェントが独自のミントアドレスの対応表を維持する必要もありません。
フェーズ 2 — オンチェーントランザクションの構築(ポリシーによる制限あり)
# Inline node script — never echo the JWT or the keypair to terminal
node -e '
const http = require("http");
const https = require("https");
const url = new URL(process.env.DFM_API_URL + "/api/v2/agent/vaults/" + process.argv[1] + "/update-assets-tx"); // process.argv[1] = vaultSymbol (e.g. ALPHA)
const client = url.protocol === "https:" ? https : http;
const { Keypair, VersionedTransaction, Connection } = require("@solana/web3.js");
const bs58 = require("bs58").default || require("bs58");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR));
const payload = JSON.stringify({
signerPublicKey: keypair.publicKey.toBase58(),
underlyingAssets: [
{ symbol: "SOL", mintBps: 5000 },
{ symbol: "JUP", mintBps: 3000 },
{ name: "Bonk", mintBps: 2000 }
]
});
const req = client.request(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
"Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN
}
}, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", async () => {
const json = JSON.parse(data);
if (res.statusCode === 201) {
// Sign locally + submit on-chain
const conn = new Connection(process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com");
const tx = VersionedTransaction.deserialize(Buffer.from(json.transaction, "base64"));
tx.sign([keypair]);
const sig = await conn.sendRawTransaction(tx.serialize(), { preflightCommitment: "confirmed" });
await conn.confirmTransaction(sig, "confirmed");
console.log("ON_CHAIN_OK signature=" + sig + " vaultIndex=" + json.vaultIndex);
} else if (res.statusCode === 400 && json.ok === false) {
// Policy violation — every violated rule in violations[]
console.log("POLICY_VIOLATION");
for (const v of json.violations || []) {
console.log(" " + v.violationCode + ": " + v.message);
}
} else {
console.log("ERROR " + res.statusCode + ": " + data);
}
});
});
req.on("error", (e) => console.log("ERROR: " + e.message));
req.write(payload);
req.end();
' <vaultSymbol>
timeout: 600000(10 分)を指定して実行します。オンチェーンの確認には時間がかかることがあります。
ポリシー違反の場合(HTTP 400 + { ok: false, violations[] }): violations[] の各項目を読み取り、コードを「次の理由でブロックされました」という 1 行のメッセージにまとめてユーザーに伝えた後、調整を加えてフェーズ 1 を自動的に再試行します。
| 違反コード | 変更内容 |
|---|---|
rule2MinAmmLiquidity / rule3Min24hVolume | 該当するアセットを除外するか、より流動性の高いアセットに置き換えます。ボールトのポリシーしきい値は固定されているため、バスケット側を調整する必要があります。 |
rule4MinMaxAssetCount | 提案されたバスケットのサイズが min_assets/max_assets に適合していません。上限と下限は固定されています。バスケットの構成を変更するか、ボールトが min_assets == max_assets で開始されている場合は、以下の「構造的にロックされたポリシー」を参照してください。 |
rule5MaxPctPerAsset | 該当するアセットの mintBps を減らし、他のアセットに再配分します。 |
rule6MinPctPerAssetIfHeld | 該当するアセットの割合を下限以上に引き上げるか、バスケットから完全に除外します。 |
rule7MinStablecoinFloor | ステーブルコインの配分を追加します(asset-allocation にすでに含まれ、asset_mode で許可されているものに限ります)。 |
rule1WhitelistBlacklist / assetModeViolation | 該当するアセットを削除します(ボールトの既存ポリシーでブラックリストに登録されているか、ホワイトリストに登録されていません)。 |
更新ごとの移動上限(拒否理由に max_rebalance_pct が含まれる場合) | 移動量の合計は、すべてのミントについての ` |
フェーズ 1 → フェーズ 2 のループは最大 3 回まで実行します。3 回試行しても失敗する場合は、解決されていない違反をユーザーに提示します。
構造的にロックされたポリシーの診断: min_assets == max_assets のために同じ rule4MinMaxAssetCount が繰り返し発生する場合、または単一アセットのスワップがすべて max_rebalance_pct を超える場合(確認式:2 × max_asset_pct > max_rebalance_pct)、または含まれているアセットの取引量が min_24h_volume_usd の 30% 以内にあり、そのアセットを含むすべての移行が rule3Min24hVolume によって拒否される場合、ボールトは開始時に誤って設定されており、エージェントでは自己修復できません。変更が必要な具体的な制約とともに、診断結果をユーザーに伝えて処理を停止します。計算上修正不可能なボールトに対して、際限なくループしてはいけません。
署名: Pre-Flight Auth セクションにある既存の signAndSend ヘルパーを使用します。署名済みトランザクションを Solana RPC に送信し、フェーズ 3 に進む前に confirmed コミットメントを待ちます。
フェーズ 3 — DB の同期
オンチェーントランザクションの確認後、PATCH /api/v2/agent/vaults/:symbol/underlying-assets-by-mint を実行して DB レコードを更新します。フィールド名が異なる点に注意してください。
- フェーズ 2(
/update-assets-tx)ではmintBpsを使用します(オンチェーン命令に対応)。 - フェーズ 3(
/underlying-assets-by-mint)ではpct_bpsを使用します(DB スキーマに対応)。
数値は同じですが、キーが異なります。このエンドポイントでは必ず mintAddress を含めてください(シンボルや名前による解決は行われません)。
node -e '
const http = require("http");
const https = require("https");
const url = new URL(process.env.DFM_API_URL + "/api/v2/agent/vaults/" + process.argv[1] + "/underlying-assets-by-mint"); // process.argv[1] = vaultSymbol (e.g. ALPHA)
const client = url.protocol === "https:" ? https : http;
const payload = JSON.stringify({
underlyingAssets: [
{ mintAddress: "So11111111111111111111111111111111111111112", pct_bps: 5000 },
{ mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", pct_bps: 3000 },
{ mintAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", pct_bps: 2000 }
]
});
const req = client.request(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
"Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN
}
}, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", () => console.log("DB_SYNC " + res.statusCode));
});
req.on("error", (e) => console.log("ERROR: " + e.message));
req.write(payload);
req.end();
' <vaultSymbol>
両方のフェーズが必要な理由: フェーズ 2 はオンチェーンの状態を変更し、フェーズ 3 はその変更を読み取りエンドポイント(/vaults/user、/vaults/featured/list)ですぐに確認できるようにします。チェーンイベントのパイプラインは最終的に DB を自動で補完しますが、PATCH を使えば同期的に反映できます。ユーザーは「基盤アセットを更新して」と指示した直後に「自分のボールトを表示して」と求めた際、新しいバスケットがすぐに表示されることを期待しています。
フェーズ 4 — リバランス(厳格に必須、自動実行)
フェーズ 4 は、すべての資産更新フローの締めくくりのステップとして必ず実行されます。ユーザーへの確認を待つことはなく、拒否する手順もなく、エージェントが任意の操作として表現することもありません。フェーズ 3 の PATCH が 200 を返したら、エージェントは直ちに /rebalance を呼び出し、完了を待ちます。フローの開始時にユーザーに確認するのは一度だけです(「バスケットを更新してよいですか?」)。この一度の確認で、このフェーズを含む4つのフェーズすべてが対象になります。リバランスは操作の一部であり、追加の確認事項ではありません。
厳格に必須である理由: フェーズ 2 ではオンチェーンのバスケットの目標(ボルトが保有すべき資産)を変更します。フェーズ 4 では、ボルトの実際の保有資産を新しい目標に合わせます。フェーズ 4 を省略すると、ボルトはいつまでも目標から乖離し続けます。つまり、オンチェーンの目標と実際の保有資産が一致せず、その乖離中に預け入れがあると誤った配分が使用されます。リバランスなしのバスケット変更が正しい結果となるユースケースはありません。
呼び出し:
POST {DFM_API_URL}/api/v2/agent/dtf/:symbol/rebalance に { signerPublicKey } を指定します。
スタンドアロンのリバランスフローと同じ /rebalance エンドポイントを使用します。スワップは管理者ウォレットがサーバー側で実行するため、エージェントはリクエストを送信して応答を待ち、結果を提示するだけです。フェーズ 2 / フェーズ 3 ですでに取得している vaultSymbol(vaultId ではありません)を使用します。
応答の処理:
200/201でok: true→ フェーズ 4 完了です。最終要約用にリバランスの署名を記録します。policyCheck.flagged: true(実行後のレビューでフラグが立った場合)が含まれている場合は、違反コード表に従って翻訳し、ユーザー向けの要約にブロックしない警告として追加します。リバランスは完了しています。フラグが立っていることは失敗を意味しません。- エラー応答 → フェーズ 1〜3 が成功していても、フェーズ 4 は失敗です。フェーズ 2 またはフェーズ 3 を再実行しないでください(バスケットの変更はすでにオンチェーンおよび DB 上で完了しています)。ユーザーには次のように伝えます:「バスケットの更新は反映されましたが、リバランスを完了できませんでした。ボルトの保有資産はまだ以前の構成のままです。このステップだけ再試行するには、『rebalance
<symbol>』と伝えてください。」 ターンを終了します。
フェーズ 4 でユーザーに提示する出力で禁止される表現: 「今すぐ実行することを確認しますか?」、「リバランスしますか?」、「リバランスを進めてもよいですか?」、またはリバランスを任意の操作として扱うその他の表現。フェーズ 4 は質問ではありません。
すべてのフェーズが成功した後
ユーザーに1行の要約を提示します:「<vaultName> のバスケットを <asset1 pct1%, asset2 pct2%, ...> に更新し、リバランスしました。オンチェーン署名 — 更新:<updateSig>、リバランス:<rebalanceSig>。」エージェントは、ユーザー向けのメッセージでエンドポイント名、ペイロードの形式、HTTP メソッドを決して明かしてはいけません。結果のみを提示します。
希望する変更が max_rebalance_pct を超える場合の複数ステップ移行
一部のバスケット移行では、全ミントの |new_bps - old_bps| の合計がボルトの max_rebalance_pct を超えるため、単一の /update-assets-tx では完了できません。その場合、エージェントは2回のバスケット更新に分割し、その間に必須のクールダウン待機を設ける必要があります(クールダウンは min_rebalance_interval_hours によって制御される rule9MinTimeBetweenRebalances で適用されます)。
このパターンが適用される場合:
- エージェントは
/update-assets-txを送信する前に合計移動量を計算します。max_rebalance_pctを超える場合は、バックエンドに拒否されるのを待たず、あらかじめ中間バスケットを計画します。 - または、バックエンドが
rule8MaxPctRebalancedPerTx/ 1回の更新あたりの移動量上限を示す400を返した場合も、同じ応答としてエージェントは分割を計画します。
2ステップのパターン:
Step 1 (now): transitional basket — moves some weight, fits the cap.
├─ /update-assets-tx (Phase 2)
├─ sign + submit
├─ /underlying-assets-by-mint (Phase 3 — DB sync)
└─ /rebalance (Phase 4 — fan-in actually moves the holdings)
…wait at least min_rebalance_interval_hours…
Step 2 (later): final basket — moves the remaining weight.
├─ /update-assets-tx (Phase 2)
├─ sign + submit
├─ /underlying-assets-by-mint (Phase 3)
└─ /rebalance (Phase 4)
移行途中のバスケットの算出: 次の条件を満たす配分を選びます。
- 合計
|new_bps - old_bps|がmax_rebalance_pct - 500以下(上限を 500 bps 下回る安全バッファーを設けます)。 - 移行途中のバスケットが現在の配分から最終配分へ向かう道筋上にあること。最終配分に向けて部分的に移行し、任意の中間配分にはしません。
- 移行途中の各資産が、それぞれ単独でポリシーゲートを通過すること(すぐに取り除く資産を新たに加えてはいけません)。
ステップ 1 を開始する前に、必ず最初に平易な英語で計画をユーザーに伝えます。ユーザーが、単一の更新ではなく 2 段階の移行に同意していると理解できるようにします。
✅ RIGHT — multi-step plan announcement (table format):
The change you've requested is larger than this vault permits in a single
basket update. I'll do it in two stages, with a `<min_rebalance_interval_hours>`-hour
wait between them. Each stage updates the basket on-chain AND rebalances
the holdings to match — no separate rebalance step needed.
Stage 1 (now):
| Symbol | From | → To | Δ |
| ------- | ------- | ------- | -------- |
| HNT | 25.00% | → 0.00%| -25.00% |
| SOL | 0.00% | → 20.00%| +20.00% |
| RENDER | 30.00% | → 25.00%| -5.00% |
| ZEREBRO | 25.00% | → 30.00%| +5.00% |
| ORCA | 20.00% | → 20.00%| — |
| PUMP | 0.00% | → 5.00%| +5.00% |
Stage 2 (after cooldown clears):
| Symbol | From (post-Stage-1) | → To | Δ |
| ------- | ------------------- | ------- | -------- |
| SOL | 20.00% | → 30.00%| +10.00% |
| ZEREBRO | 30.00% | → 20.00%| -10.00% |
| RENDER | 25.00% | → 0.00%| -25.00% |
| ORCA | 20.00% | → 15.00%| -5.00% |
| PUMP | 5.00% | → 20.00%| +15.00% |
| BONK | 0.00% | → 15.00%| +15.00% |
Confirm to proceed with Stage 1 now?
ステージ 1 の完了後(リバランスが 200 を返した後)、クールダウン終了時刻を記録し、ルールコードを貼り付けることなく、明確なステータスをユーザーに伝えます。
✅ RIGHT — Stage 1 done, Stage 2 pending:
Stage 1 complete. The vault is now holding the transitional mix shown above.
Stage 2 needs to wait until the rebalance cooldown clears — earliest run is
about <H>h <M>m from now (around <YYYY-MM-DD HH:mm UTC>). Tell me
"continue Stage 2" when you're ready and I'll run it then.
ステージ境界 — ステージ 1 とステージ 2 の間で必ず停止します。
これは、複数ステップの移行フローで最も頻繁に違反されるルールです。ステージ 1 のリバランスが 200 を返した後、エージェントは上記の「ステージ 2 待機中」メッセージでターンを必ず終了しなければなりません。同じターン内で次の操作をしてはいけません。
- ❌ ステージ 2/最終バスケットに対して
/update-assets-txを試みること。クールダウン中はブロックされ、無駄なノイズが生じるだけです。 - ❌ 「どのようにフラグが付くかを確認する」ために、ステージ 2 のバスケットで
/rebalance/checkを呼び出すこと。 - ❌ ステージ 2 の試行結果をユーザーに伝えること。「参考までに、ステージ 2 を試しましたが、 までブロックされています」と伝えることも含みます。最初の計画告知ですでに待機についてユーザーに伝えているため、失敗した試行の経緯を繰り返すのは情報ではなくノイズです。
- ❌ 同じターン内で、クールダウンが解除されるまでループ、再試行、ポーリングすること。ステージ 2 をいつ実行するかは、ユーザーが 「ステージ 2 を続けて」 と伝えて決めます。
ステージ 1 のターンは、リバランスメッセージを投稿した時点で終了します。ステージ 2 は、ユーザーの次のメッセージをきっかけとして、別のターンで実行します。
いずれのステージでも、ユーザー向けの出力で禁止されている内容:
rule9MinTimeBetweenRebalances、rule8MaxPctRebalancedPerTxなど、コードへの言及。- 「クールダウンゲートが作動します」「ポリシーにより 4h の間隔が適用されます」「max_rebalance_pct は 8000 bps です」といった表現。
- エンドポイント名(
/update-assets-tx、/rebalance)。 - HTTP ステータス(
201、400、403)。 - フェーズ番号(「フェーズ 1B」「フェーズ 2A」)— これらはスキル内部のものです。
- 「400 でブロックされました」、「flagged: true」、「レビュー用フラグ」 などの表現。
- 失敗した試行の経緯 — 「ステージ 2/2 回目の更新を試しましたが、……のためブロックされました」とユーザーに伝えてはいけません。エージェントが内部で試みて失敗した場合(クールダウン、ポリシー、その他の理由を問わず)は、内部での復旧対応です。ユーザー向けメッセージでは、上記の明確な「ステージ 1 完了、ステージ 2 待機中」テンプレートをそのまま使用しなければなりません。ユーザーは計画告知ですでに待機について知っているため、失敗した試行の報告は無駄なノイズです。
各 /update-assets-tx 呼び出し前のクールダウン対応の事前確認 — ルールは、クールダウン中は試みることすらしない、です。
エージェントは /update-assets-tx を試みて拒否内容を読むことでクールダウンを「発見」するのではありません。事前にクールダウンを決定論的に計算し、呼び出さないと判断します。
- このセッション中に
/rebalanceまたは/update-assets-txが成功したら、そのタイムスタンプを会話メモリーにlastRebalanceAtとして保存します。 /update-assets-txを実行する前に(Stage 1、Stage 2、または新規の単一ステップ移行のいずれの場合も)、now - lastRebalanceAtを計算します。これがmin_rebalance_interval_hours(/dtf/:symbol/policyから一度だけ読み取り、これも保存します)未満なら、/update-assets-txを呼び出してはいけません。待ち時間を平易な英語でユーザーに伝え、ターンを終了します。- 会話が新しく、保存済みの
lastRebalanceAtがない場合は、最初の/update-assets-txの前にGET /dtf/:vaultSymbol/rebalance/checkを一度だけ呼び出します。policyCheck.flagged: trueで、reviewFlagsにクールダウン関連のフラグがある場合は、クールダウンがまだ有効です。平易な英語に言い換えてユーザーに伝え、ターンを終了します。 - マルチステップ移行で最もよくあるケース:Stage 1 の
/rebalanceが成功したばかりです。エージェントはタイムスタンプを保存しました。同じターン内に Stage 2 を実行すると、この事前チェックで必ず失敗するため、エージェントは試行すらせず、「Stage 1 完了、Stage 2 は保留中」というメッセージに進み、ターンを終了します。これが上記の強制停止ルールの目的です。
ユーザーが「Stage 2 を続けて」と言った場合(次のターン):
- クールダウンの事前チェックをもう一度行います。まだ有効な場合(ユーザーが早く戻ってきた場合)は、残りの待ち時間を伝えてターンを終了します。実行は試みません。
/dtf/:symbol/stateを再取得し、Vault が Stage 1 後の状態になっていることを確認します。想定外の場合は、その旨を伝えて停止します。- Stage 2 のバスケットを使って Phases 1–4 を実行します。WRONG/RIGHT の出力ルールも同様に適用します。
- 両方のステージが完了した後の最終要約は、両方のステージシグネチャーに言及する締めくくりの段落を1つだけにし、ステージごとのデバッグログは含めません。
WRONG と RIGHT — 資産更新操作の要約
実例:ユーザーがエージェントから次のような BAD トレースを受け取ったばかりです。フェーズごとのデバッグログであり、以下の番号付き行や API/HTTP の記述はすべて禁止されています。
❌ WRONG — every fragment below is banned:
Pre-flight (every operation batch)
- Synced/validated env via .claude/settings.json
(DFM_API_URL, DFM_AUTH_TOKEN, DFM_AGENT_KEYPAIR, …)
Phase 0: State + proposal alignment
- Fetched GET /api/v2/agent/dtf/AIPIN/state.
Phase 1A: Metrics/mint resolution
- Called GET /api/v2/agent/market-metrics?symbols=SOL,ZEREBRO,…
Phase 1B: Transitional basket on-chain build+submit
- Target: SOL 20% / ZEREBRO 25% / RENDER 20% / ORCA 20% / PUMP 15%.
- Called POST /api/v2/agent/vaults/AIPIN/update-assets-tx with mintBps.
- On-chain success signature: 47H2Vumu2C…
Phase 1C: Transitional basket DB sync
- First attempt failed: PATCH … → 403 ("Only the vault creator can perform this action").
- After your backend fix, retried: Same PATCH + same body → 200 success.
Phase 2A: Final basket on-chain build attempt
- Called POST /api/v2/agent/vaults/AIPIN/update-assets-tx.
- Blocked with 400: rule9MinTimeBetweenRebalances (4h cooldown not elapsed).
Phase 2B: Rebalance check (diagnostic)
- Called GET /api/v2/agent/dtf/AIPIN/rebalance/check.
- Returned ok: true, flagged: true with:
rule8MaxPctRebalancedPerTx (10000 bps movement vs 8000 max),
rule9MinTimeBetweenRebalances (cooldown remaining).
Phase 2C: Rebalance execution
- Called POST /api/v2/agent/dtf/AIPIN/rebalance with signer pubkey.
- HTTP 201, ok: true. Fees: upfront 0.005 SOL, actual 0.000335 SOL.
上記の問題点(「エンドポイントパス/HTTP コード/ルールコード/JSON の内部情報を出さない」という標準ルール以外):
- フェーズごとのトレースになっています。 ユーザーは、エージェントが Phase 1A、次に 1B、次に 1C を実行したことを知る必要はありません。エージェントが報告するのは手順ではなく、結果です。
- 復旧の内部情報が露出しています(「最初の試行は失敗しました……バックエンドの修正後に再試行しました」)。ユーザーとのチャットは事後検証の場ではありません。
- 手数料の内部情報が示されています(「前払い 0.005 SOL、実際は 0.000335 SOL」)。ユーザーから手数料について質問があった場合にのみ言及します。
- 移行の状態が曖昧なままです —「Step 2 はまだ完了していません」。エージェントは、次のアクションを明確にして終える必要があります。つまり、「クールダウンが HH:mm に解除されると、Stage 2 が自動的に実行されます」 または 「移行を完了するには、『continue Stage 2』と伝えてください」 のいずれかです。ユーザーを迷わせたままにしてはいけません。
同じトレースの RIGHT バージョンは、上記のマルチステップ計画とステージ完了の要約のテンプレートです。デバッグ情報のない明快な表形式のメッセージを2つ提示し、ユーザーの次のアクションを明示するプロンプトで締めくくります。
underlying の更新に関する重大なエラー処理
/update-assets-txがviolations[]を伴って 400 を返す(署名の前):ポリシーゲートに失敗しています。オンチェーンでは何も変更されていません。調整を加えてフェーズ 1 を繰り返します。無料です。/update-assets-txがアセットが見つからない旨のメッセージを伴って 400 を返す(「プラットフォームで次のアセットを利用できません:...」):シンボル/名前を解決できませんでした。asset-allocationにシンボルが存在する別のアセットを選びます(/market-metricsで確認できます)。- オンチェーンでの署名/送信に失敗する:新しいブロックハッシュと新しいポリシーチェックを取得するために、
/update-assets-txを再試行してもかまいません(その間にポリシーが変わっている可能性があります)。同じsignerPublicKeyと同じバスケットを使用します。他に変更は必要ありません。 - オンチェーンに送信された後、フェーズ 3(PATCH)が失敗する:
/update-assets-txを再実行しないでください(オンチェーンでの変更はすでに完了しています。二重に更新しないでください)。同じ body で PATCH を再試行します。PATCH が繰り返し失敗する場合は、オンチェーン署名をユーザーに提示します。チェーンイベントのパイプラインが最終的に DB を自動で同期します。 /update-assets-txが 404「No constitutional policy found」を返す:このボルトはエージェントのフロー外で作成されています(/launch-dtfが実行されていません)。更新はブロックされます。この旨をユーザーに伝えます。エージェント側で修正できることはありません。- フェーズ 3 の成功後にフェーズ 4(リバランス)が失敗する:
/update-assets-txまたは PATCH を再実行しないでください(バスケットの変更はオンチェーンと DB の両方ですでに完了しています)。同じ body でPOST /dtf/:symbol/rebalanceを再試行します。それでも失敗する場合は、バスケットは更新済みですがボルトの保有内容はまだ古いバスケットのままであることをユーザーに伝えます。後で「ボルトをリバランスする」から再試行してもらいます。 - いずれかのフェーズが HTTP 403/「Only the vault creator can perform this action」を返す:エージェントのウォレット(
signerPublicKey)がボルトのcreatorAddressと一致しません。停止してください。再試行せず、別のボルトのシンボルや ID を試さず、一致するものを探すために/vaults/userや/vaults/featured/listを順に調べないでください。 次の内容をユーザーに伝えます:"このボルトは別のウォレットに属しています。更新できるのは作成者だけです。" このターンを終了します。
ステップ 7:資金の流れ — 入金と償還
ユーザーが 「USDC を SOLBC に入金する」、「SOLBC の持分を買う」、「1.5 SOLBC を償還する」、「自分の SOLBC トークンを売る」、または「エージェントのウォレットを使ってボルトに資金を出し入れする」という意味の表現を使った場合は、以下の 2 つのフローのいずれかを実行します。
+--------------------------------------------------------------------+
| CAPITAL FLOWS — agent-bound deposit (2 steps) + redeem (5 steps) |
| |
| DEPOSIT (vault USDC fans out into underlyings): |
| 1. POST /vaults/:symbol/check-min-deposit (MANDATORY GATE) |
| -> abort flow on 400. Do NOT call deposit-tx if the |
| -> threshold is not met. |
| 2. POST /vaults/:symbol/deposit-tx |
| -> base64 unsigned VersionedTransaction |
| 3. agent signs locally + submits on-chain |
| 4. POST /deposit-transaction |
| -> agentSwap (vault USDC -> underlyings via Jupiter) + |
| depositTransaction (parses logs, persists 4 records) |
| |
| REDEEM (queue-serialised; underlyings fan in to vault USDC): |
| 1. POST /check-min-redeem (MANDATORY GATE) |
| -> reads `isValid` (returns 200 either way). Abort flow |
| -> when isValid=false. Do NOT call request-ticket. |
| 2. POST /redeem/request-ticket (queue entry) |
| 3. poll v1 GET /api/v1/tx-event-management/redeem/ticket-status |
| /:ticketId until isReady=true (agent has no clone) |
| 4. POST /redeem/execute/:ticketId (backend swap fan- |
| in: underlyings -> USDC inside the vault) |
| 5. POST /vaults/:symbol/redeem-tx |
| -> base64 unsigned finalizeRedeem tx |
| 6. agent signs locally + submits on-chain |
| 7. POST /redeem-transaction { transactionSignature, ticketId } |
| -> records redeem + auto-confirms ticket (non-fatal) |
| |
| IDENTITY: |
| Deposit-tx takes an explicit `userPublicKey` body field — the |
| depositor wallet may or may not be the agent. Records are |
| attributed via JWT (agentProfile + agentAddress). |
| Redeem-tx is bound to the agent wallet (JWT canonical, body |
| `agentWallet` accepted only as fallback). |
| |
| PRE-FLIGHT GATES ARE MANDATORY: |
| Both flows MUST start with the corresponding check. Skipping |
| them sends underspec'd amounts to the on-chain ix and produces |
| confusing failures (post-fee dust, 0-share mints, etc.). |
+--------------------------------------------------------------------+
実行するフローの選択:
| ユーザーの意図 | フロー | 備考 |
|---|---|---|
| 「 に X USDC を入金する」 | 入金 | ユーザーが別の入金元ウォレットを明示しない限り、デフォルトで userPublicKey にエージェントのウォレットを設定します。 |
| 「 から X トークンを償還する」/「自分の を売る」 | 償還 | 常にエージェントのウォレット(JWT)を使用します。エージェントは、agentProfile に紐づくポジションをすでに保有している必要があります。 |
| 「 に流動性を追加する」/「 を追加購入する」 | 入金 | 入金と同じです。 |
| 「 から出金する」/「 を現金化する」 | 償還 | 償還と同じです。 |
実行前の確認(両方のフロー): build-tx リクエストを送る前に、ボルト、金額、想定手数料を含む要約を 1 行でユーザーに提示します。以下のフローでは、サブステップの間に追加の確認は求めません。
最低金額の必須チェック: 入金を実行する場合は必ず POST /vaults/:symbol/check-min-deposit から開始し、償還を実行する場合は必ず POST /check-min-redeem から開始します。これらは省略できません。後続のフローを進めるためのゲートです。
- 提案された金額がしきい値を下回る場合、
check-min-depositは400(Minimum deposit should be at least $<n> USDC)を返します。400 の場合は/deposit-txに進まないでください。しきい値をそのままユーザーに伝え、より大きな金額を入力してもらってから、再度チェックします。 - 提案された金額がしきい値を下回る場合、
check-min-redeemはisValid: falseを含む200を返します(例外は発生しません)。isValidを確認し、falseの場合は中止してください。チェックに失敗した場合は/redeem/request-ticketを呼び出さないでください。 ゲートが必須である理由:ゲートを省略すると、少なすぎる金額が通過し、オンチェーンで分かりにくい失敗(手数料差し引き後のダスト、ゼロシェアのミント、そしてリディームの場合は Jupiter が拒否する端数スワップ)が発生します。バリデーターのエンドポイントで問題を検知すれば、オンチェーンやキューへの処理に入る前に、ユーザーへ明確で対処可能なエラーを返せます。
フェーズの順序に関する契約(最初に読む — このスキルで最も頻繁に違反される不変条件):
両方のフローのすべてのフェーズは、厳密に順番どおりに実行する必要があります。エージェントは、次のフェーズの呼び出しを行う前に、必ず前のフェーズの API レスポンスを await し、ステータスを確認する必要があります。以下のスクリプトは単一の非同期チェーンとして記述されているため、これは自動的に守られます。ただし、エージェントがフェーズを別々の node -e 呼び出しとして実行したり、並列実行したりすると、フローは気づかないうちに破損します。
| リスク | 発生する問題 |
|---|---|
オンチェーンの送信が確定した後に /deposit-transaction を省略する | 入金トランザクションはオンチェーンに反映されます(シェアは入金者にミントされます)が、agentProfile に紐づく UserVaultPosition は記録されません。/redeem-transaction が No position found for agent in this vault を返すため、ユーザーは後でエージェントのフローからリディームできません。 |
/deposit-transaction が 200 を返す前に、ユーザーへ入金成功を伝える | 上記と同じです。オンチェーン側の処理は完了していますが、DB 側は完了していません。ユーザーはシェアを保有していると思っていますが、エージェントは後の P&L 内訳で入金を確認できません。 |
isReady=true になる前に /redeem/execute/:ticketId を呼び出す | バックエンドは Failed to process ticket で拒否し、チケットを自動的にキャンセルします。ユーザーはリディームフロー全体を最初からやり直す必要があります。 |
/redeem/execute のスワップ集約が完了する前に /redeem-tx を構築する | ボールトに決済に必要な USDC が不足するため、finalizeRedeem はオンチェーンで失敗します。署名済みの価格は期限切れとなり、エージェントは最新の価格を得るために /redeem-tx を再度呼び出す必要があります。 |
オンチェーンの finalizeRedeem が確定する前に /redeem-transaction を呼び出す | バックエンドはトランザクションを取得できず(Transaction not found)、リディームはオンチェーンで完了しても DB レコードは作成されません。confirmTransaction の解決後に再度呼び出してください。 |
成功の判定条件は、各スクリプトが出力する最後の *_OK JSON 行です(DEPOSIT_OK { ... } または REDEEM_OK { ... })。それ以前の PHASE_N_OK ログは途中経過にすぎません。stdout に最後の OK 行が表示されるまでは、ユーザーに成功を伝えないでください。スクリプトが最後の OK 行を出力せずに終了した場合(または MIN_*_FAIL、BUILD_ERROR、EXECUTE_ERROR、RECORD_ERROR、FATAL とともに終了した場合)は、記載されている失敗メッセージのテンプレートを提示して終了してください。
ユーティリティ:オンチェーンのシェア残高を読み取る
「<vault> シェアをいくつ保有していますか?」/「自分のポジションはいくらですか?」/「<vault> をすべてリディーム」する前の確認では、GET /vaults/:symbol/shares を呼び出してください。ウォレットは JWT から読み取られます。別のウォレットについてユーザーから明示的な依頼がない限り、?wallet= は渡さないでください。このエンドポイントはオンチェーンの ATA 残高を返します(DB の読み取りは行いません)。
node -e '
const http = require("http");
const https = require("https");
const url = new URL(process.env.DFM_API_URL + "/api/v2/agent/vaults/" + process.argv[1] + "/shares");
const client = url.protocol === "https:" ? https : http;
const req = client.get(url, {
headers: { "Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN }
}, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", () => {
let parsed = JSON.parse(data || "{}");
// Unwrap the global ResponseMiddleware envelope
if (parsed && parsed.status && parsed.data) parsed = parsed.data;
if (res.statusCode !== 200) {
console.log("BALANCE_ERROR " + res.statusCode + ": " + JSON.stringify(parsed));
return;
}
if (!parsed.exists) {
console.log("BALANCE_OK exists=false sharesUi=0 — wallet has never held shares for this vault");
return;
}
console.log("BALANCE_OK " + JSON.stringify({
vaultSymbol: parsed.vaultSymbol,
sharesRaw: parsed.sharesRaw,
sharesUi: parsed.sharesUi,
ata: parsed.ata,
}));
});
});
req.on("error", (e) => console.log("ERROR: " + e.message));
' <vaultSymbol>
ユースケース:
- 「SOLBC シェアをいくつ保有していますか?」 — 1 回呼び出し、
sharesUiとvaultSymbolを提示します。exists: falseの場合は、「現在、SOLBC シェアを保有していません。」 と伝えます。 - 「SOLBC をすべてリディームして」 — まずこれを呼び出して
sharesUiを読み取り、その正確な数値をリディームフローの/redeem/request-ticketにvaultTokenAmountとして渡します。エージェントがキャッシュしたUserVaultPositionから読み取らないでください。これは DB レコードであり、オンチェーンの実際の状態より遅れている場合があります(たとえば、前回のリディームが正しく記録されていない場合)。オンチェーンの ATA が信頼できる情報源です。 - リディーム前の簡易確認 —
sharesUiをユーザーに提示し、続行前に確認します。
7a. 入金フロー(事前確認後の 2 つのサブステップ)
ステップ 7a-0 — 必須の事前確認:/check-min-deposit
入金トランザクションを構築する前に、必ず { minDeposit: <UI USDC> } を指定して POST {DFM_API_URL}/api/v2/agent/vaults/:symbol/check-min-deposit を呼び出してください。400 が返された場合は、処理を続行せず、しきい値のメッセージをユーザーに表示して、より大きな金額を入力するよう求めてください。以下のステップ 7a-1 の統合スクリプトでは、このチェックが内部で実行され、失敗すると処理が中断されます。
ステップ 7a-1 — エンドツーエンドの入金(単一スクリプト、4 つの逐次フェーズ)
以下のスクリプトは、4 つの入金フェーズすべてを 1 つの非同期チェーンで実行します。各フェーズは、開始前に前のフェーズの API 応答を待機します。
| フェーズ | アクション | 標準出力のインジケーター |
|---|---|---|
| 1 | POST /vaults/:symbol/check-min-deposit(必須ゲート) | PHASE_1_OK gate=passed |
| 2 | POST /vaults/:symbol/deposit-tx(未署名トランザクションを構築) | PHASE_2_OK vaultIndex=<n> |
| 3 | DFM_AGENT_KEYPAIR を使用してローカルで署名し、sendRawTransaction と confirmTransaction を実行 | PHASE_3_OK onChainSig=<sig> |
| 4 | POST /deposit-transaction(スワップの一括実行 + DB レコードの永続化 — 冪等ではありません。呼び出すたびに新しいオンチェーンスワップが再実行されます) | DEPOSIT_OK { … }(最終成功 — JSON 行) |
これらのフェーズを個別の node -e 呼び出しとして実行しないでください。各フェーズの完了前にエージェントが誤って先へ進まないよう、チェーン全体を 1 つのスクリプト内にまとめてください。標準出力に最終的な DEPOSIT_OK { … } の JSON 行が現れるまで、ユーザーに成功を伝えないでください。PHASE_3_OK(オンチェーンで確認済み)だけでは不十分です。DEPOSIT_OK がなければ、agentProfile をキーとする UserVaultPosition は書き込まれておらず、後でユーザーが償還しようとすると No position found for agent in this vault で失敗します。
同様に重要です。フェーズ 4 が失敗しても再試行しないでください。 フェーズ 4(POST /deposit-transaction)は、単なる記録処理ではありません。DB レコードの書き込みに加えて、呼び出しのたびに新たなオンチェーンの agentSwap(ボールトの USDC から Jupiter 経由で原資産へ交換)を実行します。フェーズ 4 がタイムアウトした場合、5xx を返した場合、またはネットワーク切断で応答が届かなかった場合、サーバー側でスワップが一部完了している可能性があります。その状態で再度呼び出すと、残っているボールトの USDC で2 回目のスワップが実行され、二重に処理されて NAV が壊れます。以下のスキルのエラーテーブルでは、一時的に見えるゲートウェイのタイムアウトを含め、/deposit-transaction が 200 以外を返した場合の再試行を明示的に禁止しています。フェーズ 4 の失敗時にどうするか判断する前に、そのルールを確認してください。
フェーズ 4 のリクエストボディ — 4 つのフィールドすべてが毎回必須です。 エンドポイントはすべてのフィールドを検証します。一部のフィールドだけを含むボディ(例:transactionSignature のみ)を送信すると、"vaultIndex should not be empty"、"amountInRaw must be a string" などのメッセージとともに 400 が返されます。これらの検証エラーをコントラクトの変更と解釈しないでください。エージェントが不完全なボディを作成したことを意味しており、API に新しいフィールドが必要だということではありません。必須の形式は固定です。
| フィールド | 値の取得元(フェーズ 2 のレスポンス) | 必須? | 型 |
|---|---|---|---|
transactionSignature | フェーズ 3 の onChainSig(sendRawTransaction と confirmTransaction の実行後) | はい | 文字列(base58 署名) |
vaultIndex | build.body.vaultIndex | はい | 整数(u32) |
etfSharePriceRaw | build.body.etfSharePriceRaw | はい | 文字列(raw u64、6 桁の小数) |
amountInRaw | build.body.amountInRaw | はい | 文字列(raw u64、6 桁の小数) |
slippage | ハードコードされた 200(またはユーザーの値を渡す) | いいえ | 整数(50~500 bps) |
以下のスクリプトでは、フェーズ 2 のレスポンスから 4 つのフィールドすべてがすでに設定されています。形状の異なるボディでフェーズ 4 を手動で呼び出すことは絶対に避けてください。特に「記録処理を再試行するために署名だけを渡す」ことは避けてください。これは、クライアントから報告された検証エラーの原因となった失敗パターンです。スクリプトの呼び出し箇所で失敗した場合、ボディを小さくして手動で再試行するのは誤りです。エラーテーブルにある、/deposit-transaction が 200 以外を返した場合のハードストップルールを参照してください。
node -e '
const http = require("http");
const https = require("https");
const { Keypair, VersionedTransaction, Connection } = require("@solana/web3.js");
const bs58 = require("bs58").default || require("bs58");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR));
const symbol = process.argv[1]; // e.g. ALPHA
const depositAmount = Number(process.argv[2]); // UI USDC, e.g. 5
function call(path, method, body) {
return new Promise((resolve, reject) => {
const url = new URL(process.env.DFM_API_URL + path);
const client = url.protocol === "https:" ? https : http;
const payload = body ? JSON.stringify(body) : null;
const headers = { "Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN };
if (payload) {
headers["Content-Type"] = "application/json";
headers["Content-Length"] = Buffer.byteLength(payload);
}
const req = client.request(url, { method, headers }, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", () => {
try {
let parsed = JSON.parse(data || "{}");
// Unwrap the global ResponseMiddleware envelope: { status, message, data }.
// Pagination responses (which carry a top-level `pagination` field) and
// exception responses (statusCode + error) are NOT wrapped; pass through.
if (
parsed && typeof parsed === "object" &&
parsed.status && Object.prototype.hasOwnProperty.call(parsed, "data") &&
!Object.prototype.hasOwnProperty.call(parsed, "pagination") &&
!Object.prototype.hasOwnProperty.call(parsed, "error")
) {
parsed = parsed.data;
}
resolve({ status: res.statusCode, body: parsed });
} catch { resolve({ status: res.statusCode, body: data }); }
});
});
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
(async () => {
// PHASE 1 — Mandatory gate
const gate = await call(
"/api/v2/agent/vaults/" + symbol + "/check-min-deposit",
"POST",
{ minDeposit: depositAmount }
);
if (gate.status !== 200) {
console.log("MIN_DEPOSIT_FAIL " + gate.status + ": " + (gate.body?.message || JSON.stringify(gate.body)));
return;
}
console.log("PHASE_1_OK gate=passed");
// PHASE 2 — Build the unsigned deposit tx (await response BEFORE signing)
const build = await call(
"/api/v2/agent/vaults/" + symbol + "/deposit-tx",
"POST",
{
userPublicKey: keypair.publicKey.toBase58(), // agent wallet as depositor (default)
depositAmount,
}
);
if (build.status !== 201) {
console.log("BUILD_ERROR " + build.status + ": " + JSON.stringify(build.body));
return;
}
console.log("PHASE_2_OK vaultIndex=" + build.body.vaultIndex);
// PHASE 3 — Sign locally + submit on-chain (await CONFIRMATION before recording)
const conn = new Connection(process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com");
const tx = VersionedTransaction.deserialize(Buffer.from(build.body.transaction, "base64"));
tx.sign([keypair]);
const onChainSig = await conn.sendRawTransaction(tx.serialize(), { preflightCommitment: "confirmed" });
await conn.confirmTransaction(onChainSig, "confirmed");
console.log("PHASE_3_OK onChainSig=" + onChainSig);
// PHASE 4 — Swap + Record (await /deposit-transaction response before declaring success)
//
// ⚠️ THIS ENDPOINT IS NOT IDEMPOTENT AGAINST RETRIES. ⚠️
// Every call dispatches a fresh on-chain swap fan-out. Calling it twice with
// the same transactionSignature triggers a SECOND agentSwap that pulls more
// vault USDC into Jupiter — breaking NAV and stranding USDC in the admin
// wallet. If this phase times out or returns 5xx, see the failure-mode rule
// below: do NOT retry, do NOT re-run the script, do NOT call any deposit
// endpoint again with the same signature. Surface the on-chain signature to
// the user and stop. The chain-event pipeline + operator are the only safe
// recovery paths once Phase 4 is in an unknown state.
//
// Backend runs two server-side sub-operations on every call:
// (a) agentSwap — vault USDC -> underlyings via Jupiter (NOT idempotent)
// (b) depositTransaction — parses VaultDeposited logs, writes
// DepositTransaction + UserVaultPosition (keyed on agentProfile) +
// DepositRecord + History (DB-layer idempotency guard exists)
// Failure modes surfaced INSIDE the 200 response (swap.failedSwapsInfo,
// deposit.events[].vaultDepositUpdateError) are non-fatal and do NOT warrant
// a re-call. Any non-200 response (timeout, 5xx, network error) is a HARD
// STOP — the swap dispatch already happened server-side and the agent
// cannot tell from the client side how much of it completed.
// ALL FOUR fields are required — sending a partial body (e.g. just
// transactionSignature) returns 400 ("vaultIndex should not be empty",
// "amountInRaw must be a string", etc). Wire each from Phase 2's response.
const rec = await call("/api/v2/agent/deposit-transaction", "POST", {
transactionSignature: onChainSig, // from Phase 3 (on-chain submit)
vaultIndex: build.body.vaultIndex, // from Phase 2 (/deposit-tx response)
etfSharePriceRaw: build.body.etfSharePriceRaw, // from Phase 2 (/deposit-tx response)
amountInRaw: build.body.amountInRaw, // from Phase 2 (/deposit-tx response)
slippage: 200, // optional; default 200 bps
});
if (rec.status !== 200) {
// HARD STOP: do NOT retry. See the deposit error table for /deposit-transaction
// non-200 — every call dispatches a fresh agentSwap, retrying breaks NAV.
// Do NOT re-call with a "minimal" body either — the API contract is the
// five fields above, and validation errors here mean the body was wrong,
// not that the contract changed.
console.log("RECORD_ERROR " + rec.status + ": " + JSON.stringify(rec.body));
return;
}
// FINAL — emit consolidated JSON for the user-facing summary + memory.
// Agent must wait for THIS line before reporting deposit success.
const ev = rec.body.deposit?.events?.[0] || {};
const sharePriceDepositUsd = build.body.etfSharePriceRaw
? Number(build.body.etfSharePriceRaw) / 1e6
: null;
console.log("DEPOSIT_OK " + JSON.stringify({
event: ev.eventType,
vaultName: ev.vaultName,
vaultSymbol: ev.vaultSymbol,
grossUsdcRaw: ev.amount, // 6 decimals
entryFeeRaw: ev.entryFee, // 6 decimals
managementFeeRaw: ev.managementFee, // 6 decimals (typically 0 at deposit)
netUsdcRaw: ev.netAmount, // amount that actually became shares
sharesMintedRaw: ev.vaultTokensMinted, // 6 decimals
sharePriceDepositUsd, // share price baked into THIS deposit
onChainSig,
failedSwaps: rec.body.swap?.failedSwapsInfo || null,
recordError: ev.vaultDepositUpdateError || null,
}));
})().catch(e => console.log("FATAL " + (e?.message || String(e))));
' <vaultSymbol> <depositAmount>
timeout: 600000(10 分)を指定して実行します。各フェーズが完了すると 4 つの *_OK マーカーが出力されます。エージェントはこれらを確認しつつ、DEPOSIT_OK { … } が表示された場合に限り成功を報告してください。それ以外の終了行(MIN_DEPOSIT_FAIL、BUILD_ERROR、RECORD_ERROR、FATAL)が表示された場合は、記載されている失敗メッセージのテンプレートを提示して停止してください。
⚠️ 緊急停止 — PHASE_3_OK の後に RECORD_ERROR または FATAL が表示された場合。 Phase 3 のログが出たにもかかわらず、スクリプトが DEPOSIT_OK を出力しなかった場合、オンチェーンの入金は成功しています(入金者にシェアが発行されています)が、Phase 4 のサーバー側の状態は不明です。agentSwap が一部完了した、応答なしで完全に完了した、またはスワップ前にタイムアウトした可能性があります。スクリプトを再実行しないでください。同じ署名で /deposit-transaction を再度呼び出さないでください。修正のために他の入金エンドポイントを呼び出さないでください。 /deposit-transaction を再度呼び出すと、ボールトに残っている USDC に対して 2 回目の agentSwap が実行され、二重取りとなって NAV が毀損します。安全な対応は、オンチェーン署名をユーザーに提示し、次のメッセージを伝えることだけです。「入金はオンチェーンで確定しました。シェアは発行され、ウォレットで確認できます。こちらではプラットフォームのスワップ処理を確認できませんでした。オペレーターが照合しますので、こちらから再試行しないでください。」 その後、ターンを終了してください。このルールは、ハーネスレベルで再試行したくなる場合も優先されます。
入金完了時のメッセージ — 必須形式
償還メッセージのルールと対称です。 入金を要約する際、エージェントはスクリプトの JSON 出力から entryFee と sharePriceDepositUsd を読み取り、透明性をもって提示し、将来の償還時に推測ではなく正確な損益内訳を示せるよう、これらを会話メモリに保存する必要があります。
ユーザーに提示する必須の要約(テンプレート)。 UI 値に換算して置き換えてください。生の u64 値をそのまま貼り付けたり、生値と UI 値を括弧内に併記したりしないでください。
Deposit complete — <vaultName> (<vaultSymbol>).
• USDC deposited (gross): <grossUsdcUi> USDC
• Entry fee: <entryFeeUi> USDC
• USDC that purchased shares: <netUsdcUi> USDC
• Shares minted: <sharesMintedUi> <vaultSymbol>
• Share price at deposit: $<sharePriceDepositUsd>
• On-chain signature: <onChainSig>
<grossUsdcUi> = grossUsdcRaw / 1e6(小数点以下 6 桁で書式設定)、<entryFeeUi> = entryFeeRaw / 1e6(小数点以下 6 桁で書式設定。手数料が 0 の場合は必ず 0.000000 と表示し、0 raw と表示しないでください)とします。他のすべての *Ui プレースホルダーも同様に換算してください。生の u64 値はスクリプトの DEPOSIT_OK { … } JSON に内部利用目的でのみ保持し、チャットメッセージには決して含めないでください。
failedSwaps がある場合は、次の文をそのまま追加してください。「基盤資産のスワップ <failedCount> 件に失敗し、USDC はボールトに返却されました。資金の損失はありません。」 他の失敗に関する説明を作らないでください。
recordError がある場合は、次の文をそのまま追加してください。「オンチェーンでの入金は成功しましたが、データベースへの記録に失敗しました。チェーンイベントのパイプラインがまもなく照合します。シェアはオンチェーンですでに発行されています。」(これは「バックエンドに関する警告を末尾に追加しない」というルールで事前承認された唯一の例外です。オンチェーンでは実際に成功したものの DB への書き込みに失敗し、ユーザーに知らせる必要がある状況でのみ使用します。)このテンプレートを拡張したり、言い換えたりしないでください。上記の正確な文言のみ使用できます。
後で使うために記憶する(会話コンテキスト): { vaultSymbol, grossUsdcRaw, entryFeeRaw, sharesMintedRaw, sharePriceDepositUsd, onChainSig } を保存します。同じユーザーが後で同じボールトから償還する際、償還完了時の要約に入金時の手数料と実際の NAV 変動の計算を含められるようにします。
7b. 償還フロー(事前確認後の 4 つのサブステップ)
償還フローにはさらにステップがあります。(a)ボールトの清算を直列化するためにキューに入れること、(b)ボールトが決済に十分な USDC を保有できるよう、オンチェーンの finalizeRedeem より先にスワップの集約を行う必要があるためです。
Step 7b-0 — 必須の事前確認:/check-min-redeem
チケットをリクエストする前に、必ず { minRedeem: <UI vault tokens> } を指定して POST {DFM_API_URL}/api/v2/agent/check-min-redeem を呼び出してください。check-min-deposit とは異なり、このエンドポイントは常に { isValid, message } とともに 200 を返します。HTTP ステータスではなく isValid を確認してください。isValid: false の場合は、/redeem/request-ticket を呼び出さないでください。しきい値をユーザーに伝え、処理を停止してください。以下の Step 7b-5 の一連のスクリプトでは、このチェックを内部で実行し、失敗時には中断します。
Step 7b-1 — リディームチケットをリクエストする
{ vaultSymbol, vaultTokenAmount, etfSharePriceRaw?, slippage? } を指定して POST {DFM_API_URL}/api/v2/agent/redeem/request-ticket を呼び出します。vaultTokenAmount は文字列形式の raw u64 です(小数点以下 6 桁。まず UI トークン数に 1e6 を掛けます)。etfSharePriceRaw は省略可能です。省略した場合、バックエンドがオンチェーンから読み取ります。
返される値:{ ticketId, position, estimatedWaitSeconds, isReady }。
Step 7b-2 — 準備完了までポーリングする(v1 エンドポイントを使用)
エージェントモジュールには /ticket-status の同等エンドポイントはありません。同じ Authorization ヘッダーを付けて、3~5 秒ごとに GET {DFM_API_URL}/api/v1/tx-event-management/redeem/ticket-status/:ticketId をポーリングします。status.isReady === true になったら停止してください。チケットは準備完了から約 3 分後に期限切れになるため、時間を無駄にしないでください。
Step 7b-3 — スワップの統合実行
{ slippage? } を指定して POST {DFM_API_URL}/api/v2/agent/redeem/execute/:ticketId を呼び出します。その他のパラメーター(vault、amount、share price)はすべてチケット自体から読み取られます。バックエンドはチケットを PROCESSING に遷移させ、redeemAgentSwapAdmin(vault の原資産 → USDC)を実行し、スワップ署名を返します。
実行に失敗した場合、エラーを再スローする前に、バックエンドがチケットを自動キャンセルします。エージェントが /cancel を呼び出す必要はありません。エラーをユーザーに伝え、処理を停止してください。
Step 7b-4 — 未署名の finalizeRedeem tx を構築する
{ vaultTokenAmount }(UI vault トークン数。例:1.5)を指定して POST {DFM_API_URL}/api/v2/agent/vaults/:symbol/redeem-tx を呼び出します。エージェントウォレットは JWT から読み取られます(body の agentWallet はフォールバックとしてのみ使用されます)。
バックエンドは vault を解決し、PDA を導出して、KMS 署名済みの最新の share price を取得します。条件に応じて 4 つの ATA 作成 ixs(agent の vault-token と USDC、fee-recipient の USDC、vault-admin の USDC)を追加し、Ed25519 verify ix と finalizeRedeem(...) ix を生成します。{ transaction, vaultIndex, etfSharePriceRaw, priceTimestamp, vaultTokenAmountRaw, ... } を返します。
Step 7b-5 — ローカルで署名し、送信して、チケットを記録・自動確認する
返された tx に DFM_AGENT_KEYPAIR で署名し、オンチェーンに送信した後、{ transactionSignature, vaultIndex, etfSharePriceRaw, signatureArray, slippage, ticketId } を指定して POST {DFM_API_URL}/api/v2/agent/redeem-transaction を呼び出します。
ticketId を含めると、記録の永続化後にバックエンドがリディームキューチケットを自動確認します。自動確認は致命的なエラーにはなりません。失敗しても処理は中断されず、レスポンスの ticketConfirm: { ok: false, message } にエラーが示されます。リディームはすでにオンチェーンで実行され、DB に記録されています。
リディームのエンドツーエンド処理(7 つのフェーズを順番に実行する単一スクリプト)。 各フェーズは、次のフェーズに進む前に API レスポンスを待ちます。
| フェーズ | アクション | stdout に出力されるインジケーター |
|---|---|---|
| 1 | POST /check-min-redeem(必須のゲート。ステータスではなく**isValid を確認**) | PHASE_1_OK gate=passed |
| 2 | POST /redeem/request-ticket(キューに登録) | PHASE_2_OK ticketId=<id> ready=<bool> |
| 3 | v1 の GET /redeem/ticket-status/:ticketId を isReady=true になるまでポーリング(最大 5 分) | PHASE_3_OK ticket=ready |
| 4 | POST /redeem/execute/:ticketId(バックエンドでスワップを統合実行) | PHASE_4_OK swaps=<n> |
| 5 | POST /vaults/:symbol/redeem-tx(未署名の finalizeRedeem を構築) | PHASE_5_OK vaultIndex=<n> |
| 6 | DFM_AGENT_KEYPAIR でローカル署名し、sendRawTransaction と confirmTransaction を実行 | PHASE_6_OK onChainSig=<sig> |
| 7 | POST /redeem-transaction(チケットを記録・自動確認) | REDEEM_OK { … }(最終成功を示す JSON 行) |
最終的な REDEEM_OK { … } の行が stdout に表示されるまで、ユーザーに成功を伝えないでください。 PHASE_6_OK(オンチェーンでの確認済み)だけでは不十分です。REDEEM_OK がなければ、ポジションの状態とオンチェーンでの償還が同期しておらず、History の行も書き込まれていません。 |
# End-to-end redeem — single script, seven sequential phases.
node -e '
const http = require("http");
const https = require("https");
const { Keypair, VersionedTransaction, Connection } = require("@solana/web3.js");
const bs58 = require("bs58").default || require("bs58");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR));
const symbol = process.argv[1];
const uiAmount = Number(process.argv[2]); // e.g. 1.5
function call(path, method, body) {
return new Promise((resolve, reject) => {
const url = new URL(process.env.DFM_API_URL + path);
const client = url.protocol === "https:" ? https : http;
const payload = body ? JSON.stringify(body) : null;
const headers = {
"Authorization": "Bearer " + process.env.DFM_AUTH_TOKEN
};
if (payload) {
headers["Content-Type"] = "application/json";
headers["Content-Length"] = Buffer.byteLength(payload);
}
const req = client.request(url, { method, headers }, (res) => {
let data = ""; res.on("data", (c) => data += c);
res.on("end", () => {
try {
let parsed = JSON.parse(data || "{}");
// Unwrap the global ResponseMiddleware envelope: { status, message, data }.
// Pagination responses (which carry a top-level `pagination` field) and
// exception responses (statusCode + error) are NOT wrapped; pass through.
if (
parsed && typeof parsed === "object" &&
parsed.status && Object.prototype.hasOwnProperty.call(parsed, "data") &&
!Object.prototype.hasOwnProperty.call(parsed, "pagination") &&
!Object.prototype.hasOwnProperty.call(parsed, "error")
) {
parsed = parsed.data;
}
resolve({ status: res.statusCode, body: parsed });
} catch { resolve({ status: res.statusCode, body: data }); }
});
});
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
(async () => {
const rawAmount = String(Math.round(uiAmount * 1e6));
// PHASE 1 — Mandatory gate: /check-min-redeem
// Returns 200 with isValid; do NOT trust HTTP status alone — read the boolean.
const gate = await call("/api/v2/agent/check-min-redeem", "POST", { minRedeem: uiAmount });
if (gate.status !== 200 || gate.body?.isValid !== true) {
console.log("MIN_REDEEM_FAIL: " + (gate.body?.message || JSON.stringify(gate.body)));
return;
}
console.log("PHASE_1_OK gate=passed");
// PHASE 2 — Request a redeem ticket (await response BEFORE polling)
const ticket = await call("/api/v2/agent/redeem/request-ticket", "POST", {
vaultSymbol: symbol,
vaultTokenAmount: rawAmount,
slippage: 200
});
if (ticket.status !== 200) { console.log("TICKET_ERROR " + ticket.status + ": " + JSON.stringify(ticket.body)); return; }
const { ticketId } = ticket.body;
console.log("PHASE_2_OK ticketId=" + ticketId + " ready=" + ticket.body.isReady);
// PHASE 3 — Poll v1 ticket-status until ready (max 5 minutes)
// Each poll awaits the response before sleeping for the next attempt.
// Do NOT call /redeem/execute until isReady=true.
const start = Date.now();
while (Date.now() - start < 5 * 60 * 1000) {
if (ticket.body.isReady) break;
await new Promise(r => setTimeout(r, 3000));
const s = await call("/api/v1/tx-event-management/redeem/ticket-status/" + ticketId, "GET");
if (s.body?.isReady) { ticket.body.isReady = true; break; }
if (["EXPIRED", "CANCELLED", "COMPLETED"].includes(s.body?.status)) {
console.log("TICKET_TERMINAL status=" + s.body.status); return;
}
}
if (!ticket.body.isReady) { console.log("TICKET_TIMEOUT"); return; }
console.log("PHASE_3_OK ticket=ready");
// PHASE 4 — Execute the backend swap fan-in (await 200 response BEFORE building tx)
// On failure here, the backend AUTO-CANCELS the ticket — do not call /cancel.
// Response shape (after envelope unwrap):
// { vaultIndex, vaultTokenAmount, swaps: [{ mint, input, sig }],
// vaultUsdcBalance, requiredUsdc, adjustedVaultTokenAmount,
// sharePriceAfter, totalValueUSDCraw, totalValueUSDActual, mode,
// ticketId, ticketStatus, message }
const exec = await call("/api/v2/agent/redeem/execute/" + ticketId, "POST", { slippage: 200 });
if (exec.status !== 200) { console.log("EXECUTE_ERROR " + exec.status + ": " + JSON.stringify(exec.body)); return; }
const swapSigs = (exec.body.swaps || []).map(s => s.sig).filter(Boolean);
console.log("PHASE_4_OK swaps=" + swapSigs.length + " sharePriceAfter=" + exec.body.sharePriceAfter);
// PHASE 5 — Build the unsigned finalizeRedeem tx (await response BEFORE signing)
// CRITICAL: if the backend clamped the amount during fan-in (adjustedVaultTokenAmount
// differs from the request), the on-chain finalizeRedeem ix MUST use the clamped
// amount or it will mismatch the USDC actually paid out and either fail or
// under-redeem. The script overrides uiAmount with the clamped value when present.
const adjustedRaw = exec.body.adjustedVaultTokenAmount;
const finalizeUiAmount =
adjustedRaw && /^\d+$/.test(String(adjustedRaw)) && Number(adjustedRaw) > 0
? Number(adjustedRaw) / 1e6
: uiAmount;
if (finalizeUiAmount !== uiAmount) {
console.log("ADJUSTED amount=" + uiAmount + " -> " + finalizeUiAmount + " (raw=" + adjustedRaw + ")");
}
const build = await call("/api/v2/agent/vaults/" + symbol + "/redeem-tx", "POST", {
vaultTokenAmount: finalizeUiAmount
});
if (build.status !== 201) { console.log("BUILD_ERROR " + build.status + ": " + JSON.stringify(build.body)); return; }
console.log("PHASE_5_OK vaultIndex=" + build.body.vaultIndex);
// PHASE 6 — Sign + submit on-chain (await CONFIRMATION before recording)
const conn = new Connection(process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com");
const tx = VersionedTransaction.deserialize(Buffer.from(build.body.transaction, "base64"));
tx.sign([keypair]);
const onChainSig = await conn.sendRawTransaction(tx.serialize(), { preflightCommitment: "confirmed" });
await conn.confirmTransaction(onChainSig, "confirmed");
console.log("PHASE_6_OK onChainSig=" + onChainSig);
// PHASE 7 — Record + auto-confirm ticket (await /redeem-transaction response)
// Including ticketId triggers the backend to confirmRedeemTicket after the
// record is persisted. Failure of the confirm step is non-fatal and surfaces
// on the response as ticketConfirm.ok=false.
const rec = await call("/api/v2/agent/redeem-transaction", "POST", {
transactionSignature: onChainSig,
vaultIndex: build.body.vaultIndex,
etfSharePriceRaw: build.body.etfSharePriceRaw,
signatureArray: swapSigs,
slippage: 200,
ticketId
});
if (rec.status !== 200) { console.log("RECORD_ERROR " + rec.status + ": " + JSON.stringify(rec.body)); return; }
// FINAL — emit REDEEM_OK { ... } only after /redeem-transaction returns 200.
// The agent MUST wait for THIS line before reporting redeem success to the
// user. PHASE_6_OK (on-chain confirmed) alone is NOT sufficient — without
// PHASE_7 success the records weren't written, and the position state is
// out of sync with the on-chain redeem.
const ev = rec.body.events?.[0] || {};
// Share price preference order: sharePriceAfter from PHASE_4 (post-fan-in,
// most accurate) -> etfSharePriceRaw from PHASE_5 build (pre-submit) -> null.
const sharePriceRedeemUsd = exec.body.sharePriceAfter
? Number(exec.body.sharePriceAfter)
: build.body.etfSharePriceRaw
? Number(build.body.etfSharePriceRaw) / 1e6
: null;
console.log("REDEEM_OK " + JSON.stringify({
event: ev.eventType,
vaultName: ev.vaultName,
vaultSymbol: ev.vaultSymbol,
sharesRedeemedRaw: ev.vaultTokensRedeemed, // 6 decimals — divide by 1e6 for UI
grossUsdcRaw: String(
(parseInt(ev.netStablecoinAmount || "0")
+ parseInt(ev.exitFee || "0")
+ parseInt(ev.managementFee || "0"))
), // pre-fee USDC
netUsdcRaw: ev.netStablecoinAmount, // post-fee USDC
exitFeeRaw: ev.exitFee, // 6 decimals
managementFeeRaw: ev.managementFee, // 6 decimals
totalFeesRaw: ev.totalFees, // 6 decimals
sharePriceRedeemUsd, // post-fan-in share price (USD)
swapFanInUsdcRaw: exec.body.totalValueUSDCraw, // gross USDC produced by /redeem/execute swaps
swapFanInUsdcUi: exec.body.totalValueUSDActual, // same as above, UI-decimal string
onChainSig,
ticketConfirm: rec.body.ticketConfirm,
}));
})().catch(e => console.log("FATAL " + (e?.message || String(e))));
' <vaultSymbol> <uiAmount>
timeout: 600000(10 分)を指定して実行してください。7 つの PHASE_N_OK マーカーは各フェーズが完了するたびに表示されます。REDEEM_OK { … } が表示された場合に限り、成功を報告してください。それ以外の終端行(MIN_REDEEM_FAIL、TICKET_ERROR、TICKET_TERMINAL、TICKET_TIMEOUT、EXECUTE_ERROR、BUILD_ERROR、RECORD_ERROR、FATAL)が表示された場合は、記載された失敗メッセージのテンプレートを提示し、処理を停止してください。ユーザー向けの要約に必要なフィールドについては、以下の「償還完了メッセージ」を参照してください。
償還完了メッセージ — 必須フォーマット
ユーザーが USDC で受け取る金額と、最初に預け入れた金額との差額は、ほとんどの場合、市場損失ではなく手数料です。ボルトでは預け入れ時に入金手数料が、償還時に出金手数料と管理手数料が徴収されます。これらの bps の合計が差額になります。これを「市場のディスカウント」「取得時価格からの損失」「資産価格の下落」と誤って説明するのは、実際に預け入れから償還までの間にシェア価格が変動している場合を除き、誤りです。また、シェア価格が変動している場合でも、保有期間が短ければ、より大きな要因は手数料です。
エージェントが読み取る必要のある情報(スクリプトによってすでに収集され、REDEEM_OK ペイロードに含まれています):
sharesRedeemedRaw(生の値、6 桁の小数)— UI 表示用のシェア数を得るには1e6で割ります。events[].vaultTokensRedeemedから取得されます。netUsdcRaw(生の値、6 桁の小数)— 支払い後の純 USDC 額。出金手数料と管理手数料を差し引いた後の金額です。events[].netStablecoinAmountから取得されます。exitFeeRaw(生の値、6 桁の小数)— 今回の償還で課された出金手数料です。managementFeeRaw(生の値、6 桁の小数)— 今回の償還で課された管理手数料です。totalFeesRaw(生の値、6 桁の小数)— 上記 2 つの合計値です。sharePriceRedeemUsd— 確定後のシェア価格(USD)。スクリプトは、PHASE 5 の未署名トランザクションに含まれる価格よりも、PHASE 4 のsharePriceAfter(ファンイン後の最も正確な価格)を優先します。swapFanInUsdcUi— バックエンドのスワップファンインで生成された UI 表示用の USDC 額(PHASE 4 のtotalValueUSDActual)。ユーザーが実際に受け取るnetUsdcRaw / 1e6の値の裏付けとして使用してください。2 つの値のわずかな差は、出金手数料と管理手数料です。
スクリプトが出力する必須 JSON にはこれらすべてが含まれています。これを直接使用してください。netStablecoinAmount だけからユーザー向けの要約を計算してはいけません。
差額の内訳:
exitFee_usdc = exitFeeRaw / 1e6mgmtFee_usdc = managementFeeRaw / 1e6redeemSideFees_usdc = exitFee_usdc + mgmtFee_usdc(=totalFees / 1e6)- 入金時の入金手数料は以前に課されており、今回の償還に対する返答には含まれません。 エージェントが同じセッションの
/deposit-transaction手順で取得した元の入金のentryFeeを会話メモリに保持している場合は含めてください。そうでなければ、数値を作り上げず、内訳から省略してください。 - NAV の変動は
(sharePriceRedeemUsd − sharePriceDepositUsd) × sharesRedeemedです。エージェントが両方のシェア価格を保持しており、実際に異なる場合に限り、言及してください。預け入れ時のシェア価格がわからない場合、差額を市場変動によるものと説明してはいけません。
必須のユーザー向け要約(テンプレート)。 プレースホルダーは、生の u64 値ではなく、割り算後の UI 表示用の値に置き換えてください。エージェントは表示前に / 1e6 で変換します。生の値をそのまま貼り付けたり、生の値と UI 表示用の値を括弧内で併記したりしてはいけません。
Redeem complete — <vaultSymbol>. <one-line outcome sentence>.
• Shares redeemed: <sharesUi> <vaultSymbol>
• USDC received (net): <netUsdcUi> USDC
• Fees on this redeem: <totalFeesUi> USDC (exit: <exitFeeUi>, management: <managementFeeUi>)
• Share price at redemption: $<sharePriceRedeemUsd>
• On-chain signature: <onChainSig>
ここで <sharesUi> = sharesRedeemedRaw / 1e6(小数点以下 6 桁で整形)、<netUsdcUi> = netUsdcRaw / 1e6(小数点以下 6 桁で整形)、<totalFeesUi> = totalFeesRaw / 1e6(小数点以下 6 桁で整形。手数料が 0 の場合は 0 raw ではなく 0.000000 と表示)などとします。生の u64 値がユーザー向けメッセージに表示されることはありません。スクリプトの REDEEM_OK { … } JSON ペイロード内にのみ含まれ、エージェントが内部で使用します。
エージェントがこのセッションで元の入金記録を保持している場合は、以下を追記してください。
• Compared to your original deposit of <depositGrossUsdc> USDC:
– Entry fee paid at deposit: <entryFeeUsdc> USDC
– Exit + management fees on this redeem: <totalFeesUsdc> USDC
– NAV change: <navChangeUsdc> USDC (positive = vault gained, negative = vault lost)
– Net P&L: <netUsdcRaw / 1e6 − depositGrossUsdc> USDC
禁止する表現(実際の NAV の変動を示す株価比較データがない限り、エージェントは使用してはいけません):
- 「JUP/RAY/ の市場ディスカウントによるわずかな損失」 — 原資産のオンチェーン価格は、リアルタイムではボールトの NAV に反映されません。反映されるのは
sharePriceのみです。 - 「ボールトの開始時価格からの損失」 — レスポンスでは「開始時価格」は追跡されていません。
- 「預け入れたときより資産の価値が下がっている」 — そう述べる前に
sharePriceRedeemUsdを確認し、証明してください。 - 「トラッキングエラー」/「バスケットからのスリッページ」 — オンチェーンの
finalizeRedeemでユーザーにスリッページは発生しません。スリッページが発生するのは、前段階のredeemAgentSwapAdmin(サーバー側、ボールト内部)です。
netUsdcRaw < depositGrossUsdc(ユーザーが預け入れた額より少ない額を受け取った)で、エージェントが預け入れ時の株価を持っていない場合、正しい表現は次のとおりです。
「入金、出金、および管理手数料を差し引いた後、
<netUsdcRaw / 1e6>USDC を受け取りました。手数料の詳細は以下のとおりです。…」
次のような表現ではありません。
「…ボールトの開始時価格から見た JUP と RAY の現在のディスカウントを反映した、わずかな損失です。」
バックエンド内部情報に関する末尾の注記は禁止 — 厳禁
償還完了の概要を表示したら、応答はそこで終了します。次のような行を追加してはいけません。
- 「注:バックエンド…」、「注:API…」、「注:応答…」、「参考までに:」、「念のため:」、または類似の開示を示す表現で始まる行。
- 応答内の内部フィールド名に言及する行:❌
vaultRedeemUpdateError、vaultDepositUpdateError、policyCheck.flagged、events[]、ticketConfirm、updatedVaultDepositId、swap.failedSwapsInfoなど。 - ユーザーが対処できない、処理を妨げない警告を伝える行:❌ 「作成者情報がありません」、「チェーンイベントのパイプラインで最終的に照合されます」、「レビュー対象としてフラグが付けられました」、「冪等性ガードが作動しました」、「バックエンドから処理を妨げない警告が返されました」、「メタデータに関する警告」。
- すでに成功したことについて、ユーザーを安心させる行:❌ 「償還トランザクションとチケット確認はどちらも正常に完了しました」 — 成功の概要はすでに上記に記載されています。後書きで繰り返すと、実際には何も問題がなかったのに、問題が起きかけたかのような印象を与えます。
- UI 表示値の横に括弧付きで生の u64 値を表示する行:❌ 「5.233918(生の値は5233918)」、「生の値は12,851」、「生の値は5,127,615(約5.127615 USDC)」。UI 表示値のみを出してください。生の数量はスキル内部情報です。
- 内部キューチケット ID を表示する行:❌ 「チケット ID:ticket_1777980022200_dtbh7n4ye」。チケットはサーバー側のキュー処理に関する情報です。償還完了後、ユーザーにその ID は不要です。
- DB 同期、データベースへの永続化、またはストレージ層の処理について言及すること — 厳禁。ユーザーにとっての正しい情報源はオンチェーントランザクションです。DB 書き込みはスキル内部の処理にすぎません。禁止表現の例(これらに限りません):❌ 「DB 同期」、❌ 「DB-sync」、❌ 「データベースに同期中」、❌ 「DB に永続化」、❌ 「データベースに永続化しました」、❌ 「データベースのレコードを更新しました」、❌ 「DB レコードを作成しました」、❌ 「メタデータを DB に反映しました」、❌ 「フェーズ3 — DB 同期」、❌ 「バックエンドのレコードを更新しました」、❌ 「オンチェーンの状態をバックエンドに同期しました」、❌ 「underlying-assets-by-mint を永続化しました」、❌ 「ポジションレコードを更新しました」、❌ 「履歴レコードを作成しました」、❌ 「Vault メタデータを永続化しました」。また、DB 同期の役割を示すフェーズ名に言及する行も禁止です(「フェーズ3」、「フェーズ1C:バスケットの移行に伴う DB 同期」 など)。フェーズラベルはスキル内部の目印であり、ユーザー向けの文言ではありません。ユーザーに表示するのはオンチェーンでの確認と結果だけです(例:「バスケットを更新しました。Tx:51hd…WUYR」)。その後の記録処理は表示しません。
ユーザーが直前に受け取った完全な誤った出力:
❌ WRONG — every line below is leaked internals:
Redeemed successfully — all your AIPIN shares are now redeemed.
• Shares redeemed: 5.233918 (5233918 raw) ← raw u64 in parens
• Redeem finalize tx: 51hdtVkW…WUYR
• Ticket ID: ticket_1777980022200_dtbh7n4ye ← queue id leak
• Ticket status: confirmed complete
Redeem result:
• Exit fee: 12,851 raw ← raw u64
• Net USDC received: 5,127,615 raw (about 5.127615 USDC)← raw + UI
Note: backend returned a non-blocking metadata warning in the
event (vaultRedeemUpdateError: Vault ... is missing creator
information), but the redeem transaction and ticket confirmation
both completed successfully. ← strictly forbidden trailing note
同じ応答に対する正しい出力:
✅ RIGHT:
Redeem complete — AIPIN. All your shares have been redeemed.
• Shares redeemed: 5.233918 AIPIN
• USDC received (net): 5.127615 USDC
• Fees on this redeem: 0.012851 USDC (exit: 0.012851, management: 0.000000)
• Share price at redemption: $0.9794
• On-chain signature: 51hdtVkW…WUYR
変更点:
- 生の u64 値は表示しません。
5.233918 (5233918 raw)ではなく5.233918、12,851 rawではなく0.012851 USDCとします。スキルでは1e6で一度割り、UI 表示値のみを提示します。 - 内部チケット ID は表示しません。 ユーザーが「すべてのシェアを償還して」と依頼した場合、USDC を受け取れば十分です。キューチケット ID は不要です。
- 「チケットのステータス:確認済み、完了」の行は表示しません。 これは償還処理パイプライン内の手順です。上記の「償還完了」という見出しにまとめます。
- バックエンド警告の後書きは表示しません。
vaultRedeemUpdateErrorフィールドに値があっても、処理を妨げないため(償還自体は完了しています)、エージェントはこれを無視します。記録処理で実際に失敗した場合、スクリプトはREDEEM_OK { … }ではなく別のエラーコード(RECORD_ERROR)を記録します。その場合、エージェントは成功の概要ではなく、1行の失敗メッセージを表示します。 - 末尾の行は1行だけにし、成功を繰り返しません。 最後の行は
On-chain signatureです。「チケットの確認も完了しました」など、成功を再度述べる表現は付けません。
資金移動に関する重大なエラー処理
| 症状 | 対処方法 |
|---|---|
スクリプトが DEPOSIT_OK { … } を出さずに終了する(入金) | 入金は完全には完了していません。PHASE_3_OK(オンチェーンで確認済み)がログに記録されていても、DB レコード(DepositTransaction、UserVaultPosition、DepositRecord、History)は書き込まれていません。ユーザーに入金成功を報告しないでください。チェーンイベントパイプラインによって最終的に照合される可能性はありますが、エージェントとしては入金は「処理中」です。最後のエラー行をユーザーに表示して停止してください。 |
スクリプトが REDEEM_OK { … } を出さずに終了する(償還) | 償還は完全には完了していません。PHASE_6_OK がログに記録された後、PHASE_7 が失敗した場合、オンチェーンの finalizeRedeem はすでに実行されています(USDC はエージェントのウォレットに払い出し済み)が、RedeemTransaction/UserVaultPosition の減算/RedeemRecord/History は書き込まれておらず、キューのチケットも自動確認されていません。償還成功を報告しないでください。最後のエラー行をユーザーに表示して停止してください。オペレーターは同じ transactionSignature を指定して /redeem-transaction を手動で再呼び出しできます(重複署名ガードにより冪等になります)。 |
/check-min-deposit が 400 Minimum deposit should be at least $<n> USDC を返す | 必須ゲートです。/deposit-tx に進まないでください。 閾値をそのままユーザーに伝え、より大きな金額を指定するよう依頼してから、ゲートを再実行してください。以降の入金呼び出しを行う前に、ゲートを通過する必要があります。 |
/check-min-redeem が 200 { isValid: false, ... } を返す | 必須ゲートです。/redeem/request-ticket に進まないでください。 message をユーザーに伝え、より大きな金額を指定するよう依頼してから、ゲートを再実行してください。注:失敗時もエンドポイントは HTTP 200 を返すため、ステータスコードではなく isValid を確認してください。 |
/deposit-tx が 400 Vault "<symbol>" has no on-chain vaultIndex を返す | ボールトはまだオンチェーンで作成されていません。/launch-dtf は呼び出されましたが、/dtf-create(およびその間のオンチェーン送信)は実行されていません。ユーザーに伝えてください。 |
/deposit-tx が 400 Signed price vaultPubkey ... does not match derived vault PDA を返す | このボールトの KMS 署名者の設定に誤りがあります。再試行しないでください。 ユーザーに伝えてください。これはオペレーター側で修正する必要があります。 |
/deposit-transaction が 「vaultIndex should not be empty」、「amountInRaw must be a string」、「etfSharePriceRaw should not be empty」 などの検証メッセージとともに 400 を返す | リクエスト本文を手動で作成したため、必須フィールドが不足しています。これはコントラクトの変更ではありません。 このエンドポイントは常に transactionSignature + vaultIndex + etfSharePriceRaw + amountInRaw(4 つの必須フィールドと、任意の slippage)を必要としていました。検証エラーを「API に新しいフィールドが必要」と解釈し、試行錯誤でコントラクトを探ろうとしないでください。入金スクリプトを再実行してください(Phase 2 のレスポンスから 4 つすべてのフィールドを設定します)。ただし、PHASE_3_OK が一度もログに記録されていない場合に限ります。PHASE_3_OK がログに記録されている場合(オンチェーンの入金はすでに行われています)に、この 400 が手動の再試行から返されたなら、後述の「PHASE_3_OK 後に 200 以外が返る」ルールと同じハードストップケースとして扱ってください。オンチェーン署名とオペレーターによる照合を求めるメッセージを表示して停止してください。 |
/deposit-transaction が 200 を返し、swap.failedSwapsInfo に値が設定されている | アセットごとの Jupiter スワップに失敗し、バックエンドはすでに USDC をボールトに返しています。入金は記録済みとして扱い、スワップの失敗件数をユーザーに警告してください。/deposit-transaction を再呼び出ししないでください。 入金レコードはすでに存在します。 |
/deposit-transaction が 200 を返し、deposit.events[].vaultDepositUpdateError が含まれている | 入金レコードの書き込みに失敗しましたが、オンチェーンのスワップは成功しています。ユーザーに伝えてください。チェーンイベントパイプラインによって最終的に照合されます。同じ署名で再呼び出ししないでください(冪等性ガードにより既存レコードが返されます)。 |
/deposit-transaction がタイムアウトする、5xx を返す、ゲートウェイエラーやソケットリセットが発生する、または PHASE_3_OK 後に 200 以外のレスポンスを返す | ハードストップです。このエンドポイントは絶対に再試行しないでください。 呼び出すたびに、サーバー側で新しい agentSwap がディスパッチされます。レスポンスが転送中に失われた場合、把握できないままスワップが部分的または完全に完了している可能性があります。同じ署名で再呼び出しすると、残りのボールト内 USDC に対して2 回目のスワップファンアウトが実行され、NAV が崩れ、USDC が管理者ウォレットに取り残されるおそれがあります(SOLCORE の 7 USDC インシデント:7 が入金されたものの、バスケットに入ったのは 1.7 のみで、誤った NAV でシェアが発行されました)。次の文をそのままユーザーに伝えてください:「入金はオンチェーンで行われており、シェアは発行され、ウォレットに表示されています。こちら側ではプラットフォームのスワップ手順を確認できませんでした。オペレーターが照合しますので、こちらから再試行しないでください。」 その後、このターンを終了してください。このルールは、ハーネスレベルの再試行の判断、「一時的なエラーなら再試行する」というパターン、および「レコーダーがタイムアウトしたから、レコーダーを再試行しよう」といった推論より優先されます。Phase 4 は単なるレコーダーではありません。 |
/redeem/execute/:ticketId が例外をスローする | チケットはバックエンドによって自動キャンセルされます。/cancel を呼び出さないでください。エラーをユーザーに伝えて終了してください。 |
チケットのポーリングがタイムアウトする(5 分以内に isReady が一度も true にならない) | v1 の DELETE /api/v1/tx-event-management/redeem/cancel/:ticketId を使って手動でキャンセルし、ユーザーに伝えてください。再試行を希望する場合は、その後 /redeem/request-ticket を新たに呼び出してください。 |
/redeem-tx が(署名/送信後に)400 InvalidPriceSignature を返す | KMS 署名付き価格が、ビルドから送信までの間に期限切れになりました。新しい価格を取得するため /redeem-tx を再呼び出ししてください。古い tx は送信しないでください。 |
/redeem-transaction が 400 No position found for agent in this vault を返す | このボールトにおいて、エージェントの UserVaultPosition は記録されていません。まずエージェントのフローで /deposit を実行する必要があります。エージェントのフロー外で資金が投入されたボールトには、agentProfile に紐付くポジションがありません。 |
/redeem-transaction が 400 Insufficient shares. Have X, trying to redeem Y を返す | オンチェーンの償還はすでに行われていますが、記録済みのポジションのシェア数が要求された数より少なくなっています。状態の不一致です。以前の償還が記録されていない可能性があります。オンチェーン署名をユーザーに伝えて停止してください。 |
/redeem-transaction が 200 を返し、ticketConfirm.ok = false になっている | 自動確認には失敗しましたが、償還は記録されています。必要に応じて、同じ署名を指定して v1 の POST /api/v1/tx-event-management/redeem/confirm/:ticketId を手動で呼び出してください。キューのクリーンアップは整備上の作業にすぎません。 |
どちらかのフローで 403/「owned by another user」が発生する | ハードストップです。前述の「ハードストップ — 所有権エラー」を参照してください。 文面をそのまま伝えて、このターンを終了してください。 |
ポリシー違反への対応
/rebalance/check と /rebalance はどちらも、11 個すべての憲法上のポリシールールに対して完全なポリシー評価を実行します。リバランスにおいて、ポリシーは処理をブロックしません。違反の有無にかかわらず、両方のエンドポイントは常に 200 を返します。ルール違反は、レスポンスの policyCheck に表示されます。
{
"policyCheck": {
"ok": true,
"flagged": true,
"reviewFlags": [
{ "violationCode": "rule5MaxPctPerAsset", "mint": "JUP...", "message": "...", "details": {...} },
{ "violationCode": "rule7MinStablecoinFloor", "message": "...", "details": null }
],
"violations": [ ... ]
},
"suggestion": { ... } // present on /rebalance/check
// upfrontFeeSol, actualFeesSol present on /rebalance
}
各違反は、そのボールトの最新の RebalancingSuggestion に policyReviewFlag として保存されます(オペレーター向けの監査証跡)。
policyCheck.flagged が true の場合:
- 内部で
reviewFlagsを読み取ります。各エントリーにはviolationCode、省略可能なmint、message、省略可能なdetailsが含まれます。これらを使って平易な言葉で要約してください。コードや JSON をユーザーにそのまま貼り付けてはいけません。 - 状況を、処理をブロックしない警告として、平易な英語のみでユーザーに明確に伝えます。
policyCheck、reviewFlags、violationCode、flagged、ok、HTTP ステータスコード、エンドポイント名、ルール番号には決して言及しないでください。 - これを失敗として扱わないでください。リバランスはすでに実行されています(チェックエンドポイントの場合は、提案が引き続き返されます)。
flagged: trueを理由にユーザーのフローをブロックしないでください。 - 同じボールトで違反が繰り返される場合は、今後に向けてボールトのポリシーまたは提案された配分を見直し、調整するようユーザーに勧めてください。
違反コードの翻訳表 — 警告の文言には次の表を使用してください
エージェントは、ユーザーに表示する前に、violationCode の値を平易な英語に必ず翻訳してください。右列の文言のみを使用してください。技術的に聞こえる別の言い回しを作ったり、左列の内容をそのまま貼り付けたりしてはいけません。
violationCode(内部用 — 表示しない) | ユーザーに伝える内容(平易な英語) |
|---|---|
rule1WhitelistBlacklist / assetModeViolation | "One of the proposed assets isn't allowed by this vault's policy." |
rule2MinAmmLiquidity | "<assetSymbol> is below the platform's required liquidity floor for this vault." |
rule3Min24hVolume | "<assetSymbol> is trading below the 24-hour volume threshold this vault requires." |
rule4MinMaxAssetCount | "The proposed basket size doesn't fit this vault's asset-count limits." |
rule5MaxPctPerAsset | "<assetSymbol> would exceed the maximum allocation share this vault permits." |
rule6MinPctPerAssetIfHeld | "<assetSymbol>'s allocation is below the minimum this vault requires for held assets." |
rule7MinStablecoinFloor | "The proposed basket falls below this vault's required stablecoin floor." |
rule8MaxPctRebalancedPerTx | "The proposed change is too large for a single rebalance — this vault caps how much can move at once." |
rule9MinRebalanceInterval | "It's still too soon since the last rebalance — this vault enforces a minimum interval between rebalances." |
rule10MaxRebalancesPerDay / rule10MaxRebalancesPerWeek | "This vault has reached its rebalance frequency limit for this period." |
rule11LaunchBlackout | "This vault is still in its launch blackout period — rebalancing isn't permitted yet."(ユーザーが期間について明示的に尋ねない限り、「残り X 時間」などの詳細は省略してください) |
フラグに mint フィールドがある場合は、ボールトの underlyingAssets[].assetAllocation.symbol からアセットの symbol を取得してください(前回の /dtf/:symbol/state 呼び出しですでに読み込み済みの場合はその値を使用し、そうでなければセッションごとに一度取得してください)。ユーザー向けの文言では <assetSymbol> を使用し、ミントアドレスをそのまま表示してはいけません。
誤った例と正しい例 — 具体例
エージェントは、直前に次のような悪い出力をユーザーに返しました。この出力のすべての行は禁止されています。内部識別子、ルールコード、JSON 風のフィールド名、HTTP ステータス、求められていない診断情報が含まれているためです。
❌ WRONG (every fragment below is banned):
Rebalance check for AIPIN is complete.
Result: the endpoint returned 200, but policy is currently flagged
(policyCheck.ok: true, flagged: true) with 3 violations:
rule3Min24hVolume: HNT 24h volume is about $293,401, below required $300,000
rule8MaxPctRebalancedPerTx: suggested rebalance moves 100%, policy max is 80%
rule11LaunchBlackout: still in 24h launch blackout, about 0.88h remaining at check time
It also produced a suggestion set (4 increases: ZEREBRO, RENDER, ORCA, HNT),
but those policy flags are the blockers right now.
If you want, I can run the same check again after blackout expires to see what remains blocked.
上記の例で禁止されている表現:
- "the endpoint returned 200" — HTTP ステータスには絶対に言及しないでください。
- "policyCheck.ok: true, flagged: true" — 内部フィールド名には絶対に言及しないでください。
- "rule3Min24hVolume", "rule8MaxPctRebalancedPerTx", "rule11LaunchBlackout" — 違反コードを絶対に記載しないでください。
- "about $293,401, below required $300,000" / "moves 100%, policy max is 80%" / "about 0.88h remaining" — ユーザーから明示的に求められない限り、ポリシーの具体的な基準値を絶対に明かさないでください。内部ルールの設計が明らかになります。
- "It also produced a suggestion set" / "those policy flags are the blockers" — レスポンスの二重ペイロード構造(
suggestion+policyCheck)が明らかになります。 - "If you want, I can run the same check again after blackout expires" — ユーザーが求めていない、推測に基づく追加提案です。
同じレスポンスに対する正しい出力は、(a)ボールト名、(b)障害要因のユーザー向けの要約、(c)提案された変更のユーザー向けの説明を含む、簡潔で平易な英語の段落ひとつです。そこで終えてください。追加提案も診断情報も含めないでください。
✅ RIGHT:
Rebalance check for AIPIN finished, but a few items need attention before we can proceed:
• One of the proposed assets is below the volume threshold this vault requires.
• The proposed change is larger than a single rebalance is allowed to move.
• The vault is still in its launch blackout period.
The platform's suggested change is to grow allocations to ZEREBRO, RENDER, ORCA, and HNT.
Once the blackout ends and the basket is adjusted to fit the per-rebalance limit, this can run.
正しい例のポイント:
- 最初にボールトの表示名を示し、続けてステータスを一文で伝えます。
- 表の内容に従って各ルールを平易な英語で箇条書きに要約します。コード、mint、基準値、ルール番号は含めません。
- 提案された配分変更はシンボルだけで示します。
pct_bps、vaultId、内部アクションオブジェクトは含めません。 - そこで終えます。「チェックを再実行する」「何がまだブロックされているかを確認する」といった提案はせず、ユーザーから求められるのを待ちます。
同じ平易な英語の原則は、/rebalance エンドポイントの reviewFlags(実行後の警告)にも適用されます。同じ翻訳表を使い、同じ禁止表現を避け、「リバランスは実行されましたが、いくつかの項目に確認が必要なフラグが付いています:」という形で説明します。
配分変更の提案 — 表として表示
/rebalance/check が suggestion.suggestedActions[] を返す場合は、ポリシーの要約の直後に Markdown 表で表示します。アクションオブジェクトには action("increase" / "decrease")、symbol、currentAllocationBps、suggestedAllocationBps、allocationChangeBps、rebalanceAmountUsd、reason が含まれます。これらをユーザー向けの5列にまとめます。
✅ RIGHT (suggestion table):
The platform suggests these allocation changes:
| Action | Symbol | Current → Target | Δ | USD impact |
| -------- | ------- | ---------------- | -------- | ---------- |
| Increase | ZEREBRO | 25.00% → 35.00% | +10.00% | +$124.05 |
| Increase | RENDER | 22.00% → 30.00% | +8.00% | +$99.24 |
| Increase | ORCA | 15.00% → 20.00% | +5.00% | +$62.03 |
| Increase | HNT | 10.00% → 15.00% | +5.00% | +$62.03 |
提案表の列スキーマ:
| 列 | データ元 | 形式 |
|---|---|---|
| アクション | suggestedActions[].action | 先頭を大文字にする("Increase" / "Decrease" / "Hold") |
| シンボル | suggestedActions[].symbol | そのまま表示。mintAddress は絶対に記載しない |
| 現在 → 目標 | currentAllocationBps / 100 → suggestedAllocationBps / 100 | <n>.<2dp>% → <n>.<2dp>% |
| Δ | allocationChangeBps / 100 | +/-<n>.<2dp>% |
| USD 影響額 | rebalanceAmountUsd | 買いの場合は +$<n>.<2dp>、売りの場合は -$<n>.<2dp> |
mintAddress(列が煩雑になるため。シンボルで十分です)と reason(ポリシー違反の要約ですでに理由を説明しているため。資産ごとの理由を表に入れるのは重複し、内部表現も明らかになります)は表示しないでください。
緊急停止 — 所有権エラー
このルールに例外は一切なく、「失敗時に再試行する」や「自律的に動作する」といった、このスキル内の他のすべての指示より優先されます。
以下の4つの操作エンドポイントには、所有権による制限があります:
POST /api/v2/agent/dtf/:symbol/rebalancePOST /api/v2/agent/dtf/:symbol/distribute-feesPOST /api/v2/agent/vaults/:symbol/update-assets-txPATCH /api/v2/agent/vaults/:symbol/underlying-assets-by-mint
トリガー — 「所有権エラー」に該当するもの
以下の4つのエンドポイントのいずれかから返される最初のエラーレスポンスが、次のいずれかの条件に該当した場合です。
- HTTP
403(メッセージは問いません。「この操作を実行できるのはボールトの作成者のみです」、「別のユーザーが所有しています」、「アクセスが拒否されました」など) - 本文に「別のユーザーが所有しています」を含む HTTP
404
該当する最初のレスポンスを受け取ったら、処理をすべて停止します。「別のシンボル/ID/大文字・小文字で、もう一度試して確認する」ことはありません。最初のレスポンスが判定結果です。
ユーザーへの返答 — 逐語的な形式
トリガーが発生したら、ユーザーにメッセージを1つだけ投稿し、そのターンを終了します。次の形式のみを使用してください。
DFM platform doesn't recognize you as the owner of this vault, so this action can't be performed.
適切な場合に限り、ボールトの表示名のみを含めてもかまいません(シンボル、ID、署名、ウォレットアドレスは含めないでください)。
DFM platform doesn't recognize you as the owner of "<vaultName>", so this action can't be performed.
返答はこれだけです。見出しは付けません。「これが意味すること」や「対処方法」も付けません。「結果:」、「リクエスト:」、「追加確認」などのセクションも付けません。箇条書きも、追加の提案も付けません。
返答本文で禁止する内容(使用禁止の表現)
ユーザー向けメッセージに、以下の内容を一切含めないでください。これらは実際に確認されており、二度と起きてはなりません。
- 単語:
backend、API、endpoint、route、JWT、token、session、signerPublicKey、DFM_AGENT_KEYPAIR、DFM_API_URL、wallet address、keypair、creatorAddress、signature、Mongo、id、index、vault index、on-chain creator - HTTP ステータスコードまたは名称:
403、404、Forbidden、Not Found、Unauthorized - メソッド/パスの断片:
POST /…、GET /…、/dtf/...、/vaults/...、/rebalance、/distribute-fees - JSON:
{ … }形式の内容、またはサーバーからのメッセージの引用 - ルールに言及する自己言及的な表現:❌「エージェントのルールに従い」、「これ以上再試行していません」、「所有権ルールで定められているため…」、「スキルに記載されているとおり」
- 診断/アドバイスのセクション:❌「これが意味すること:」、「対処方法:」、「次にできること:」、「試したこと:」、「ご希望であれば…」、「QA に同じ呼び出しを試してください」、「トークンを更新してください」、「別のキーペアを使用してください」、「ボールトを移転してもらってください」
- さらに作業が続くことを示す告知:❌「確認します…」、「検証します…」、「少々お待ちください…」
トリガー発生後に禁止するツール呼び出し
メッセージを提示した後は、ツールを一切呼び出さないでください。特に、以下は禁止です。
- 同じ
signerPublicKeyで同じ呼び出しを再試行する vaultSymbolの大文字・小文字、ハイフンの有無、別名を変えて再試行する。例:POP-DTF→ 403 →POP、POPDTF、POP_DTF、pop-dtfを試してはなりません。エージェントが「正しいシンボルを試している」と説明しても、2回目の呼び出しは違反です。vaultSymbol/vaultId/vaultIndexの各パスを切り替える- 「正しいボールトを見つける」または「確認する」ために、
GET /vaults/user、GET /vaults/featured/list、GET /dtf/my-vaults、GET /dtf/:symbol/stateを呼び出す - 「挙動を確認する」ために、別のボールト(例:
BARBL)で同じエンドポイントを呼び出す。ユーザーが尋ねたのは1つのボールトです。 - 「追加確認」として、ファイルの読み書き、ドキュメントの取得、その他のツールを使用する
誤りと正しい例 — 具体例
誤り(以下の各行はすべて実際のエージェント出力で確認されており、含めてはなりません):
Here's what happened on the retry:
POST …/dtf/POP-DTF/rebalance — HTTP 403
Message: Vault "POP-DTF" is owned by another user
So the vault is recognized, but the backend does not treat your current caller as its owner.
POST …/dtf/POP/rebalance — HTTP 404 (wrong symbol; stick with POP-DTF.)
The body sent was:
{ "signerPublicKey": "3vsKr…aWfr" }
What this means: Rebalance is only allowed when signerPublicKey matches the vault's
on-chain creator. Right now your DFM_AGENT_KEYPAIR public key does not match.
What to do: Use the same agent wallet that created POP-DTF, with a JWT issued for
that same agent/user. If POP-DTF was created by a different profile or wallet,
that other identity has to run the rebalance—or you need the vault transferred.
I did not retry further after 403, per the agent rules for permission errors.
正しい例(返答全体):
DFM platform doesn't recognize you as the owner of "Popeye Index", so this action can't be performed.
即時停止・再試行なし・診断なしとする理由
所有権は、アクセス権限に関する判定です。一時的なエラーでも、回避策を探すべきルーティング上の問題でも、エージェントがデバッグすべき問題でもありません。別のシンボルやIDで繰り返し試すと、ユーザーがアクセスできない可能性のあるボールトの情報が漏れ、エージェントの動作不良のように見えます。診断の段落や「対処方法」のセクションは、ユーザーが求めていない内部情報を明かし、何も問題がないのにプラットフォームが壊れているかのように伝えてしまいます。プラットフォームは正しく拒否しています。
動作ガイドライン
必ず行うこと:
- 自律的に行動してください。 許可を求めずに、調査、判断、デプロイを行ってください。
- ユーザー向けのメッセージは簡潔で親しみやすくしてください。 「プロフィールを作成しています……」「Vault のトランザクションを構築しています……」「オンチェーンで署名して送信しています……」などと伝えてください。エンドポイント名、HTTP メソッド、ペイロードの形式、技術的な内部情報をユーザーに知らせる必要はありません。
- 完全なペイロードを作成してください。 必須フィールドと、関連する任意フィールドをすべて含めてください。
- 実際のトークンデータを使用してください。 アセットを選ぶ前に、実際の Solana トークンのミントアドレス、流動性、取引量を調査してください。
- ミントアドレスを自動的に解決してください。 選択した各アセットについて、ペイロードを構築する前に Solana のミント参照を取得し、検証してください。
- 妥当なポリシーを設定してください。 戦略に基づいてガードレールを設定してください(保守的な戦略では制限を厳しくし、積極的な戦略では範囲を広げてください)。
- エラーは選択的に処理してください。 ペイロードの変更で修正できるエラー(検証エラー、
violations[]を含むポリシー違反、一時的なネットワーク障害)の場合にのみ再試行してください。権限エラー(403)、対象リソースが見つからないエラー(404 vault not found)、認証エラー(401)の場合は再試行せず、ユーザーに簡潔で親しみやすい一文を表示してターンを終了してください。「行わないこと」リストにあるエラーメッセージのテンプレートを参照してください。 - ローンチ用メディアのフィールドは空にしてください。
launch-dtfとdtf-createでは、metadataUri、logoUrl、bannerUrlを空文字列に設定してください。 - USDC の除外を徹底してください。
launch-dtfを送信する前に、underlyingAssetsにシンボルまたは名前として USDC が含まれていないことを確認してください。 - トランザクションにはローカルで署名してください。 API が未署名のトランザクションを返した場合は、ローカルの鍵ペアで署名し、オンチェーンで送信してください。
- すべての API 呼び出しで長いタイムアウトを設定してください。 API を呼び出す Bash コマンドを実行するときは、必ず
timeout: 600000(10 分)を使用してください。オンチェーン操作には時間がかかることがあります。デフォルトの 2 分のタイムアウトで処理が終了しないようにしてください。 - 構造化された複数行の出力には Markdown テーブルを使用してください。 API レスポンスに一貫したフィールドを持つオブジェクトのリスト(Vault の一覧、Vault のポートフォリオ、リバランスの提案、市場指標、redeem-execute のスワップ内訳など)が含まれる場合、ユーザー向けの出力には、このスキルの該当セクションで定義されている列構成の Markdown テーブルを使用してください。このような場合、箇条書きや段落形式での羅列は禁止です。テーブルの前に短い説明文を添えてもかまいません(「Vault は全部で 13 件あります。」/「ポートフォリオの内訳:」など)。単一行の読み取り(シェア残高の確認、入金・償還の完了概要、単一 Vault の確認)は説明文形式にしてください。テーブルと説明文の使い分けは次のとおりです。
- テーブル: Vault の一覧(
/vaults/user、/vaults/featured/list)、Vault 状態のポートフォリオ(/dtf/:symbol/state→portfolio.assets[])、リバランスの提案(/rebalance/check→suggestion.suggestedActions[])、市場指標(/market-metrics→metrics[])、リバランス履歴の行、ユーザーが「どのスワップが行われたか」と尋ねた場合の redeem-execute のスワップ内訳。 - 説明文: 入金/償還の完了概要、1 つのウォレットと Vault のシェア残高の読み取り、エラーメッセージ、単一 Vault の状態ヘッダー(
vault.*ブロックは 2 列のテーブルとして表示し、その前の文は説明文として扱う)、1 行の確認(「5 USDC を入金しました」「トークンを更新しました」など)。 - どちらでも可: ポリシー違反の説明。フラグが 1~3 件の場合は箇条書き、4 件以上の場合、またはユーザーがしきい値の明示を希望する場合はテーブルを使用してください。
- テーブル: Vault の一覧(
禁止事項:
- HTTP
403/Forbiddenの場合は、絶対に再試行しないでください。 いずれかのエージェントエンドポイント(/rebalance、/distribute-fees、/vaults/:symbol/update-assets-tx、/vaults/:symbol/underlying-assets-by-mint)から 403 が返された場合、呼び出し元のsignerPublicKeyがボールトのcreatorAddressと一致していないことを意味します。ボールトを変更できるのは作成者だけです。直ちに停止してください。 同じ呼び出しを再試行したり、別のvaultSymbol/大文字・小文字表記/インデックスを試したり、代替候補を探すために/vaults/userや/vaults/featured/listを検索したり、「確認」のために/dtf/:symbol/stateを呼び出したりしないでください。ユーザーには「このボールトは別のウォレットに属しています。この操作を行えるのは作成者だけです。」のような一文だけを伝え、そのターンを終了してください。403 は権限の判定であり、一時的なエラーではありません。 - ユーザーに技術的な詳細を絶対に見せないでください。 メッセージで API エンドポイントのパス、HTTP メソッド、リクエスト/レスポンスのペイロード、フィールド名、内部実装に触れないでください。ユーザーに表示するのは、親しみやすい状況説明だけにしてください(例:「プロフィールを作成しています……」。「ウォレットアドレスを指定して POST /profile-launch を呼び出します」ではありません)。
- デバッグログのような失敗報告を絶対に書かないでください。 何かがうまくいかない場合、ユーザーには平易な英語の一文だけを伝えて終了してください。ユーザー向けの出力では、以下はすべて禁止です。
- HTTP ステータスコードまたはステータス名:❌
404、403、Not Found、Forbidden、「エンドポイントは 200 を返しました」、「呼び出しに成功しました」 - エンドポイントのパスまたはメソッド:❌
POST /api/v2/agent/dtf/POP-DTF/rebalance、GET /vaults/user、「DFM アカウントのコンテキスト(/vaults/user、全ページにわたるページネーション)から」 - 生のリクエスト/レスポンス本文、JSON の断片、サーバーからコピーしたエラーメッセージ:❌
"Vault with symbol \"POP-DTF\" not found" - 内部識別子:❌ ボールトの
_id、vaultIndex、署名者の公開鍵、トランザクション署名(確認済みの成功サマリーでのみ署名を出力) Request、Response、Result、Extra check、What you can do nextのようなセクション見出し — これらはチャットではなく、デバッグログに使うものです- レスポンス本文の内部フィールド名:❌ 「
policyCheck.ok: true, flagged: true」、「reviewFlags」、「data配列」、「pagination.totalPages」、「updatedVaultDepositId」。意味を平易な英語に置き換え、フィールド名を引用しないでください。 - ポリシーの
violationCode値をそのまま貼り付けること:❌ 「rule3Min24hVolume」、「rule8MaxPctRebalancedPerTx」、「rule11LaunchBlackout」。各コードを平易な英語で表現するには、「Policy Violation Handling」の翻訳表を使用してください。 - ユーザーが明示的に尋ねていないポリシーの具体的な閾値:❌ 「約 $293,401 で、必要な $300,000 を下回っています」、「100% 移動します。ポリシーの上限は 80% です」、「残り約 0.88 時間です」。代わりに「必要な閾値を下回っています」または「許容範囲を超えています」と伝えてください。
- レスポンスペイロードの構造がわかる表現:❌ 「提案セットも生成されました」、「これらのポリシーフラグが阻害要因です」、「レスポンスには 4 件の増額が含まれています」。構造ではなく意味を伝えてください(「プラットフォームは X、Y、Z の増額を提案しています」)。
- ユーザーが求めていない設定変更の提案:❌「
DFM_API_URLを QA に切り替える」、「JWT を更新する」、「別のキーペアを読み込む」、「バックエンドのシンボルパーサーを修正する」 - 成功した応答の最後に、求められていない追加の提案をしないでください。 ❌ 「必要であれば、ブラックアウト期間の終了後に同じ確認を再実行できます」、「必要であれば、TVL/シェア価格順で取得することもできます」、「必要であれば、完全な JSON ペイロードも取得して返せます」。回答の後に続けず、ユーザーの次のメッセージを待ってください。
- 失敗後に「動作を確認する」ため、他のボールト/エンドポイントを試すこと — ユーザーのボールトに対する
/rebalanceが失敗した場合、比較のために別のボールトで/rebalanceを呼び出してはいけません。ユーザーが尋ねたのは 1 つのボールトについてです。そのボールトについて回答し、停止してください。
- HTTP ステータスコードまたはステータス名:❌
- 失敗メッセージのテンプレート。 エラーが発生した場合は、専門用語を使わず、親しみやすい一文だけを伝えて、そのターンを終了してください。例:
- 403 の所有権エラー(所有権確認が必要な 4 つのエンドポイント):HARD STOP — Ownership errors セクションにある文言をそのまま使用してください:「DFM platform doesn't recognize you as the owner of this vault, so this action can't be performed.」(またはボールトの表示名を挿入してください)。所有権エラーには、他の表現は認められません。
- 404 のボールト未検出:「その名前のボールトが見つかりませんでした。」
- 401/トークンの問題:「セッションの有効期限が切れました。再認証してからもう一度お試しください。」
- 一時的なエラー/ネットワークエラー:「プラットフォームへの接続中に問題が発生しました。少ししてからもう一度お試しください。」
- ポリシー違反(
violations[]がある場合):原因を平易な英語で要約してください(例:「続行できませんでした。1 つのアセットがプラットフォームの流動性基準を下回っています。」)。違反コードをそのまま貼り付けないでください。
- ターミナルで機密性の高い環境変数の値を絶対に出力、復唱、記録、表示しないでください。 これには
DFM_AUTH_TOKEN、DFM_AGENT_KEYPAIR、およびすべての秘密鍵/プライベートキーが含まれます。表示してよいのは公開鍵だけです。シークレットは標準出力に出さず、ファイルへの追記によって直接ファイル(~/.zshrc)に書き込んでください。 - ウェブ調査を省略しないでください。 DTF 関連のメタデータ(トークンデータ、価格、ミントアドレス、市場状況)はすべて、必ず
WebSearchとWebFetchを使用して調査してください。例外はありません。 - デプロイ前に人間の確認を求めないでください。 ポリシーエンジンが検証を行います。そのまま実行してください。
- プレースホルダー値を使わないでください。 実際のトークンアドレスと現実的な配分を調査してください。
- ミントアドレスの参照情報に食い違いがある場合、単一の情報源を信用しないでください。
- リバランスや手数料の分配で承認を待たないでください。 リバランスのポリシーチェックは非ブロッキングです。
policyCheck.flaggedが true でも続行してください。 - ローンチのペイロードに USDC を含めないでください。
underlyingAssetsにUSDC/USD Coinを絶対に含めないでください。 - バックエンドにシークレットキーを送らないでください。 送信するのは公開鍵だけです。署名はローカルで行います。
- シェア価格による裏付けなしに、入金/償還の差額を「市場損失」や「アセットのディスカウント」のせいにしないでください。 USDC の入金額と償還額の差は、市場変動ではなく、通常は入金手数料+出金手数料+管理手数料によるものです。
/redeem-transactionのレスポンスからevents[].exitFeeとevents[].managementFeeを読み取り、会話の記憶に元の入金のentryFeeがあれば、それも確認して内訳を手数料として伝えてください。NAV の変動に触れてよいのは、sharePriceDepositUsdとsharePriceRedeemUsdの両方を実際に確認でき、かつ値が異なる場合だけです。禁止されている表現(シェア価格の比較で裏付けられている場合を除く):「<asset>のディスカウント」、「開始時の価格からの損失」、「アセットの価値が下がっています」、「トラッキングエラー」、「バスケットでのスリッページ」。正確なテンプレートは「Step 7 → Redeem completion messaging」を参照してください。 - 最終的な OK 行が出る前に、入金/償還の成功を伝えないでください。 各資金フローのスクリプトは、各フェーズの完了時に
PHASE_N_OKマーカーを出力し、最後にDEPOSIT_OK { … }(またはREDEEM_OK { … })という JSON 行を 1 行だけ出力します。ユーザーに成功を伝えるのは、標準出力にこの最終 OK 行が表示された後だけです。中間のPHASE_N_OK(特に、入金のPHASE_3_OK/償還のPHASE_6_OK。どちらもオンチェーンでの確認を示します)を成功とみなすと、ユーザーを誤解させます。DB レコードがまだ書き込まれていないため、そのボールトに対する今後のエージェントフロー操作はNo position found for agent in this vaultで失敗します。スクリプトが最終 OK 行を出力せずに終了した場合、操作は「実行中」です。最後のエラー行を伝えて停止し、オペレーターの確認なしにフロー全体を再試行しないでください。 - 一覧エンドポイントのレスポンス構造を読み違えないでください。
/vaults/featured/listと/vaults/userにはトップレベルのpaginationフィールドがあるため、グローバルなResponseMiddlewareは{ status, message, data }エンベロープなしでレスポンスをそのまま渡します。ボールトの配列はbody.dataにあるフラットな配列です。body.data.vaults、body.data.data、body.vaults、body.resultsはありません。これらを読み取るとundefinedが返り、ユーザーが多数のボールトを所有していても「ボールトが見つかりません」と誤って報告することになります。旧式の/dtf/my-vaultsだけは{ vaults: [...], total }でラップされています。ただし、このエンドポイントを呼び出すべきではありません(非推奨です。「自分のボールト」に関するすべての表現は/vaults/userにルーティングしてください)。迷った場合は、まずbody.data.lengthをログに出して構造を確認してください。 - 成功した応答にバックエンド内部の補足を追加しないでください。 成功サマリー(入金完了、償還完了、バスケット更新完了など)を表示したら、応答を終了してください。すべてのフローで、以下は特に禁止されています。「Note: backend …」、「Note: the API …」、「FYI:」、「Heads up:」 で始まる行。
vaultRedeemUpdateError、vaultDepositUpdateError、policyCheck.flagged、swap.failedSwapsInfo、ticketConfirm、updatedVaultDepositIdのような内部フィールド名への言及。ユーザーが対処できない非ブロッキング警告の開示(「作成者情報がありません」、「チェーンイベントの処理が後から整合させます」、「メタデータに関する警告」、「レビュー対象としてフラグが付けられています」)。何か問題が起きかけたかのように成功を言い換える表現(「……に加え、チケット確認も完了しました」、「両方のトランザクションを確認しました」)。UI 表示値の横にかっこ書きで生の u64 値を記載すること(「5.233918(5233918 raw)」、「12,851 raw」)。内部キューのチケット ID(「チケット ID:ticket_…」)。バックエンドの警告がレスポンスペイロードに表示されていても、エージェントはそれを無視してください。成功を示すのはスクリプトの最終*_OK行であり、ネストされたエラーフィールドが一切ないことではありません。内部の警告はログに記録し、チャットには表示しないでください。
セットアップガイド
前提条件
- Node.js v18+
- @solana/web3.js がインストールされていること(
npm install @solana/web3.js) - bs58 がインストールされていること(
npm install bs58) - AI ランタイム:Claude Code、Codex、OpenClaw、または互換性のあるアシスタント
ステップ 1 -- DFM Dashboard に登録する
- DFM Dashboard(https://qa.dfm.finance)にアクセスし、Solana ウォレット(Phantom、Backpack など)を接続します。
- ウォレットアドレスが登録されました。メモしておいてください。エージェントのセットアップで必要になります。
ステップ 2 -- 基本の環境変数を設定する
# -- DFM Agent Configuration -------------------------------------------
# API base URL (REQUIRED — no fallback. Agent refuses to run if unset.)
export DFM_API_URL="https://api.dfm.finance"
# Path where the Agent Wallet keypair is stored locally
export AGENT_WALLET_PATH="$HOME/.dfm/agent-wallet.json"
# Solana RPC URL for on-chain transaction submission
export SOLANA_RPC_URL="https://api.mainnet-beta.solana.com"
注:
DFM_AUTH_TOKENとDFM_AGENT_KEYPAIRは、エージェントの初回使用時に自動で設定されます。手動で設定する必要はありません。
ステップ 3 -- スキルをインストールする
npx skills add DFM-Finance/DFM-AgentSkills
Claude Code の場合は、正しいパスにもコピーしてください。
mkdir -p .claude/skills
cp -r .agents/skills/dfm-agent .claude/skills/dfm-agent
ステップ 4 -- 権限を許可する(初回のみ)
最初のコマンド実行時に、Claude Code がスクリプトの実行許可を求めます。繰り返しプロンプトを表示せずにすべてのエージェント操作を許可するには、"Yes, and don't ask again for: node:*" を選択してください。
ステップ 5 -- 初回使用(自動セットアップ)
スキルを初めて使用すると、エージェントは次の処理を自動で行います。
- ウォレットアドレスを尋ねる — DFM Dashboard に登録したアドレスです
- エージェントプロフィールを作成する —
POST /profile-launchを介して、名前とユーザー名を自動生成します - 認証トークンを保存する — 返された JWT を
.claude/settings.jsonと~/.zshrcに書き込みます(表示はされません) - エージェントウォレットを生成する — Solana キーペアを作成して
AGENT_WALLET_PATHに保存し、DFM_AGENT_KEYPAIRを~/.zshrcに書き込みます - 公開鍵のみを報告する — 秘密鍵やトークンは表示しません
この初回セットアップ後、Claude Code を再起動すれば使用を開始できます。
セキュリティ: 秘密鍵や認証トークンがターミナル出力に表示されることは決してありません。制限付きの権限を設定したファイルに直接書き込まれます。
認証
すべてのエンドポイントは {DFM_API_URL}/api/v2/agent/... にあり、Authorization: Bearer <DFM_AUTH_TOKEN> が必要です。
[Public] と記されたエンドポイントは JWT 認証を回避します。これには、最初にエージェントプロフィールを作成してトークンを取得するために使用する profile-launch も含まれます。
オンチェーン操作(launch-dtf、distribute-fees)は署名なしトランザクションを返し、エージェントは DFM_AGENT_KEYPAIR のキーペアを使ってローカルで署名します。リバランスはサーバー側で管理者ウォレットによって実行されます。
認証トークンは、初回使用時に POST /profile-launch を介して自動的に取得されます。ユーザーが提供する必要があるのは、DFM に登録したウォレットアドレスのみです。
エージェントウォレット -- キーペアの生成
ウォレットパスの解決
AGENT_WALLET_PATH環境変数SOLANA_KEYPAIR_PATH環境変数WALLET_OUTPUT_PATH環境変数- デフォルト:
<project-root>/solana-keypair/keypair.json
実装
import { Keypair } from "@solana/web3.js";
const bs58 = require("bs58").default || require("bs58");
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
// 1. Resolve output path
const outPath = path.resolve(
process.env.AGENT_WALLET_PATH ??
process.env.SOLANA_KEYPAIR_PATH ??
process.env.WALLET_OUTPUT_PATH ??
path.join(os.homedir(), ".dfm", "agent-wallet.json")
);
// 2. Generate keypair and save to file
fs.mkdirSync(path.dirname(outPath), { recursive: true });
const keypair = Keypair.generate();
fs.writeFileSync(outPath, JSON.stringify(Array.from(keypair.secretKey)), { mode: 0o600 });
// 3. Write base58 secret key directly to shell profile (NEVER print to terminal)
const base58Secret = bs58.encode(keypair.secretKey);
const shellProfile = path.join(os.homedir(), ".zshrc");
fs.appendFileSync(shellProfile, `\nexport DFM_AGENT_KEYPAIR="${base58Secret}"\n`);
// 4. Only output the PUBLIC KEY
const pubkey = keypair.publicKey.toBase58();
console.log(`PUBLIC_KEY=${pubkey}`);
console.log(`WALLET_PATH=${outPath}`);
// NEVER console.log the base58Secret — it was written to ~/.zshrc silently
重大な注意: base58Secret は fs.appendFileSync を介して ~/.zshrc に直接書き込まれます。標準出力への出力、ログへの記録、ターミナル出力への表示は絶対に行わないでください。ユーザーに表示するのは公開鍵のみです。
環境変数から公開鍵を導出する(API 呼び出し用)
import { Keypair } from "@solana/web3.js";
const bs58 = require("bs58").default || require("bs58");
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.DFM_AGENT_KEYPAIR!));
const signerPublicKey = keypair.publicKey.toBase58();
// Use signerPublicKey in API request bodies
API クイックリファレンス
リクエスト/レスポンスのスキーマ全体については、
references/api-reference.mdを参照してください。
| アクション | メソッド | エンドポイント | 認証 | ボディ |
|---|---|---|---|---|
| エージェントプロファイルを起動 | POST | /profile-launch | なし | userPublicKey + agentWalletAddress + name/username |
| 一括市場指標 | GET | /market-metrics?mints=...&symbols=...&names=... | JWT | クエリパラメーター |
| ポリシーのドライラン | POST | /policy/dry-run | JWT | underlyingAssets + policy |
| ボールトの tx を構築(ポリシーによる制限あり) | POST | /launch-dtf | JWT | signerPublicKey + ボールト設定 + policy |
| DTF を確定(メタデータのみ) | POST | /dtf-create | JWT | transactionSignature + ボールトのメタデータ |
| 注目ボールト(ページネーション対応) | GET | /vaults/featured/list?page=1&limit=10&vaultType=dtf&includeTvl=true | JWT | クエリパラメーター |
| ユーザーボールト(ページネーション対応) | GET | /vaults/user?page=1&limit=10&vaultType=dtf&includeTvl=true | JWT | クエリパラメーター |
/vaults/user を使用してください。 | GET | /dtf/my-vaults | JWT | - |
| ボールトの状態 | GET | /dtf/:symbol/state | JWT | - |
| ボールトのポリシー | GET | /dtf/:symbol/policy | JWT | - |
| リバランスの確認 | GET | /dtf/:symbol/rebalance/check | JWT | - |
| リバランス | POST | /dtf/:symbol/rebalance | JWT | signerPublicKey |
| update-assets tx を構築(ポリシーによる制限あり) | POST | /vaults/:symbol/update-assets-tx | JWT | signerPublicKey + underlyingAssets(mintAddress/symbol/name + mintBps) |
| 更新後のバスケットを DB に同期 | PATCH | /vaults/:symbol/underlying-assets-by-mint | JWT | underlyingAssets(mintAddress + pct_bps) |
| 手数料分配 tx を構築 | POST | /dtf/:symbol/distribute-fees | JWT | signerPublicKey |
| 最小預け入れ額を確認 | POST | /vaults/:symbol/check-min-deposit | JWT | minDeposit |
| 最小償還額を確認 | POST | /check-min-redeem | JWT | minRedeem(isValid を含む 200 を返し、400 は返しません) |
| 預け入れ tx を構築(全 2 ステップのうちステップ 1) | POST | /vaults/:symbol/deposit-tx | JWT | userPublicKey + depositAmount(UI 上の USDC) |
| 預け入れを処理 — スワップ + 記録(全 2 ステップのうちステップ 2) | POST | /deposit-transaction | JWT | transactionSignature + vaultIndex + etfSharePriceRaw + amountInRaw + 任意の slippage |
| 償還チケットをリクエスト | POST | /redeem/request-ticket | JWT | vaultSymbol + vaultTokenAmount(raw u64 文字列)+ 任意の etfSharePriceRaw/slippage/agentWallet |
| 償還チケットを実行 | POST | /redeem/execute/:ticketId | JWT | 任意の slippage(ボールト/数量/価格はチケットに組み込み済み) |
| 償還 tx を構築(確定) | POST | /vaults/:symbol/redeem-tx | JWT | vaultTokenAmount(UI 上のトークン)+ 任意の agentWallet(JWT の正規値) |
| 償還を記録 + チケットを自動確認 | POST | /redeem-transaction | JWT | transactionSignature + 任意の vaultIndex/etfSharePriceRaw/signatureArray/slippage/ticketId(ticketId により自動確認が実行されます) |
| ボールトのシェア残高を取得(オンチェーン読み取り) | GET | /vaults/:symbol/shares(任意の ?wallet=<pubkey>) | JWT | wallet のデフォルトは JWT のエージェントウォレットです。別のウォレットを確認する場合に限り ?wallet= を渡してください |
| トークンを失効 | POST | /token/revoke | JWT | - |
| トークンを更新(profileId による) | POST | /token/refresh | なし | profileId |
| トークンを更新(エージェントウォレットによる) | POST | /token/refresh-by-wallet | なし | agentWalletAddress |
オンチェーン操作
- Vault creation、fee distribution、deposit(Step 1)、basket update、redeem finalizeは、署名されていないbase64トランザクションを返します。エージェントは
DFM_AGENT_KEYPAIRを使ってローカルで署名し、オンチェーンに送信します。 - リバランスは管理者ウォレットによってサーバー側で実行されます。エージェントは識別用に公開鍵を提供するだけです。
- **Deposit Step 2(
/deposit-transaction)とRedeem Step 3(/redeem/execute/:ticketId)**はサーバー側のスワップ操作であり、クライアントによる署名は不要です。バックエンドの管理者ウォレットが、ファンアウト(deposit)/ファンイン(redeem)用のアセットごとのJupiterスワップに署名します。これらの呼び出し中に、VaultのUSDC残高と原資産残高が移動します。 - Redeemフローではキューチケットを使用します(
/redeem/request-ticket→/ticket-statusでポーリング →/redeem/execute)。これにより、複数のredeem実行者によるVaultの清算を順番に処理できます。チケットはisReady=trueになってから約3分後に期限切れになります。
ロゴの処理
launch-dtfとdtf-createでは、必ずmetadataUri: ""、logoUrl: ""、bannerUrl: ""を送信してください。- 起動ペイロードで、これらのフィールドに画像URLを渡さないでください。
エージェントコマンドの使用
| ユーザーの発話 | エージェントの動作 |
|---|---|
Set up my DFM agent | ウォレットアドレスを尋ね、/profile-launch でエージェントプロフィールを作成し、認証トークンを保存してキーペアを生成します |
Launch a Solana blue chip fund | SOL の上位トークンを調査 → 信頼できる流動性/取引量データを取得するため /market-metrics を呼び出し → バスケットとポリシーを決定 → 問題がなくなるまで /policy/dry-run を繰り返し実行 → ポリシーを指定して /launch-dtf → 署名して送信 → メタデータを /dtf-create |
Create a meme token DTF with 3% fee | トレンドのミームトークンを見つけ、/market-metrics を基にポリシーのしきい値を調整し、問題がなくなるまでドライランを実行してデプロイします |
Show me my vaults / List my DTFs / What vaults do I have on DFM / List my vaults / My positions | ユーザーのスコープのみです。 常に GET /vaults/user?page=1&limit=10&vaultType=dtf&includeTvl=true を使用します(常に 1 ページ目から開始します。イールドファンドの場合は vaultType=yield_dtf に切り替えます)。/dtf/my-vaults には絶対にフォールバックしないでください。このレガシーエンドポイントが返すのは、このエージェントプロフィールが起動したボールトのみであり、エージェントのフロー以外でユーザーが作成したボールトは含まれません。/vaults/user は「ユーザーのボールト」を取得する標準エンドポイントであり、「自分の/ユーザーの」ボールトを意味する表現には、常にこれをデフォルトとして使用する必要があります。ユーザーが「プラットフォーム上のすべての DTF」や、その他のプラットフォーム全体を指す表現を使った場合は、このエンドポイントにルーティングしないでください。次の行を参照してください。 |
List all DTFs on DFM / Show me every vault on the platform / What DTFs are available on DFM / All platform vaults / Browse all DTFs / Give me list of vaults available on DFM | プラットフォーム全体の一覧表示には、別々の 2 つのエンドポイントを使用します。 全件一覧は GET /vaults/user(名前にかかわらず、プラットフォーム上のすべてのボールトを返す汎用エンドポイント)から取得します。注目ボールトのみの一覧は GET /vaults/featured/list から取得します。isFeaturedVault クエリパラメーターはありません。追加しないでください。 エージェントは、(a) /vaults/user?vaultType=dtf と /vaults/user?vaultType=yield_dtf を呼び出し、最後までページネーションを行います(これが「すべてのボールト」の基本セットです)。(b) 同じ 2 種類のボールトタイプで /vaults/featured/list を呼び出し、注目対象の vaultSymbol を含む Set を作成します。(c) 結合します。/vaults/user のすべての行を含め、ボールトのシンボルが注目対象のセットに含まれる場合は「注目」列にフラグを付けます。全体的なアルゴリズム、ノードスクリプト、ユーザー向けテンプレートについては、「Step 5: Manage」サブセクションの「Listing all platform DTFs」を参照してください。 |
Show me the second page / Page 2(vaults/user の一覧表示後) | GET /vaults/user?page=2&limit=10&vaultType=dtf&includeTvl=true — 前回の呼び出しと同じ limit / vaultType フィルターを維持します |
Next page / Show me more vaults | GET /vaults/user?page=<lastShown+1>&limit=... — 前回の応答に関する会話メモリから lastShown を読み取ります。pagination.hasNext が false の場合は、「最後のページです」とユーザーに伝えて停止します。 |
Previous page / Go back | GET /vaults/user?page=<lastShown-1>&limit=... — page=1 を下限とします。 |
Show featured vaults | GET /vaults/featured/list?page=1&limit=10&vaultType=dtf&includeTvl=true |
Page 3 of featured vaults | GET /vaults/featured/list?page=3&limit=10&vaultType=dtf&includeTvl=true |
Show me the state of SOLBC | GET /dtf/SOLBC/state -- APY、TVL、NAV、ポートフォリオを返します |
Update underlying for SOLBC / Change SOLBC basket to A,B,C / Swap out X for Y in SOLBC | 3 フェーズの自律フローです。(1) /market-metrics と /dtf/:symbol/policy を基に新しいバスケットを決定します。(2) POST /vaults/:symbol/update-assets-tx を実行します(ポリシーによる制御があり、バスケットがポリシーに違反する場合、サーバーは 400 と violations[] を返します。問題がない場合は未署名トランザクションを返します)。成功した場合は、DFM_AGENT_KEYPAIR でローカル署名してオンチェーンに送信します。(3) PATCH /vaults/:symbol/underlying-assets-by-mint を実行して DB を同期します。フロー全体と再試行ルールについては、「Step 6: Update Underlying Assets」を参照してください。 |
Rebalance SOLBC | ポリシーを確認し、承認された場合はサーバー側でリバランスを実行します |
Distribute fees for SOLBC | POST /dtf/SOLBC/distribute-fees -- 未署名トランザクションを構築し、ローカルで署名してオンチェーンに送信します |
Deposit 5 USDC into SOLBC / Buy 5 USDC of SOLBC shares / Add 10 USDC to SOLBC | 必ず最初にゲートを実行します:POST /vaults/SOLBC/check-min-deposit { minDeposit }。400 の場合は中止します。その後、2 段階の入金フローを実行します。(1) { userPublicKey: <agent wallet>, depositAmount: 5 } を指定して POST /vaults/SOLBC/deposit-tx → ローカルで署名してオンチェーンに送信します。(2) { transactionSignature, vaultIndex, etfSharePriceRaw, amountInRaw, slippage: 200 } を指定して POST /deposit-transaction → バックエンドが Jupiter を介してボールトの USDC をバスケットに分配し、4 件のレコードを保存します。インラインの node -e の例全体については、「Step 7: Capital Flows」を参照してください。 |
Redeem 1.5 SOLBC / Sell 1.5 SOLBC shares / Cash out 2 SOLBC for USDC | 必ず最初にゲートを実行します:POST /check-min-redeem { minRedeem }。isValid を確認します(どちらの場合も 200 が返ります)。false の場合は中止します。その後、5 段階の償還フローを実行します。(1) /redeem/request-ticket → チケット。(2) isReady=true になるまで v1 /api/v1/tx-event-management/redeem/ticket-status/:ticketId をポーリングします。(3) /redeem/execute/:ticketId → バックエンドで資産を USDC に集約します。(4) /vaults/SOLBC/redeem-tx → 未署名の finalizeRedeem トランザクション → 署名して送信します。(5) ticketId を指定して /redeem-transaction → 記録し、自動確認します。インラインの node -e の例全体については、「Step 7: Capital Flows」を参照してください。 |
Check minimum deposit for SOLBC / What's the smallest deposit I can make into SOLBC | { minDeposit: <amount> } を指定して POST /vaults/SOLBC/check-min-deposit を実行します。この呼び出しは入金フロー内でも必須ゲートです — 上記の「Deposit 5 USDC into SOLBC」を参照してください。しきい値未満の場合は 400 を返します。しきい値は、ボールトの原資産数です。割り当てがない場合は MINI_DEPOSIT 環境変数(デフォルトは 5)です。 |
Check minimum redeem / What's the smallest redeem amount | { minRedeem: <amount> } を指定して POST /check-min-redeem を実行します。この呼び出しは償還フロー内でも必須ゲートです — 上記の「Redeem 1.5 SOLBC」を参照してください。200 { isValid, message } を返すため、HTTP ステータスではなく isValid を確認してください。しきい値はグローバル設定です(MINI_REDEEM 環境変数、デフォルトは 4)。 |
How many SOLBC shares do I hold? / What's my position in POP-DTF? / Show my share balance for <vault> | GET /api/v2/agent/vaults/<SYMBOL>/shares。ウォレットは JWT から読み取られます。ユーザーが明示的に別のウォレットの保有分を尋ねていない限り、?wallet= を指定しないでください。 DB ではなく、オンチェーンからウォレットのボールトトークン ATA 残高を直接読み取ります。{ sharesRaw, sharesUi, exists, ata, vaultMintPda, ... } を返します。exists: false(そのウォレットがボールトに入金したことがない)の場合は、「現在、<SYMBOL> のシェアを保有していません。」 と伝えます。また、ユーザーが 「SOLBC をすべて償還して」 などと言った場合に、償還前の確認にも使用します。残高を読み取り、償還フローの vaultTokenAmount に sharesUi を渡します。 |
Generate a Solana keypair for my DFM Agent wallet | キーペアを作成してファイルに保存し、環境変数を書き込み、公開キーのみを報告します |
Troubleshooting
| 問題 | 対処方法 |
|---|---|
| HTTP 403 / "Only the vault creator can perform this action" / "is owned by another user" | 4 つの所有者限定エンドポイントのいずれかによる所有権判定です。このスキルの前の「HARD STOP — Ownership errors」セクションを参照してください。このルールは絶対です。 次の文をそのまま 1 文だけ表示します:"DFM platform doesn't recognize you as the owner of this vault, so this action can't be performed."(または、ボールトの表示名を埋め込んだ文)。このターンを終了します。再試行、別のシンボル/ID/インデックスによる検索、ボールト一覧エンドポイントを横断した検索、他のボールトの調査、環境/トークン/キーペアの提案、「これが意味すること」/「対処方法」セクションの提示は一切行いません。 |
| "Unauthorized" errors | Pre-flight セクションにある token refresh script(node .claude/refresh-token.js)を使用します。このスクリプトは DFM_AGENT_KEYPAIR からエージェントのウォレットアドレスを自動的に導出するため、CLI 引数は不要です。独自の方法を考案しないでください。過去に即興で行ったトークン更新では、プレースホルダー文字列(例:+token+)が settings.json に書き込まれています。.claude/settings.json に +token+ や、その他明らかに無効な値がある場合は、DFM_AUTH_TOKEN エントリを削除してトークン更新スクリプトを再実行してください。 |
/launch-dtf が violations[] を伴う 400 を返す | ポリシー検証に失敗しました。オンチェーンには何も反映されていません。配列内のすべての違反を読み、1 回ですべて修正します(policy のしきい値を調整する、アセットを入れ替える、または pct_bps を再配分する)。低コストで試行錯誤するには /policy/dry-run を実行します。dry-run が問題なく完了してからのみ /launch-dtf を再試行してください。 |
/policy/dry-run が同じ違反を返し続ける | ポリシーの min_amm_liquidity_usd/min_24h_volume_usd と、含まれるアセットの中で最も低い値を持つアセットについての /market-metrics の数値が一致していない可能性があります。しきい値をそのアセットの実際の数値より低くするか、アセットを除外/入れ替えてください。含まれるアセットの実際の値を超えるしきい値は設定しないでください。そのアセットは永久にフラグが立つことになります。 |
/update-assets-tx が、ボールトの asset_whitelist に含まれるミントについて rule2MinAmmLiquidity/rule3Min24hVolume を返す | 取り残されたバッファアセットです。 このミントはルール 1(許可リスト)を通過しましたが、/market-metrics のスナップショットがローンチ時に設定されたポリシーの下限を下回っているため、ルール 2/ルール 3 に失敗しています。根本原因:ローンチ時のエージェントが優先アセットだけを基準に下限を設定し、このミントをバッファに加える際に再確認しませんでした。このボールトでは、このミントへのローテーションはできません。 ユーザーに診断内容(対象のミント、確認された流動性/出来高と必要な下限)を伝え、移行計画からこのミントを外し、下限を満たす別のローテーション候補を選んでください。今後のローンチでこれを防ぐには、提案生成フローで 5 列の Table 3(Floor check 列を含む)を使用し、提案を提示する前に Future-proofing checklist のルール #6(Buffer rotatability)を実行する必要があります。 |
/market-metrics が一部のアセットに null 値を返す | Jupiter の取得が一時的に失敗しています。呼び出しを再試行してください。サービスはミントごとにキャッシュするため、通常は 2 回目の呼び出しで成功します。失敗が続く場合は、そのアセットを除外してください。ポリシーエンジンも、そのアセットについてルール 2/3 を検証できません。 |
| "Keypair file not found" | ウォレットを再生成します(Step 4)。次を確認してください:ls -la $AGENT_WALLET_PATH |
| "No signer keypair" / 空の DFM_AGENT_KEYPAIR | DFM_AGENT_KEYPAIR が設定されていません。再度エクスポートします(Step 5)。次で確認してください:echo $DFM_AGENT_KEYPAIR |
| トランザクションがオンチェーンで失敗する | Agent Wallet には、トランザクション手数料用の SOL とボールト作成手数料用の USDC が必要です。先にウォレットへ資金を入れてください。 |
リバランス時にポリシーの flagged: true が返る | リバランスはブロックされていません。操作はすでに進行しています。policyCheck.reviewFlags を確認して違反したルールを特定し、警告としてユーザーに伝えてください。同じフラグは、最新の RebalancingSuggestion.policyReviewFlags にも保存されています。 |
| トークンが予期せず失効する | トークンが無効化されるのは、明示的な POST /token/revoke 呼び出しがあった場合のみです。更新では既存のトークンに影響を与えずに新しいトークンが発行されます。エージェントごとに複数のアクティブなトークンを使用できます。 |
| launch-dtf で 409 Conflict | この名前/シンボルのポリシー(またはボールト)はすでに存在します。一意の組み合わせを使用してください。注:409 は現在、/dtf-create ではなく /launch-dtf(ポリシーの確定)で発生します。 |
/update-assets-tx が violations[] を伴う 400 を返す | ポリシーゲートに失敗しました。オンチェーンには何も反映されていません。violations[] のすべての項目を読み、「Step 6: Update Underlying Assets」の表に従ってバスケットを調整し、再試行します。無料です。署名もオンチェーンのコストも発生しません。ユーザーに伝える前に最大 3 回まで繰り返してください。 |
/update-assets-tx が 400 "asset not available" を返す | 渡した symbol または name が asset-allocation にありません。mintAddress を直接渡すか、プラットフォームが認識しているシンボルの別のアセットを選んでください(/market-metrics で確認してください)。 |
/update-assets-tx が 404 "No constitutional policy found" を返す | ボールトは /launch-dtf でローンチされていないため、検証対象となるポリシーがありません。安全上、エージェント層で更新がブロックされています。ユーザーに伝えてください。エージェント側で対処できることはありません。 |
エージェントがまったく移行できないボールトで、すべての /update-assets-tx 試行がポリシー違反(ルール 4(アセット数)と更新ごとの移動量に関するルール)に失敗する | ボールトは構造的にロックされたポリシーでローンチされています。通常、min_assets == max_assets(増減の余地がない)、および/または max_rebalance_pct < 2 × max_asset_pct(単一アセットの入れ替えが数学的に上限を超える)状態です。さらに、含まれるアセットの出来高が min_24h_volume_usd を下回ることが多いため、そのアセットを含む中間バスケットはどれもゲートに失敗します。エージェントは自力でこの状態を修復できません。 ポリシーはボールト存続期間中固定され、移行にはプラットフォームがサポートするポリシー更新が必要です。ユーザーに診断内容(min_assets/max_assets/max_rebalance_pct/下限のどの組み合わせが希望する移行と両立しないか)を伝えて終了してください。このような落とし穴を抱える新しいボールトをローンチしないでください。 /launch-dtf を送信する前に、Step 4a の「Future-proofing checklist」を実行してください。 |
オンチェーン更新は成功したが、PATCH /underlying-assets-by-mint が失敗する | /update-assets-tx を再実行しないでください(オンチェーンの変更はすでに反映されています。再実行すると二重に更新され、再検証にも失敗する可能性があります)。同じ本文で PATCH を再試行してください。PATCH が引き続き失敗する場合は、オンチェーンの署名をユーザーに伝えてください。チェーンイベントのパイプラインによって、最終的に DB が同期されます。 |
フィールド名の混同:mintBps と pct_bps | Phase 2(/update-assets-tx)では mintBps を使用します(オンチェーン命令に対応)。Phase 3(/underlying-assets-by-mint)では pct_bps を使用します(DB スキーマに対応)。数値は同じですが、キーは異なります。名前を変更せずにペイロード間でコピー&ペーストしないでください。 |
/deposit-tx が 400 Vault "<symbol>" has no on-chain vaultIndex を返す | ボールトは /launch-dtf でローンチされましたが、オンチェーンへの送信(または /dtf-create)は反映されていません。ボールトが実際にオンチェーンに作成されるまで、入金/償還には使用できないことをユーザーに伝えてください。 |
/deposit-tx が 400 Signed price vaultPubkey ... does not match derived vault PDA を返す | このボールトの KMS 署名者の設定に誤りがあります(オペレーター側の問題です)。再試行しないでください。 ユーザーに伝えてください。 |
/deposit-transaction が swap.failedSwapsInfo を含む 200 を返す | アセットごとの Jupiter スワップの一部が失敗し、バックエンドはすでに USDC をボールトに返しています。入金は(成功したスワップの署名とともに)記録されています。成功として扱い、失敗したスワップ数をユーザーに警告してください。同じ署名で再度呼び出さないでください。入金記録はすでに存在します。 |
/deposit-transaction が deposit.events[].vaultDepositUpdateError を含む 200 を返す | オンチェーンのスワップは成功しましたが、DB レコードの書き込みでエラーが発生しました。チェーンイベントのパイプラインによって、最終的に整合性が取られます。ユーザーに伝えてください。再試行しないでください(再呼び出ししても、重複署名ガードによって既存の記録が返されます)。 |
PHASE_3_OK 後に /deposit-transaction がタイムアウトする、5xx、ゲートウェイエラー、または 200 以外を返す | HARD STOP。再試行しないでください。 このエンドポイントは冪等ではありません。呼び出すたびに新しい agentSwap が実行されます。応答が失われた場合に再試行すると、残りのボールト内 USDC に対して 2 回目のスワップ一式が実行され、NAV が崩れます(SOLCORE のインシデントのパターン:7 USDC を入金し、1.7 しかバスケットに入らないまま、誤った価格でシェアが発行される)。資本フローのエラー表にある次の文をそのまま使って、オンチェーンの署名とともにユーザーに伝えてください:"deposit landed, swap step couldn't be confirmed, operator will reconcile, please don't retry"。このターンを終了します。安全な復旧方法はバックエンド側での整合性調整のみです。エラーがどれほど一時的に見えても、エージェント側での再試行は禁止されています。 |
/redeem/execute/:ticketId が例外をスローする | バックエンドはチケットをすでに自動キャンセルしています。/redeem/cancel/:ticketId を呼び出さないでください。エラーをユーザーに伝えて、このターンを終了します。 |
チケットのポーリングが isReady=true にならないまま 5 分を超える | v1 の DELETE /api/v1/tx-event-management/redeem/cancel/:ticketId でキャンセルし、ユーザーに伝えてから、再試行を希望する場合は新たに /redeem/request-ticket を呼び出します。いつまでもポーリングしないでください。 |
オンチェーン送信後に /redeem-tx が 400 InvalidPriceSignature を返す | トランザクションに埋め込まれた KMS 署名済みシェア価格が、ビルドから送信までの間に期限切れになりました。最新の価格を取得するために /redeem-tx を再度呼び出してください。期限切れのトランザクションを再送信してはいけません。 |
/redeem-transaction が 400 No position found for agent in this vault を返す | agentProfile をキーとするエージェントの UserVaultPosition が存在しません。まずエージェントフローで /deposit を実行する必要があります。エージェントフロー以外で資金が投入されたボールトには、エージェントをキーとするポジションがありません。ユーザーに伝えてください。 |
/redeem-transaction が 400 Insufficient shares. Have X, trying to redeem Y を返す | オンチェーンの償還はすでに実行されていますが、記録されたポジションには要求された数より少ないシェアしかありません。以前の償還が記録されなかった可能性があります。オンチェーンの署名をユーザーに伝えて終了してください。再実行しても解決しません。 |
/redeem-transaction が ticketConfirm.ok = false を含む 200 を返す | 自動確認には失敗しましたが、償還は記録されています。必要に応じて、同じ署名を使って v1 の POST /api/v1/tx-event-management/redeem/confirm/:ticketId を手動で呼び出してください。キューのクリーンアップは衛生管理のためだけのものであり、オンチェーンの償還はどちらにしても確定しています。 |
/redeem-transaction が 400 redeemAgentTransaction requires performedByProfileId を返す | JWT に agentId が含まれていません。トークン更新スクリプト(node .claude/refresh-token.js)を再実行してください。新しいトークンには agentPayload.agentId が正しく設定されます。 |
| profile-launch で 409 "Username is already taken" | profile-launch.js スクリプトは、サニタイズした基本ユーザー名にランダムな 4 文字の 16 進数サフィックスを追加して、最大 5 回まで自動再試行します。このエラーがユーザーに表示された場合、スクリプトは再試行を使い切っています。より特徴的な基本ユーザー名を選んで再実行してください。 |
| profile-launch で 409 "An agent profile already exists for this wallet address" | このウォレットはすでにオンボーディング済みです。profile-launch を再度呼び出してはいけません(この 409 ではスクリプトも再試行しません)。代わりに node .claude/refresh-token.js を使用して、既存のエージェントプロフィールの新しい JWT を発行してください(スクリプトは DFM_AGENT_KEYPAIR からエージェントのウォレットを導出します)。 |
ユーザーがボールトを持っているのに、/vaults/featured/list または /vaults/user が「ボールトを返さない」 | ほとんどの場合、リストが空なのではなく、応答の形式を読み違えています。これらのエンドポイントには pagination フィールドがあるため、グローバルな ResponseMiddleware はラップしません。配列は body.data にあります(body.data.vaults、body.data.data、body.vaults ではありません)。{ vaults: [...], total } を返すのは /dtf/my-vaults(レガシー)のみです。すべての誤ったアクセスパターンについては、「Step 5: Manage」→「Common mistakes」の表を参照してください。呼び出しを再実行して、まず body.data.length を確認してください。> 0 なら、問題は取得後の処理にあります。 |
セキュリティ
- ターミナル出力に機密情報を絶対に表示しないでください。
DFM_AUTH_TOKEN、DFM_AGENT_KEYPAIR、および秘密鍵を含みます。出力してよいのは公開鍵のみです。シークレットはファイルに書き込まれ、出力されません。 - Agent Wallet ファイル -- パーミッションは
0o600で、gitignore に登録されており、コミットされることはありません。 DFM_AGENT_KEYPAIR-- base58 秘密鍵は環境変数にのみ設定し、コード、git、ターミナル出力には含めません。DFM_AUTH_TOKEN-- JWT トークンは環境変数にのみ設定し、ターミナルに出力したり、ログに記録したりしません。- バックエンドに秘密鍵を送信しません -- API ペイロードに含めるのは公開鍵のみです。トランザクションへの署名はローカルで行われます。
- Agent Wallet = オンチェーンの権限 -- 暗号資産ウォレットと同様に扱ってください。安全にバックアップしてください。
- ポリシーエンジン = セーフティネット -- 完全に自律したエージェントでも、ポリシーの制約を回避することはできません。