REST API
Base URL: https://api.deploytalon.com/api/v1 (local: http://localhost:4000/api/v1). OpenAPI JSON at /api/v1/openapi.json, Swagger UI at /api/v1/docs.
Authentication
TALON accounts are wallet only. A user signs in by connecting an EVM wallet and signing a Sign-In With Ethereum (EIP-4361) message. There is no email signup, magic link, password, or social login. The connected wallet is the owner of that user's agents; each agent still has its own server wallet.
Credentials, checked in order:
Authorization: Bearer tlk_live_...ortlk_test_...API key (created on/developersor by the CLI device flow). Keys are SHA-256 hashed at rest.- A better-auth session from wallet sign-in: the
better-auth.session_tokencookie (the dashboard), or the session token asAuthorization: Bearer <token>. X-Talon-Internal-Token+X-Talon-User-Id(service-to-service, used by the Telegram bot).
Every request is scoped to the caller's user. Agents belong to users; a request for an agent the user does not own returns 404.
Cookie sessions and CSRF: a POST, PUT, PATCH, or DELETE authenticated by the session cookie (no Authorization header) must send Origin: <WEB_URL> (or <API_BASE_URL>) and a JSON or multipart body. Anything else, such as a cross-site form or a text/plain body, is 403 FORBIDDEN. API keys and bearer session tokens are not affected. The session cookie is SameSite=Lax when SESSION_COOKIE_SAMESITE=lax (recommended: serve the API on the same site as the dashboard) and SameSite=None; Secure otherwise in production.
Wallet sign-in
better-auth is mounted at /api/auth/* on the API service (https://api.deploytalon.com/api/auth, local http://localhost:4000/api/auth). Both routes are POST (a GET returns 404), send Origin: <WEB_URL>, and are limited to 20 requests per minute per IP. The client IP comes from the trusted proxy (TRUSTED_PROXY_HOPS entries from the right of X-Forwarded-For, or CLIENT_IP_HEADER), never from the client-written left of the header. Both responses carry X-Server-Time (ms since epoch); build Issued At and Expiration Time from it so a device with a wrong clock can still sign in.
| Step | Method | Path | Body | Returns |
|---|---|---|---|---|
| 1 | POST | /api/auth/siwe/nonce | {} (or no body) | { nonce }: 24 alphanumeric characters, single use, valid 15 minutes |
| 2 | wallet | personal_sign | the EIP-4361 message below | 0x... signature |
| 3 | POST | /api/auth/siwe/verify | { message, signature } (no other fields) | { token, success: true, user: { id, walletAddress, chainId } } and a Set-Cookie session |
The message (build it with viem createSiweMessage) must contain:
| Field | Requirement |
|---|---|
| domain | SIWE_DOMAIN, default the host of WEB_URL (for example www.deploytalon.com, or localhost:3000 in dev) |
| address | the signing wallet |
| URI | any URL on the WEB_URL origin |
| Version | 1 |
| Chain ID | the wallet's chain; smart contract wallets must use Robinhood Chain (4663 in production; 4663 or 46630 in development) |
| Nonce | the value from step 1 |
| Issued At | now, by the server clock (X-Server-Time); rejected if more than 60 s in the future or older than 10 minutes at verify time |
| Expiration Time | required, in the future (5 minutes is typical) |
| Not Before | optional, honored |
www.deploytalon.com wants you to sign in with your Ethereum account:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Sign in to TALON.
URI: https://www.deploytalon.com/login
Version: 1
Chain ID: 4663
Nonce: Q7mZk2PxT9wLrV4aH3nB8cDe
Issued At: 2026-09-23T12:00:00.000Z
Expiration Time: 2026-09-23T12:05:00.000Z
Any failure is 401 with a code: UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE (unknown or reused nonce), UNAUTHORIZED_SIWE_MESSAGE_EXPIRED, UNAUTHORIZED_SIWE_MESSAGE_MISMATCH (domain, address, or chain ID the plugin could not accept), SIWE_CLOCK_SKEW (Issued At outside the window), SIWE_DOMAIN_MISMATCH (domain or URI origin), SIWE_SMART_WALLET_CHAIN (not an EOA signature and not on an accepted smart wallet chain, or a smart wallet registered on another chain), SIWE_BAD_SIGNATURE, or SIWE_INVALID_MESSAGE. A nonce is consumed on the first verify attempt, even a failed one; request a new one to retry. EOA signatures are verified locally. On Robinhood Chain, ERC-1271 and ERC-6492 smart wallet signatures are verified through the chain RPC (in production only on the production chain); on other chains only EOA signatures are accepted.
The first sign-in creates the user and records the wallet as the primary (owner) wallet. Signing in later with the same EOA address, on any chain, reaches the same user. A smart wallet is bound to the chain it registered on: the same contract address on another chain may have a different controller, so that sign-in is refused. Leftover better-auth routes (email, password, social, update-user, and similar) return 404. GET /api/auth/get-session returns the current session and POST /api/auth/sign-out ends it. The web client uses better-auth's siweClient() plugin, which calls these same routes.
API keys and the CLI
API keys work exactly as before and are unaffected by wallet sign-in. The CLI gets one through a device-code flow: the CLI starts it, the user approves the code on WEB_URL/device while signed in with their wallet, and the CLI receives a normal tlk_live_ key. Endpoints are listed under Auth: device flow.
Conventions
- JSON in, JSON out. Errors:
{ "error": { "code": "POLICY_DENIED", "message": "...", "details": {} } }. - Amounts are strings in human units unless the field ends in
WeiorRaw. - Write endpoints accept
Idempotency-Key. A replay returns the original response. - Rate limit: 120 req/min per key (
X-RateLimit-*headers). Wallet sign-in (/api/auth/siwe/*): 20 req/min per IP. - Sensitive actions may return
202 { "approval": { id, status: "pending" } }instead of a result. PollGET /approvals/:idor resolve it from the dashboard or Telegram.
Tools endpoint (canonical)
POST /tools/:name with { "agentId": "...", "input": { ... } } runs any tool from packages/tools (see docs/tools.md). Response { "ok": true, "result": ... } or 202 with an approval. GET /tools lists tools with their JSON schemas. Everything below is an alias that maps 1:1 onto a tool.
Agents
| Method | Path | Tool | Notes |
|---|---|---|---|
| GET | /agents | list the user's agents | |
| POST | /agents | { name, avatarUrl?, modelProvider, model, systemPrompt?, chainId? } creates the agent and its own server wallet. The default policy whitelists the owner wallet for transfers (withdrawals), which still require approval | |
| GET | /agents/:id | profile, wallet address, full policy and policySummary (served from the DB, same data as agent_get) | |
| PATCH | /agents/:id | name, avatar, model, systemPrompt, status | |
| DELETE | /agents/:id | archives (wallet is retained) | |
| GET | /agents/:id/policy | full policy JSON | |
| PUT | /agents/:id/policy | replace policy (validated with the zod schema) | |
| GET | /agents/:id/balances | wallet_balances | |
| GET | /agents/:id/portfolio | wallet_portfolio | |
| GET | /agents/:id/history | wallet_history | ?limit= |
| POST | /agents/:id/transfer | wallet_transfer | sensitive |
| POST | /agents/:id/chat | streaming chat with the agent runtime (SSE, Vercel AI SDK data stream) | |
| GET | /agents/:id/messages | chat history |
Market
| Method | Path | Tool |
|---|---|---|
| GET | /tokens/:addressOrSymbol | token_lookup |
| GET | /tokens/:address/price | token_price |
| GET | /tokens/:address/chart?interval=5m&limit=200 | token_chart |
| GET | /tokens/:address/trades?limit= | token_trades when registered, else { ok: true, result: { trades: [] } } |
| GET | /market/trending?window=1h | market_trending |
| GET | /market/new?limit=50"e=ETH | market_new_launches |
| GET | /market/stocks | stock_tokens_list |
Market endpoints are public (no auth) and cached for 5 to 15 seconds.
Trading
| Method | Path | Tool |
|---|---|---|
| POST | /agents/:id/swap/quote | swap_quote |
| POST | /agents/:id/swap | swap_execute |
| POST | /agents/:id/curve/buy | curve_buy ({ quoteOnly: true } runs curve_quote with side: "buy") |
| POST | /agents/:id/curve/sell | curve_sell ({ quoteOnly: true } runs curve_quote with side: "sell") |
| POST | /agents/:id/approve | approve_token |
Launch and earn
| Method | Path | Tool |
|---|---|---|
| POST | /launch/quote | launch_quote |
| POST | /agents/:id/launch | launch_token (sensitive) |
| GET | /agents/:id/earnings | earnings_get |
| POST | /agents/:id/earnings/claim | earnings_claim |
| POST | /uploads/image | multipart image upload to IPFS, returns { cid, url } |
Automations
| Method | Path | Tool |
|---|---|---|
| GET | /agents/:id/automations | automation_list |
| POST | /agents/:id/automations | automation_create |
| DELETE | /agents/:id/automations/:automationId | automation_cancel |
Approvals (user side)
| Method | Path | Tool |
|---|---|---|
| GET | /approvals?status=pending | approval_list |
| GET | /approvals/:id | |
| POST | /approvals/:id/resolve { decision: "approve" | "deny" } | approval_resolve (responds 200 { approval, result }; if execution fails the mapped error is returned with the approval marked) |
Developer
| Method | Path |
|---|---|
| GET | /keys |
| POST | /keys { name, env } returns the plaintext key once |
| DELETE | /keys/:id |
| GET | /me returns { id, walletAddress, chainId, name, image, createdAt, telegramLinked, user, agentCount, telegram }. walletAddress is the owner wallet (EIP-55 checksummed, null only for accounts created before wallet sign-in) |
| POST | /telegram/link returns { code, deepLink }; the code is valid for 10 minutes |
Internal (Telegram bot, X-Talon-Internal-Token only):
| Method | Path |
|---|---|
| POST | /internal/telegram/claim { code, chatId } links the chat to the code's user; returns { ok, userId, chatId, user: { id, name, walletAddress, chainId } } |
| GET | /internal/telegram/users/:chatId returns { userId, user: { id, name, walletAddress, chainId }, agents } |
| GET | /internal/telegram/chat-for-user/:userId returns { chatId: string | null } |
Any other route can be called on behalf of a user with X-Talon-Internal-Token plus X-Talon-User-Id.
Auth: device flow
Wallet sign-in lives at /api/auth/siwe/* (see Authentication). The CLI uses a device-code flow implemented by the API (Redis backed, 10 minute codes) rather than the better-auth plugin, because it must end in an API key:
| Method | Path | Body | Returns |
|---|---|---|---|
| POST | /api/v1/auth/device/code | { clientId? } | { deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval } |
| POST | /api/v1/auth/device/approve | { userCode, decision? } (wallet session, from WEB_URL/device) | { ok: true, status: "approved" } |
| POST | /api/v1/auth/device/token | { deviceCode } (poll every interval s) | { apiKey, keyId, prefix, userId } or 400 { error: { code: "authorization_pending" | "expired_token" | "access_denied" } } |
The same routes are aliased under /api/auth/device/*.
Webhooks and events
GET /agents/:id/events?since= returns the audit log (approval created, tx confirmed, launch, graduation of a token the agent created).