Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Autonomous Solana portfolio management. Non-custodial wallets, auto-trading, market analysis, orders, transfers, and AI conversations.
.claude/skills/itamarzand88-milo/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | 731% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 415% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 635% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 469% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 530% | 0% |
<!-- source: milo-solana-portfolio — https://raw.githubusercontent.com/and-milo/agent-to-agent-portfolio-manager/main/skill.md -->
Milo is an autonomous Solana portfolio manager. Through this API you can register users, create non-custodial wallets, send tokens, place buy/sell orders, manage positions, configure auto-trading strategies, and converse with Milo's AI agents.
Follow these 4 steps to go from zero to a fully active Milo account.
Request a SIWX message, sign it with your Solana wallet, and register.
bash# 1a. Get the message to sign curl -X POST {{BASE_URL}}/api/v1/users/siwx/message \ -H "Content-Type: application/json" \ -d '{ "accountAddress": "<your-wallet-address>", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }' # 1b. Sign the returned "message" (UTF-8 bytes) with your ed25519 key, base58-encode the signature # 1c. Register curl -X POST {{BASE_URL}}/api/v1/users \ -H "Content-Type: application/json" \ -d '{ "signupWallet": "<your-wallet-address>", "siwx": { "data": <data-object-from-1a>, "message": "<message-string-from-1a>", "signature": "<base58-signature-from-1b>" } }' # → returns apiKey, user.id, wallets[] (save these to ~/.milo/config.json)
Start a conversation with the market analyst to explore current opportunities.
bash# 2a. Create a conversation curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "What tokens are trending right now on Solana?", "agentType": "market-analyst" }' # → returns conversationId # 2b. Poll for the response (wait 2-3s between polls) curl "{{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages" \ -H "X-API-Key: $API_KEY" # → when processing: false, the agent has responded
Deposit SOL to your Milo wallet so it has capital to trade. The Milo wallet address is wallets[1].address from signup (the type: "milo" wallet).
Send SOL from your external wallet to that address. Once funded, verify:
bashcurl {{BASE_URL}}/api/v1/wallets/{walletId}/holdings \ -H "X-API-Key: $API_KEY"
Turn on Milo's autonomous trading agent. It will start scanning for opportunities within ~30 minutes.
bashcurl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isActive": true, "riskTolerance": "balanced", "strategy": "SWING TRADER" }'
That's it — Milo is now managing your portfolio. Read on for the full feature guide and API reference.
> Note: You do NOT need to read this entire document. Use the guide below to jump to the relevant section for your task.
| If you need to... | Read section | |---|---| | Look up your user ID and wallet IDs | "Me" (under Complete API Reference) | | Register a new user / get an API key | "First Boot Protocol" and "Signup" (under Complete API Reference) | | Activate or configure auto-trading | "Recipe 2: Activate the auto-trader" and "Auto-Trade Settings" (under Complete API Reference) | | Create, link, or manage strategies | "Strategies" (under Complete API Reference) | | Deploy and manage arena strategies | "Arena" (under Complete API Reference) | | View quests and claim bones | "Quests & Bones" (under Complete API Reference) | | Place, list, or manage orders | "Orders" (under Complete API Reference) | | Send tokens to another wallet | "Wallet Actions" (under Complete API Reference) | | Check positions, holdings, or PnL | "Recipe 4: Fetch your positions" and "Portfolio" (under Complete API Reference) | | Chat with Milo's AI agents | "Recipe 3: Talk with Milo" / "Recipe 6: Ask about a token" and "Conversations" (under Complete API Reference) | | Understand the full onboarding flow | "Quick Start" and "First Boot Protocol" | | Set up a recurring portfolio check-in | "Heartbeat Protocol" |
Capabilities:
POST /api/v1/users/siwx/message with your wallet address to get a message to sign.POST /api/v1/users with the signed SIWX proof to create a user and receive an API key + Milo wallet.~/.milo/config.json.type: "milo" wallet from signup).PATCH /api/v1/users/{userId}/auto-trade-settings with { "isActive": true }.GET /api/v1/users/{userId}/quests to see available quests. Claim bones for any completed quests.After signup, persist credentials locally so they survive across sessions:
json// ~/.milo/config.json { "api_key": "mk_live_...", "user_id": "uuid", "wallet_id": "uuid", "wallet_address": "7xKXtg...", "base_url": "{{BASE_URL}}" }
> Warning: In ephemeral environments (containers, serverless), this file may not persist. Use environment variables or a secrets manager instead.
All authenticated requests require the header:
X-API-Key: <api_key>(NOT Authorization: Bearer — the API uses the X-API-Key header.)
The partner API server exposes MCP on the same host at /mcp using Streamable HTTP:
POST /mcp for initialize and JSON-RPC requestsGET /mcp for SSE streamDELETE /mcp for session terminationPOST /mcp initialize accepts optional X-API-Key. If omitted, the MCP session starts unauthenticated and only signup/public tools are usable until signup returns an API key. Request throttling and session-capacity protections are enforced, so initialize may return 429. On 429, respect Retry-After and retry after the indicated delay. Sessions can also expire, so re-initialize after invalid-session errors.
For sessioned requests after initialize, send Mcp-Session-Id.
Milo creates a non-custodial Solana wallet for each user via Turnkey. The wallet-creating account is the owner; Milo receives delegated permission for trading.
On signup you receive two wallets:
type: "signup" - Your external signing wallet (used for SIWX verification).type: "milo" - Your Milo trading wallet. Deposit SOL here for trading.To check your wallet balance, use the holdings endpoint:
bashcurl {{BASE_URL}}/api/v1/wallets/{walletId}/holdings \ -H "X-API-Key: $API_KEY"
When starting fresh with no saved credentials:
bash curl -X POST {{BASE_URL}}/api/v1/users/siwx/message \ -H "Content-Type: application/json" \ -d '{ "accountAddress": "<your-wallet-address>", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }' This returns { "data": { ... }, "message": "<message-string>" }. The data object contains all SIWX fields (domain, uri, nonce, etc.) and the message is the human-readable string to sign.
message string (UTF-8 encoded bytes) with your Solana keypair's ed25519 private key. Base58-encode the resulting signature.bash curl -X POST {{BASE_URL}}/api/v1/users \ -H "Content-Type: application/json" \ -d '{ "signupWallet": "<your-wallet-address>", "siwx": { "data": <data-object-from-step-2>, "message": "<message-string-from-step-2>", "signature": "<base58-signature-from-step-3>" } }' Important: Pass the data and message exactly as returned by the server. Do not construct them manually.
apiKey, user.id, and wallets[1] (the Milo wallet) to ~/.milo/config.json.bash curl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isActive": true, "riskTolerance": "balanced", "strategy": "SWING TRADER" }'
See heartbeat.md for the recurring check-in protocol. Run every 4+ hours to stay informed about your portfolio.
This cookbook walks through the most common workflows end-to-end. Follow these recipes to get up and running quickly.
Before anything else you need credentials. Generate a Solana keypair, request a SIWX message from the server, sign it, and register.
bash# 1. Request a SIWX message to sign curl -X POST {{BASE_URL}}/api/v1/users/siwx/message \ -H "Content-Type: application/json" \ -d '{ "accountAddress": "<your-wallet-address>", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }' # → returns { "data": { "data": { ... }, "message": "<message-to-sign>" } } # 2. Sign the "message" string (UTF-8 bytes) with your ed25519 private key # Base58-encode the signature # 3. Register with the signed proof curl -X POST {{BASE_URL}}/api/v1/users \ -H "Content-Type: application/json" \ -d '{ "signupWallet": "<your-wallet-address>", "siwx": { "data": <data-object-from-step-1>, "message": "<message-string-from-step-1>", "signature": "<base58-signature-from-step-2>" } }' # 4. Save the response — you need these for every subsequent call # api_key → Authorization header # user.id → {userId} in routes # wallets[1].id → {walletId} (the "milo" wallet) # wallets[1].address → deposit SOL here
Store credentials in ~/.milo/config.json so they persist across sessions.
Once registered, turn on Milo's autonomous trading agent.
bashcurl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isActive": true, "riskTolerance": "balanced", "strategy": "SWING TRADER", "instructions": "Focus on SOL ecosystem tokens. Avoid meme coins.", "customTickers": ["SOL", "JUP", "BONK"] }'
That's it — Milo will start monitoring markets and placing trades according to your configuration. You can verify it's active:
bashcurl {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY"
Use the auto-trader agent to discuss, refine, or brainstorm your trading strategy. This is a conversation — Milo understands your portfolio context.
bash# 1. Start a conversation with the auto-trader curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "I want to shift my strategy toward DeFi blue-chips. What allocation do you recommend for a balanced risk profile?", "agentType": "auto-trader" }' # → returns { conversationId, processing: true } # 2. Poll for the response (wait 2-3s between polls) curl "{{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages" \ -H "X-API-Key: $API_KEY" # → when processing: false, the agent has responded # 3. Continue the conversation curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Good plan. Apply those changes to my auto-trade settings." }'
Tip: The auto-trader agent can directly update your settings when you ask it to — it has tool access to modify your allocation, risk tolerance, and instructions.
Check what Milo is holding and how each position is performing.
bash# List all active positions with PnL curl "{{BASE_URL}}/api/v1/users/{userId}/positions?status=active&page=1&pageSize=50" \ -H "X-API-Key: $API_KEY"
Each position includes the token, entry price, current PnL, and thesis. Use status=pending for positions still being built, or omit the filter to get everything.
For a deeper look at your wallet's token balances:
bashcurl {{BASE_URL}}/api/v1/wallets/{walletId}/holdings \ -H "X-API-Key: $API_KEY"
Have Milo re-analyze an existing position to decide whether to hold, add, or exit.
bash# 1. Get the position's thesisId from the positions list (Recipe 4) # 2. Start a conversation with the auto-trader, referencing the position curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Re-evaluate my position on JUP (thesis {thesisId}). Given the latest market conditions, should I hold, take partial profit, or close it entirely?", "agentType": "auto-trader" }' # 3. Poll for the response curl "{{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages" \ -H "X-API-Key: $API_KEY" # 4. If Milo recommends closing, you can close it directly: curl -X POST {{BASE_URL}}/api/v1/users/{userId}/positions/{thesisId}/close \ -H "X-API-Key: $API_KEY"
Use the market-analyst agent for token research. It can pull market data, analyze trends, and give you a thesis.
bash# 1. Ask the market analyst about a token curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "What do you think about RENDER? Give me a full analysis — fundamentals, technicals, and whether it fits my current portfolio.", "agentType": "market-analyst" }' # 2. Poll for the response curl "{{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages" \ -H "X-API-Key: $API_KEY" # 3. Follow up with more questions in the same conversation curl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Compare it with RNDR vs HNT for a DePIN play. Which is the better entry right now?" }'
Tip: The market-analyst agent focuses on research and analysis. If you want Milo to actually execute a trade based on the analysis, either create an order manually or switch to the auto-trader agent.
A typical session with Milo looks like:
Run the heartbeat protocol every 4+ hours to keep this cycle going automatically.
All endpoints (except signup) require:
X-API-Key: <api_key>Get the current user profile and wallets for the authenticated API key. Use this to discover your userId and walletId values.
bashcurl {{BASE_URL}}/api/v1/me \ -H "X-API-Key: $API_KEY"
Response (200):
json{ "user": { "id": "uuid", "signupWalletId": "uuid", "provider": "siwx", "createdAt": "2025-01-01T00:00:00.000Z" }, "wallets": [ { "id": "uuid", "address": "<address>", "chain": "solana", "type": "signup" }, { "id": "uuid", "address": "<address>", "chain": "solana", "type": "milo" } ] }
Signup is a two-step process: first request a SIWX message from the server, then sign it and submit the proof.
Generate a SIWX message for the wallet to sign. This is step 1 of signup.
bashcurl -X POST {{BASE_URL}}/api/v1/users/siwx/message \ -H "Content-Type: application/json" \ -d '{ "accountAddress": "<wallet-address>", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }'
Request body: | Field | Type | Required | Description | |-------|------|----------|-------------| | accountAddress | string | yes | Solana wallet address (32-64 chars) | | chainId | string | yes | CAIP-2 chain ID (e.g. solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) | | inviteCode | string | no | Optional invite code |
Response (200):
json{ "data": { "data": { "accountAddress": "<wallet-address>", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "domain": "andmilo.com", "uri": "https://andmilo.com", "version": "v1", "nonce": "<server-generated-nonce>", "issuedAt": "2025-06-15T12:00:00.000Z", "expirationTime": "2025-06-22T12:00:00.000Z", "statement": "By signing, you agree to andmilo Terms of Use..." }, "message": "andmilo.com wants you to sign in with your Solana account:\n<wallet-address>\n\n..." } }
Sign the message string (UTF-8 encoded bytes) with the wallet's ed25519 private key. Base58-encode the signature. Then proceed to step 2.
Create a new user via SIWX wallet verification. This is step 2 of signup.
bashcurl -X POST {{BASE_URL}}/api/v1/users \ -H "Content-Type: application/json" \ -d '{ "signupWallet": "<wallet-address>", "siwx": { "data": <data-object-from-siwx-message-response>, "message": "<message-string-from-siwx-message-response>", "signature": "<base58-ed25519-signature>" } }'
Important: Pass the data and message fields exactly as returned by POST /api/v1/users/siwx/message. Do not construct them manually.
Response (200):
json{ "data": { "user": { "id": "uuid", "signupWalletId": "uuid", "provider": "siwx", "createdAt": "..." }, "wallets": [ { "id": "uuid", "address": "<address>", "chain": "solana", "type": "signup" }, { "id": "uuid", "address": "<address>", "chain": "solana", "type": "milo" } ], "apiKey": "mk_live_..." } }
Get current auto-trade configuration.
bashcurl {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY"
Update auto-trade configuration.
bashcurl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isActive": true, "riskTolerance": "balanced", "strategy": "SWING TRADER", "instructions": "Focus on SOL ecosystem tokens", "customTickers": ["SOL", "JUP", "BONK"], "dataSources": { "fundingRates": true, "openInterest": true }, "assetClassSettings": { "memes": { "dataSources": { "liquidationData": true } }, "majors": { "dataSources": { "macroData": true } } } }'
Settings fields: | Field | Type | Values | |-------|------|--------| | isActive | boolean | Enable/disable auto-trading | | riskTolerance | string | conservative, balanced, degen | | strategy | string | VALUE INVESTOR, SWING TRADER, SCALPER, CUSTOM | | strategyId | uuid \| null | Link a saved strategy to auto-trade settings | | modelVersion | string \| null | Preferred model | | instructions | string | Free-text trading instructions for the agent | | customTickers | string] | Specific tokens to focus on | | allocation | object | Asset class allocation percentages | | dataSources | object \| null | Global data-source toggles: fundingRates, openInterest, liquidationData, macroData | | assetClassSettings | object \| null | Per-asset-class configuration, including nested dataSources overrides |
Asset classes for allocation: trenches, memes, promising-memes, staking, native, majors, stables, xStocks, custom
Data-source resolution notes:
dataSources overrides win over top-level dataSources.dataSources both globally and inside assetClassSettings, so partial updates do not wipe sibling keys.Model entitlement notes:
o3, gpt-5.2-high, gpt-5.2-xh, and gpt-5.4.claude-opus-4.5 and claude-opus-4.6.gemini-3-pro and gemini-3.1-pro-preview.grok-4.1-fast-reasoning and grok-4.400 Bad Request with error.details.requiredPlan, error.details.upgradeUrl, and an error message containing the same plan-specific Stripe link.Data-source entitlement notes:
pro and max users can create, update, apply, sync, or otherwise change dataSources.dataSources in GET responses.dataSources remain visible but are inactive in auto-trader chat, execution, and position review until the account is back on Pro or Max.400 Bad Request with error.details.feature = "dataSources", error.details.requiredPlan = "pro", and upgrade details when available.Strategies are reusable autotrade configurations that live under auto-trade settings. Create a strategy, link it to your settings, and sync when the strategy evolves.
Workflow:
PATCH /auto-trade-settings with { "strategyId": "<uuid>" }. This takes a snapshot of the strategy into your settings.strategySync.synced: false.POST .../strategies/{strategyId}/sync to re-apply the latest version.Create a new strategy.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "SOL Ecosystem DCA", "description": "Dollar cost average into SOL ecosystem tokens", "strategy": "SWING TRADER", "instructions": "Focus on SOL, JUP, and BONK with balanced entries", "allocation": { "majors": 45, "native": 25, "staking": 10, "promising-memes": 15, "xStocks": 5 }, "customTickers": ["SOL", "JUP", "BONK"], "dataSources": { "fundingRates": true, "openInterest": true }, "assetClassSettings": { "memes": { "dataSources": { "liquidationData": true } }, "majors": { "dataSources": { "macroData": true } } }, "isPublic": false }'
Response (201):
json{ "data": { "id": "uuid", "name": "SOL Ecosystem DCA", "strategy": "SWING TRADER", "allocation": { ... }, "createdAt": "..." } }
Create/update fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | name | string | yes (create) | Strategy name (1-200 chars) | | description | string \| null | no | Description (max 2000 chars) | | instructions | string | no | Free-text instructions (max 4000 chars) | | strategy | string | yes (create) | VALUE INVESTOR, SWING TRADER, SCALPER, CUSTOM | | allocation | object | no | Asset class allocation percentages | | customTickers | string] \| null | no | Token tickers to focus on | | dataSources | object \| null | no | Global data-source toggles carried into the strategy snapshot | | assetClassSettings | object \| null | no | Per-asset-class configuration, including nested dataSources | | isPublic | boolean | no | Make strategy publicly discoverable |
The same Pro/Max restriction applies when creating or updating a strategy with dataSources, and when syncing a linked strategy that already contains them.
List strategies with filters.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies?scope=owned&page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Values | Description | |-------|--------|-------------| | scope | all, owned, public | Filter by ownership | | q | string | Search by name/description | | page | number | Page number (default: 1) | | pageSize | number | Items per page (default: 25, max: 100) |
Get strategy details.
bashcurl {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies/{strategyId} \ -H "X-API-Key: $API_KEY"
Update a strategy. All fields are optional.
bashcurl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies/{strategyId} \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Strategy Name", "instructions": "New instructions for the agent" }'
Delete a strategy.
bashcurl -X DELETE {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies/{strategyId} \ -H "X-API-Key: $API_KEY"
Re-sync auto-trade settings with the linked strategy. Takes a fresh snapshot of the strategy's current allocation, instructions, and configuration.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies/{strategyId}/sync \ -H "X-API-Key: $API_KEY"
Strategy sync workflow:
bashcurl -X PATCH {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "strategyId": "<strategy-uuid>", "isActive": true }'
json{ "strategyId": "uuid", "strategySync": { "strategyId": "uuid", "synced": false, "strategyUpdatedAt": "2025-06-15T14:00:00.000Z", "settingsUpdatedAt": "2025-06-01T10:00:00.000Z" } }
synced is false, re-sync:bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/auto-trade-settings/strategies/{strategyId}/sync \ -H "X-API-Key: $API_KEY"
Deploy a public strategy to the arena leaderboard. Milo creates a custody wallet, funds it, and trades autonomously using the strategy. The strategy must be public and owned by the user. Deployment requires at least 1 SOL balance (0.01 SOL in dev). Withdrawing transfers all holdings back to the user's Milo wallet.
Deploy a strategy to the arena.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/arena/deploy \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "strategyId": "<strategy-uuid>" }'
Request body: | Field | Type | Required | Description | |-------|------|----------|-------------| | strategyId | uuid | yes | ID of a public strategy owned by the user |
Response (200):
json{ "data": { "arenaUserId": "uuid", "custodyWalletId": "uuid", "custodyWalletAddress": "<solana-address>", "fundingTxSignature": "<transaction-signature>", "strategyId": "uuid" } }
Withdraw from the arena. Transfers all holdings from the custody wallet back to the user's Milo wallet.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/arena/withdraw \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "strategyId": "<strategy-uuid>" }'
Request body: | Field | Type | Required | Description | |-------|------|----------|-------------| | strategyId | uuid | yes | ID of the deployed strategy to withdraw |
Response (200):
json{ "data": { "arenaUserId": "uuid", "custodyWalletId": "uuid", "custodyWalletAddress": "<solana-address>", "recipientWalletAddress": "<solana-address>", "transferTxs": [ { "tokenAddress": "<mint-address>", "amount": 1.5, "signature": "<tx-signature>" } ] } }
Get the arena leaderboard.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/arena/leaderboard?timeframe=30d&page=1&pageSize=25&sortKey=pnl&sortDirection=desc" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Type | Values | Default | |-------|------|--------|---------| | timeframe | string | 1d, 30d, 90d | — | | page | number | Page number | 1 | | pageSize | number | Items per page (max: 100) | 25 | | sortKey | string | pnl, winRate, returnPct, accountValue | — | | sortDirection | string | asc, desc | — |
winRate is token-PnL based: (number of tokens with positive token PnL / total tracked tokens) * 100, excluding USDC.
Response (200):
json{ "data": [ { "strategy": "uuid", "strategyName": "SOL Ecosystem DCA", "ownerUserId": "uuid", "ownerUsername": "trader1", "pnl": 120.50, "winRate": 0.65, "returnPct": 12.5, "accountValue": 1120.50, "arenaWalletAddress": "<solana-address>", "currentHoldings": [ ... ] } ], "meta": { "page": 1, "pageSize": 25, "total": 100, "pages": 4 } }
Quests are event-driven tasks (trades, signups, etc.) that reward bones (points) upon completion. Each quest has requirements (count, sum, or streak-based) and awards bones when all requirements are met. Check quests regularly — call list_quests to see open quests, and with unclaimed=true to discover completed quests, then claim_quest to collect the bones.
List quests with progress and requirements. By default returns only unlocked (available) quests.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/quests?page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Type | Values | Default | |-------|------|--------|---------| | unlocked | boolean | Filter for unlocked quests (available) | true | | unclaimed | boolean | Filter for completed but unclaimed quests | — | | claimed | boolean | Filter for claimed quests | — | | mode | string | completed_last | — | | page | number | Page number | 1 | | pageSize | number | Items per page (max: 100) | 25 |
Response (200):
json{ "data": [ { "userId": "uuid", "questId": "uuid", "title": "First Trade", "description": "Execute your first trade", "award": 100, "totalRequirements": 1, "completedRequirements": 1, "claimed": false, "completed": true, "unlocked": true, "requirements": [ { "requirementId": "uuid", "aggregationKind": "count", "targetValue": 1, "currentValue": 1, "completed": true } ] } ], "meta": { "page": 1, "pageSize": 25, "total": 10, "pages": 1 } }
Claim bones for a completed quest.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/quests/{questId}/claim \ -H "X-API-Key: $API_KEY"
Response (200):
json{ "data": null }
Returns 404 if the quest is not found or already claimed.
Get the user's bones balance.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/quests/bones" \ -H "X-API-Key: $API_KEY"
Response (200):
json{ "data": { "userId": "uuid", "username": "alice", "balance": 500, "unclaimed": 100 } }
balance — total bones already claimedunclaimed — bones from completed quests not yet claimed (call claim_quest to collect)Send tokens from your Milo wallet to a recipient address.
bashcurl -X POST {{BASE_URL}}/api/v1/wallets/{walletId}/actions/send \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient": "<solana-address>", "token": "<token-mint-address>", "amount": 1.5 }'
Response (202):
json{ "data": "<transaction-signature>" }
> Note: For native SOL, use the SOL mint address: So11111111111111111111111111111111111111112. > If a JSON body includes walletId, it must match the {walletId} path parameter.
Create a new order, optionally with take-profit and stop-loss dependants.
bashcurl -X POST {{BASE_URL}}/api/v1/wallets/{walletId}/orders \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tokenAddress": "So11111111111111111111111111111111111111112", "type": "buy", "status": "active", "expiresAt": "2026-12-31T23:59:59.000Z", "payload": { "type": "buy", "amount": { "type": "absolute_usd", "amount": 50 }, "trigger": { "type": "absolute", "trigger": "price", "operator": "gte", "value": 0 }, "execution": {} }, "takeProfits": [ { "percentage": 50, "profitPercentage": 20 }, { "percentage": 50, "profitPercentage": 50 } ], "stopLosses": [ { "percentage": 100, "lossPercentage": 15 } ] }'
Response includes the main order plus a dependants array:
json{ "data": { "id": "...", "type": "buy", "status": "active", "dependants": [ { "type": "take_profit", "order": { "id": "...", "parentId": "...", "subType": "take_profit", "status": "draft" } }, { "type": "take_profit", "order": { "id": "...", "parentId": "...", "subType": "take_profit", "status": "draft" } }, { "type": "stop_loss", "order": { "id": "...", "parentId": "...", "subType": "stop_loss", "status": "draft" } } ] } }
Take-profit / Stop-loss ladder (optional):
takeProfits array — each item:
percentage (1–100) — percent of position to sellprofitPercentage (> 0) — profit percent to trigger the TPstopLosses array — each item:
percentage (1–100) — percent of position to selllossPercentage (1–100) — loss percent to trigger the SLDependant creation flow (enforced):
parentId = main order ID).201.Guardrails (always enforce mode):
takeProfits.length <= 5stopLosses.length <= 5takeProfits.length + stopLosses.length <= 8Violations return 400 bad_request with a clear validation message. Dependants use relative triggers (rise for TP, drop for SL).
Order payload structure:
Buy order amounts:
{ "type": "absolute", "amount": 1000000 } - Raw token amount{ "type": "absolute_usd", "amount": 50 } - USD equivalentSell order amounts (additional option):
{ "type": "relative", "percentage": 50 } - Percentage of positionTrigger types:
{ "type": "absolute", "trigger": "price", "operator": "gte", "value": 0 }{ "type": "absolute", "trigger": "price", "operator": "lte", "value": 150.00 }{ "type": "relative", "trigger": "price", "operator": "drop", "value": 10 } (10% drop){ "type": "relative", "trigger": "price", "operator": "rise", "value": 25 } (25% rise)Market orders require expiresAt, and it must be within 120 minutes of the request time.
Execution options (all optional):
slippagePercentage - Max slippage (0-100)priorityFee - Priority fee in SOL lamportsplatformFeeBps - Platform fee override in basis pointsLinking to a thesis: Pass positionThesisId (UUID) in the body to attach the order (and its TP/SL dependants) to a position thesis.
List orders with filters.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/orders?status=active&type=buy&page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Type | Values | |-------|------|--------| | status | string | active, paused, error, fulfilled, archived, draft | | type | string | buy, sell | | tokenAddress | string | Filter by token | | page | number | Page number (default: 1, max: 100) | | pageSize | number | Items per page (default: 25, max: 100) |
Get order details.
bashcurl {{BASE_URL}}/api/v1/users/{userId}/orders/{orderId} \ -H "X-API-Key: $API_KEY"
Pause an active order.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/orders/{orderId}/pause \ -H "X-API-Key: $API_KEY"
Activate a draft or paused order.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/orders/{orderId}/activate \ -H "X-API-Key: $API_KEY"
Archive (delete) an order.
bashcurl -X DELETE {{BASE_URL}}/api/v1/users/{userId}/orders/{orderId} \ -H "X-API-Key: $API_KEY"
Milo uses an async conversation model. You send a message and the agent processes it in the background. Poll the messages endpoint for the response using the processing flag.
Create a new conversation and send the first message.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "What is the current market sentiment for SOL?", "agentType": "market-analyst" }'
Agent types: | Agent | ID | Purpose | |-------|----|---------| | Market Analyst | market-analyst | Market analysis, token research, sentiment | | Auto Trader | auto-trader | Trading strategy discussion | | Game Agent | milo-game-agent | Gamified trading experience |
Response (201):
json{ "data": { "conversationId": "...", "status": "active", "agentActive": true, "processing": true, "createdAt": "..." } }
List conversations.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/conversations?page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Get conversation details.
bashcurl {{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId} \ -H "X-API-Key: $API_KEY"
Send a follow-up message.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "What about JUP?" }'
Poll for messages. Check processing flag to know if the agent is still working.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/conversations/{conversationId}/messages?page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Response:
json{ "data": { "messages": [ { "messageId": "...", "role": "user", "content": "...", "createdAt": "..." }, { "messageId": "...", "role": "assistant", "content": "...", "createdAt": "..." } ], "processing": false }, "meta": { "page": 1, "pageSize": 25, "total": 2, "pages": 1 } }
Polling pattern:
processing is false, the agent has finished responding.Get current token holdings for a wallet.
bashcurl {{BASE_URL}}/api/v1/wallets/{walletId}/holdings \ -H "X-API-Key: $API_KEY"
Get all transactions for a wallet.
bashcurl "{{BASE_URL}}/api/v1/wallets/{walletId}/transactions?page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Get executed transactions (order-linked trades) for a wallet. Uses cursor-based pagination.
bashcurl "{{BASE_URL}}/api/v1/wallets/{walletId}/executed-transactions?limit=25" \ -H "X-API-Key: $API_KEY" # To paginate, pass the nextCursor from the previous response: curl "{{BASE_URL}}/api/v1/wallets/{walletId}/executed-transactions?limit=25&cursor=<nextCursor>" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Type | Description | |-------|------|-------------| | limit | number | Items per page (default: 25, max: 200) | | cursor | string | Cursor from previous response's nextCursor | | txType | string | Filter by buy or sell | | token | string | Filter by token address |
Get positions with PnL data.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/positions?status=active&page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Query parameters: | Param | Values | |-------|--------| | status | active, pending, not_active |
Close a single position. Cancels pending orders and creates a sell order for remaining holdings.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/positions/{thesisId}/close \ -H "X-API-Key: $API_KEY"
Close all active and pending positions for a user. Each position is closed independently — partial failures don't block other positions.
bashcurl -X POST {{BASE_URL}}/api/v1/users/{userId}/positions/close-all \ -H "X-API-Key: $API_KEY"
Response:
json{ "data": { "successes": [ { "thesisId": "...", "cancelled": 2, "sellOrderCreated": true } ], "failures": [ { "thesisId": "...", "error": "No wallet found" } ] } }
Get auto-trade diary logs.
bashcurl "{{BASE_URL}}/api/v1/users/{userId}/diary-logs?page=1&pageSize=25" \ -H "X-API-Key: $API_KEY"
Rate limits are enforced across endpoints. Effective thresholds vary by endpoint and request context and may be adjusted over time.
Rate limit rejections return 429 Too Many Requests with:
error.code = "rate_limit_exceeded"Retry-After header (seconds)X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headersRetry behavior: back off and retry only after Retry-After elapses.
All list endpoints support pagination:
page - Page number (default: 1, max: 100)pageSize - Items per page (default: 25, max: 100)Response includes meta:
json{ "meta": { "page": 1, "pageSize": 25, "total": 100, "pages": 4 } }
| Status | Code | Description | |--------|------|-------------| | 400 | bad_request | Invalid input, missing fields, validation error | | 401 | unauthorized | Missing or invalid API key | | 404 | not_found | Resource not found | | 409 | error | Conflict (e.g., user already exists) | | 429 | rate_limit_exceeded | Rate limit exceeded | | 500 | internal_error | Unexpected server error |
Error response format:
json{ "error": { "code": "bad_request", "message": "Model \"gpt-5.2-high\" requires a Pro plan. Upgrade here: https://buy.stripe.com/...", "details": { "requestedModelVersion": "gpt-5.2-high", "requiredPlan": "pro", "requiredPlanLabel": "Pro", "upgradeUrl": "https://buy.stripe.com/...", "upgradeText": "Upgrade to Pro to use model \"gpt-5.2-high\"." } } }
Schema validation failures also return 400 bad_request, with error.details.validationIssues as a field-level issue list:
json{ "error": { "code": "bad_request", "message": "Missing required field: expiresAt", "details": { "validationIssues": [ { "target": "json", "path": "expiresAt", "code": "invalid_type", "message": "Required" } ] } } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-15 | fail→pass | 10,601 | 4,361 | -59% | 1 | 1 | 0% | 1,864 | 15,489 | +731% | 0 | 0 | — |
case-01 | fail→pass | 16,302 | 15,838 | -3% | 1 | 1 | 0% | 3,638 | 18,750 | +415% | 0 | 0 | — |
case-02 | fail→pass | 11,654 | 8,267 | -29% | 1 | 1 | 0% | 2,293 | 16,846 | +635% | 0 | 0 | — |
case-03 | fail→pass | 12,187 | 5,104 | -58% | 1 | 1 | 0% | 2,781 | 15,830 | +469% | 0 | 0 | — |
case-04 | fail→pass | 13,862 | 3,219 | -77% | 1 | 1 | 0% | 2,441 | 15,387 | +530% | 0 | 0 | — |
case-05 | fail→pass | 9,128 | 2,308 | -75% | 1 | 1 | 0% | 1,787 | 15,170 | +749% | 0 | 0 | — |
case-06 | fail→pass | 11,668 | 3,134 | -73% | 1 | 1 | 0% | 2,565 | 15,410 | +501% | 0 | 0 | — |
case-07 | fail→pass | 9,325 | 2,562 | -73% | 1 | 1 | 0% | 1,598 | 15,156 | +848% | 0 | 0 | — |
case-08 | fail→pass | 10,809 | 3,273 | -70% | 1 | 1 | 0% | 1,967 | 15,367 | +681% | 0 | 0 | — |
case-09 | fail→pass | 13,266 | 3,426 | -74% | 1 | 1 | 0% | 2,267 | 15,354 | +577% | 0 | 0 | — |
case-16 | fail→fail | 7,610 | 3,751 | -51% | 1 | 1 | 0% | 1,259 | 15,439 | +1126% | 0 | 0 | — |
case-10 | fail→pass | 7,917 | 2,944 | -63% | 1 | 1 | 0% | 1,412 | 15,306 | +984% | 0 | 0 | — |
case-11 | pass→pass | 10,729 | 4,521 | -58% | 1 | 1 | 0% | 2,279 | 15,634 | +586% | 0 | 0 | — |
case-12 | fail→pass | 6,519 | 3,366 | -48% | 1 | 1 | 0% | 1,201 | 15,364 | +1179% | 0 | 0 | — |
case-13 | fail→pass | 8,447 | 4,039 | -52% | 1 | 1 | 0% | 1,720 | 15,622 | +808% | 0 | 0 | — |
case-14 | fail→pass | 10,724 | 2,975 | -72% | 1 | 1 | 0% | 2,166 | 15,316 | +607% | 0 | 0 | — |
case-17 | fail→pass | 8,765 | 4,329 | -51% | 1 | 1 | 0% | 1,610 | 15,674 | +874% | 0 | 0 | — |
case-18 | fail→pass | 7,600 | 3,064 | -60% | 1 | 1 | 0% | 1,583 | 15,324 | +868% | 0 | 0 | — |
case-19 | fail→pass | 7,393 | 2,940 | -60% | 1 | 1 | 0% | 1,346 | 15,248 | +1033% | 0 | 0 | — |
case-20 | fail→pass | 9,528 | 2,768 | -71% | 1 | 1 | 0% | 1,828 | 15,208 | +732% | 0 | 0 | — |
case-21 | pass→fail | 11,078 | 8,436 | -24% | 1 | 1 | 0% | 2,390 | 16,590 | +594% | 0 | 0 | — |
case-22 | pass→pass | 10,654 | 5,752 | -46% | 1 | 1 | 0% | 2,199 | 16,080 | +631% | 0 | 0 | — |
case-23 | pass→pass | 8,488 | 5,835 | -31% | 1 | 1 | 0% | 2,137 | 16,062 | +652% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +74 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.