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.
| 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" } ] } } }
Other measured skills in the registry, with their headline benchmark lift.