keyfo docs
One API key for AI models and Robinhood Chain tools. Start with the quickstart, then connect over MCP or REST.
Quickstart
Get a key, make your first call, then add the tools to your agent. About a minute end to end.
Get a key
A trial key needs no sign-in. It goes into every code sample on this page.
Key added to every code sample.—
Copy it now. Only this tab keeps it. Closing the tab removes it from this browser, but the key works until it expires.
POST/v1/chat/completions
pip install openai
npm install openai
curl https://keyfo.xyz/v1/chat/completions \ -H "Authorization: Bearer YOUR_KEYFO_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4.1-mini", "messages": [ { "role": "user", "content": "In one sentence, what is a stock token?" } ] }'
import os from openai import OpenAI client = OpenAI(base_url="https://keyfo.xyz/v1", api_key=os.environ.get("KEYFO_KEY", "YOUR_KEYFO_KEY")) r = client.chat.completions.create( model="openai/gpt-4.1-mini", messages=[{"role": "user", "content": "In one sentence, what is a stock token?"}], ) print(r.choices[0].message.content)
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://keyfo.xyz/v1", apiKey: process.env.KEYFO_KEY ?? "YOUR_KEYFO_KEY" }); const r = await client.chat.completions.create({ model: "openai/gpt-4.1-mini", messages: [{ role: "user", content: "In one sentence, what is a stock token?" }], }); console.log(r.choices[0].message.content);
Add the tools
The chat endpoint doesn’t call keyfo tools, so the model can’t see prices. One stock_quote call returns a stock token’s pool price, 24h change and pool depth.
GET/v1/tools/stock_quote
curl "https://keyfo.xyz/v1/tools/stock_quote?ticker=NVDA" \ -H "Authorization: Bearer YOUR_KEYFO_KEY"
Streaming
Set stream to true to get the reply as server-sent events. The stream ends with data: [DONE].
curl https://keyfo.xyz/v1/chat/completions \ -H "Authorization: Bearer YOUR_KEYFO_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4.1-mini", "stream": true, "messages": [ { "role": "user", "content": "In one sentence, what is a stock token?" } ] }'
data: {"id": "kf-1793359030-UCNVXSuIHo27", "object": "chat.completion.chunk", "created": 1793359030, "model": "openai/gpt-4.1-mini", "choices": [{"index": 0, "delta": {"content": "A", "role": "assistant"}, "finish_reason": null}]}
data: {"id": "kf-1793359030-UCNVXSuIHo27", "object": "chat.completion.chunk", "created": 1793359030, "model": "openai/gpt-4.1-mini", "choices": [{"index": 0, "delta": {"content": " stock", "role": "assistant"}, "finish_reason": null}]}
data: [DONE]Authentication
Send your key in the Authorization header or as X-API-Key. MCP clients that take only a URL can pass it as ?key=. Keep the key on your server.
Authorization: Bearer YOUR_KEYFO_KEY # MCP clients that take only a URL: https://keyfo.xyz/mcp?key=YOUR_KEYFO_KEY
Trial keys
A trial key needs no sign-in. It draws from a free daily allowance for each visitor, expires after 24 hours and works with low-cost models only. Replies are capped at 512 tokens. GET /v1/models with a trial key lists the models it can use.
With a trial key, send images as data:image/… URLs. Links need your own key.
POST/api/trial
curl -X POST https://keyfo.xyz/api/trial
{
"key": "YOUR_KEYFO_KEY",
"id": "hG7kQ2mXp9Tz",
"allowance": {
"type": "free",
"period": "daily",
"remaining": true,
"resets_at": 1793433600
},
"expires_at": 1793424125,
"max_tokens": 512,
"example_model": "openai/gpt-4.1-mini",
"models_count": 173,
"models": [
"openai/gpt-4.1-mini",
"anthropic/claude-haiku-5.5",
"deepseek/deepseek-v4.1-flash-1017"
],
"mcp_url": "https://keyfo.xyz/mcp"
}Allowance
Tool calls are free and need no allowance. Model calls draw from a daily allowance: every visitor gets a free one with a trial key, and a wallet key gets one in tiers by the $KEYFO in its wallet. All reset at 00:00 UTC. You do not set limits yourself.
Until $KEYFO launches, only trial keys can call models. Full keys open with the launch.
Check a key
GET /v1/key shows a key’s allowance. A trial key returns an allowance with type free, period daily, remaining and resets_at, reset at 00:00 UTC. A wallet key returns its holder allowance. Calls still running are already subtracted. It also gives expires_at and max_tokens.
GET/v1/key
curl https://keyfo.xyz/v1/key \
-H "Authorization: Bearer YOUR_KEYFO_KEY"{
"object": "key",
"trial": true,
"allowance": {
"type": "free",
"period": "daily",
"remaining": true,
"resets_at": 1793433600
},
"expires_at": 1793424125,
"max_tokens": 512
}Models
Models run through an OpenAI-compatible API. Point any OpenAI SDK at keyfo and pick a model by its ID.
GET/v1/models
| Model | id | Context |
|---|---|---|
| GPT-4.1 Mini | openai/gpt-4.1-mini | |
| GPT-6.1 Terra Pro | openai/gpt-6.1-terra-pro | |
| GPT-6.1 Terra | openai/gpt-6.1-terra | |
| anthropic/claude-haiku-5.5 | ||
| anthropic/claude-sonnet-5.5 | ||
| google/gemini-omni-1.2-flash | ||
| google/gemini-omni-1.1-flash | ||
| Grok 4.7 | x-ai/grok-4.7 | |
| Grok 4.6 | x-ai/grok-4.6 | |
| deepseek/deepseek-v4.1-flash | ||
| deepseek/deepseek-v4-pro | ||
| qwen/qwen3.8-max-prime |
Couldn’t load the model list.Try again
—
curl https://keyfo.xyz/v1/models
MCP
One MCP server gives your agent all four tools. Add it to Claude Code, Cursor or VS Code with your key in the header.
HTTP/mcp
claude mcp add -s user --transport http keyfo https://keyfo.xyz/mcp \
--header "Authorization: Bearer YOUR_KEYFO_KEY"{
"mcpServers": {
"keyfo": {
"url": "https://keyfo.xyz/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEYFO_KEY"
}
}
}
}{
"servers": {
"keyfo": {
"type": "http",
"url": "https://keyfo.xyz/mcp",
"headers": {
"Authorization": "Bearer YOUR_KEYFO_KEY"
}
}
}
}curl "https://keyfo.xyz/v1/tools/stock_quote?ticker=NVDA" \ -H "Authorization: Bearer YOUR_KEYFO_KEY"
New key? Run claude mcp remove keyfo -s user first. Check with claude mcp list: keyfo shows Connected.
Tools
Four tools for Robinhood Chain. Same names and inputs over MCP and REST.
Every tool takes GET with query parameters or POST with a JSON body.
REST wraps the result in data. Over MCP, a tool returns the same object without the wrapper.
GET · POST/v1/tools/<name>
stock_quote
Returns price, 24h change and pool depth for a stock token on Robinhood Chain.
GET · POST/v1/tools/stock_quote?ticker=NVDA
tickerstringRequired
Ticker of a stock token or ETF, like NVDA. A token address works too.
stock_quote over REST works without a key.
Stock tokens are not available to US persons and are restricted in Canada, the UK, Switzerland and other countries. This service shows data and builds transactions. It does not sell stock tokens.
curl "https://keyfo.xyz/v1/tools/stock_quote?ticker=NVDA" \ -H "Authorization: Bearer YOUR_KEYFO_KEY"
{
"data": {
"ticker": "NVDA",
"name": "NVIDIA",
"type": "stock",
"token": "0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec",
"price_usd": 210.127,
"change_24h_pct": -1.89,
"depth_usd": 35229786,
"logo": "https://cdn.jsdelivr.net/…/NVDA.png",
"chain_id": 4663,
"as_of": 1793318233,
"price_note": null,
"note": "Stock tokens are not available to US persons…"
}
}stock_portfolio
Returns every stock token and ETF in an agent wallet with amounts and value in dollars, plus its ETH balance.
GET · POST/v1/tools/stock_portfolio?wallet=0xd8dA…6045
walletstringRequired
Wallet address on Robinhood Chain.
curl -G https://keyfo.xyz/v1/tools/stock_portfolio \ --data-urlencode wallet=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \ -H "Authorization: Bearer YOUR_KEYFO_KEY"
{
"data": {
"wallet": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
"chain_id": 4663,
"holdings": [
{
"ticker": "NVDA",
"name": "NVIDIA",
"token": "0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec",
"amount": 0.57243038,
"amount_raw": "572430380000000000",
"amount_dec": "0.57243038",
"price_usd": 210.127,
"price_note": null,
"value_usd": 120.28,
"change_24h_pct": -1.89,
"logo": "https://cdn.jsdelivr.net/…/NVDA.png"
}
],
"holdings_count": 1,
"priced_count": 1,
"unpriced_count": 0,
"stocks_value_usd": 120.28,
"unpriced": 0,
"eth": 0.05,
"eth_raw": "50000000000000000",
"eth_dec": "0.05",
"eth_value_usd": 112.54,
"as_of": 1793318233,
"note": "Stock tokens are not available to US persons…"
}
}amount_raw and eth_raw are exact whole numbers in the smallest unit. amount_dec and eth_dec are the same amounts as exact decimals. Use them for arithmetic.
price_note is null when a price is known, otherwise no_liquidity or unavailable.
build_trade
Returns an unsigned buy of a stock token for ETH, sized in
ETH or in dollars, or a sale of a percentage of a position for
ETH. Your agent signs and sends it.
A buy needs the ETH, plus gas, already in the wallet. If it is short, fund it with fund_agent first.
GET · POST/v1/tools/build_trade
unsigned · agent signsThis transaction is unsigned. Your agent signs it with private keys it holds itself.A sale may first need a token approval. Your agent signs that transaction too.
See Sign and send for how your agent submits these calls.
Trades route through LI.FI.
tokenstringRequired
Ticker, like TSLA, or the token address.
sideenumRequired
buy or sell.
walletstringRequired
The wallet that signs and sends. The route is quoted for it.
usdnumberBuy: eth or usd
Dollars of ETH to spend, at the current
ETH price.
ethnumberBuy: eth or usd
ETH to spend on a buy, up to 10.
pctnumberSell: required
Share of the holding to sell, above 0 and up to 100.
slippage_pctnumberOptional
Price tolerance in percent: 1 by default, from 0.1 to 10.
curl -X POST https://keyfo.xyz/v1/tools/build_trade \ -H "Authorization: Bearer YOUR_KEYFO_KEY" \ -H "Content-Type: application/json" \ -d '{ "token": "TSLA", "side": "buy", "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "usd": 50 }'
{
"data": {
"chain_id": 4663,
"side": "buy",
"ticker": "TSLA",
"token": "0x322f0929c4625ed5bad873c95208d54e1c003b2d",
"wallet": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
"amount_in": 0.022301,
"amount_in_raw": "22300621295309287",
"amount_in_usd": 50.05,
"expected_out": 0.126576,
"expected_out_raw": "126576214763029118",
"min_out": 0.125311,
"min_out_raw": "125310452615398827",
"expected_out_usd": 49.92,
"fees_usd": 0.1251,
"quoted_at": 1793318233,
"valid_until": 1793318278,
"valid_for_s": 45,
"calls": [
{
"label": "Buy TSLA",
"to": "0xB477751B76CF82d00a686A1232f5fCD772414Af3",
"data": "0x736eac0b…",
"value": "22300621295309287",
"gas": 669595
}
],
"how_to_sign": "Send each call in order from the wallet address…",
"simulated": true,
"note": "Stock tokens are not available to US persons…"
}
}valid_until is the time the quoted route expires; quoted_at is when it was quoted. Send the calls before valid_until, or build the trade again.
simulated is true when the calls passed a test on the network just before the reply, and false when that test could not run. If it is false, build the trade again or check the calls yourself before signing.
amount_in_raw, expected_out_raw and min_out_raw are exact whole numbers in the token’s smallest unit, as strings. Use them for arithmetic.
fund_agent
Returns a quote and ready transactions that fund an agent wallet on Robinhood Chain from another network, such as Ethereum, Base, Arbitrum, Optimism, BNB Chain or Polygon. Funds arrive as ETH. The payer signs.
GET · POST/v1/tools/fund_agent
origin_chain accepts these networks:
ethereum
base
arbitrum
optimism
bnb
polygon
avalanchelinea
scroll
zksync
mode
zora
mantleunichain
ink
walletstringRequired
Agent wallet on Robinhood Chain that receives the ETH.
payerstringRequired
Wallet that pays on the origin network and signs there. Usually not the agent wallet.
origin_chainstring or numberRequired
Network to pay from: a name, like base, or its chain ID, like 8453.
amountstring or numberRequired
Amount in the smallest unit of what you pay with, like wei for ETH.
currencystringOptional
Token address to pay with on the origin network. Empty pays in its native coin.
curl -X POST https://keyfo.xyz/v1/tools/fund_agent \ -H "Authorization: Bearer YOUR_KEYFO_KEY" \ -H "Content-Type: application/json" \ -d '{ "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "payer": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "origin_chain": "base", "amount": "1000000000000000" }'
{
"data": {
"origin_chain_id": 8453,
"destination_chain_id": 4663,
"recipient": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
"payer": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f",
"amount_in": "0.001",
"amount_out_eth": "0.000990057421606488",
"eta_s": 1,
"transactions": [
{
"step": "deposit",
"action": "Confirm transaction in your wallet",
"chain_id": 8453,
"to": "0x4cd00e387622c35bddb9b4c962c136462338bc31",
"data": "0x49290c1c…",
"value": "1000000000000000"
}
],
"how_to_sign": "The payer signs and sends each transaction on the origin chain."
}
}Stock token list
GET /v1/stocks returns stock tokens and ETFs on Robinhood Chain with pool price, 24h change and pool depth, deepest pool first. No key needed. Unknown values are null, never 0.
GET/v1/stocks
curl https://keyfo.xyz/v1/stocks
{
"data": [
{
"ticker": "NVDA",
"name": "NVIDIA",
"type": "stock",
"token": "0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec",
"price_usd": 209.062,
"change_24h_pct": -2.03,
"depth_usd": 14871922,
"logo": "https://cdn.jsdelivr.net/…/NVDA.png",
"price_note": null
}
],
"count": 64,
"chain_id": 4663,
"as_of": 1793344758,
"note": "Stock tokens are not available to US persons…"
}Prompts
keyfo publishes ready-made tasks as MCP prompts. Your client lists them with prompts/list and sends one with prompts/get. A prompt only writes the request: it calls nothing itself.
- buy_stock
- Quotes a stock token, builds an unsigned buy and waits for your approval before anything is signed. Arguments: ticker, usd, wallet.
- portfolio_checkup
- Reads a wallet’s stock tokens and quotes every position. When issuer data is available, it adds upcoming dividends and splits. Argument: wallet.
- rebalance
- Reads the portfolio and builds the unsigned trades that move it toward a target mix, such as 50% SPY and 50% QQQ. Arguments: wallet, target.
- fund_my_agent
- Quotes moving funds from another network to the agent wallet and returns the transactions the payer signs. Arguments: wallet, payer, from_chain, amount.
- market_now
- Shows today’s stock movers and quotes SPY and QQQ. Movers rank by the pool’s 24h change, and by issuer data when it is available.
- usdg_vault
- Compares Morpho
USDG vaults by rate, size and curator, names the risks and builds an unsigned deposit. Arguments: wallet, amount. - cheapest_model
- Finds the lowest-cost model that can do a task and shows the cost of a typical call. Argument: task.
- check_approvals
- Lists a wallet’s open approvals to the spenders keyfo knows and builds unsigned revokes for the ones you pick. Argument: wallet.
- dividends_ahead
- Lists scheduled dividends and splits, for one wallet’s holdings or for all stock tokens. Argument: wallet, optional.
Resources
Reference data your agent can read without a tool call: the stock list, the model catalog and how signing works. Read it with resources/list and resources/read.
Sign and send
build_trade and fund_agent return unsigned transactions. Your agent’s wallet signs and sends them, one by one.
- Take each item of
callsin the order given. A sale may start with an approval. - Check
valid_until. If it has passed, build the trade again. - Sign and send it from the wallet address on Robinhood Chain (chain ID 4663), with
to,dataandvalueas returned, andgaswhen present. - Wait for its receipt before the next one. An approval has to confirm before the swap.
- The last receipt is the trade. Read the new balance with stock_portfolio.
fund_agent is different: the payer signs its transactions on the origin network, not on Robinhood Chain.
Example with viem. Any wallet library works.
import { createWalletClient, createPublicClient, http, defineChain } from "viem"; import { privateKeyToAccount } from "viem/accounts"; const chain = defineChain({ id: 4663, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } }, }); const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`); const wallet = createWalletClient({ account, chain, transport: http() }); const client = createPublicClient({ chain, transport: http() }); const res = await fetch("https://keyfo.xyz/v1/tools/build_trade", { method: "POST", headers: { Authorization: "Bearer YOUR_KEYFO_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ token: "TSLA", side: "buy", wallet: account.address, usd: 50, }), }); const body = await res.json(); if (!res.ok) throw new Error(body.error.message); const trade = body.data; if (Date.now() / 1000 > trade.valid_until) { throw new Error("Quote expired: build the trade again"); } for (const c of trade.calls) { const hash = await wallet.sendTransaction({ to: c.to, data: c.data, value: BigInt(c.value), gas: c.gas ? BigInt(c.gas) : undefined, }); const receipt = await client.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error(c.label); }
Run it with npx tsx sign.mts (Node 22 or newer) after npm install viem.
Chat with tools
Give a model the keyfo tools in one chat request, then run each tool call it makes on /v1/tools/{name} and send the result back. Your agent loops until the model answers.
Example with the OpenAI SDK, keyfo as base URL.
import OpenAI from "openai"; const BASE = "https://keyfo.xyz"; const KEY = process.env.KEYFO_KEY ?? "YOUR_KEYFO_KEY"; const client = new OpenAI({ baseURL: `${BASE}/v1`, apiKey: KEY }); const list = await fetch(`${BASE}/mcp`, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), }).then((r) => r.json()); const tools = list.result.tools.map((t) => ({ type: "function" as const, function: { name: t.name, description: t.description, parameters: t.inputSchema, }, })); const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: "user", content: "What is NVDA trading at on Robinhood Chain?" }, ]; while (true) { const r = await client.chat.completions.create({ model: "openai/gpt-4.1-mini", messages, tools, }); const msg = r.choices[0].message; messages.push(msg); if (!msg.tool_calls?.length) { console.log(msg.content); break; } for (const call of msg.tool_calls) { const url = `${BASE}/v1/tools/${call.function.name}`; const res = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", }, body: call.function.arguments, }); const content = await res.text(); messages.push({ role: "tool", tool_call_id: call.id, content }); } }
Run it with npx tsx agent.mts (Node 22 or newer) after npm install openai.
Errors
Each error says what went wrong and what to do next.
| code | HTTP | meaning |
|---|---|---|
| API and MCP | ||
| bad_request | 400 | A parameter is missing or invalid, a message part is a file, or the body is too large (413). The message names it. LI.FI and Relay refusals add lifi_code or relay_code. |
| no_route | 400 | LI.FI has no route for this trade right now. Change the amount or retry later. |
| no_holding | 400 | The wallet holds none of this stock token, so there is nothing to sell. |
| insufficient_eth | 400 | The wallet holds less |
| invalid_api_key | 401 REST403 MCP | Key missing, wrong, revoked or expired. Check it or create a new one in the app. |
| key_limit_reached | 402 | Today’s allowance is used up. It resets at 00:00 UTC, see resets_at. |
| insufficient_balance | 402 | Your allowance for today can’t cover this model call. Tool calls are free and still work. |
| model_not_allowed | 403 | Trial keys use low-cost models only. See GET /v1/models with your key. |
| model_not_found | 404 | Unknown model ID. See GET /v1/models. |
| not_found | 404 | No such stock token, key or page. |
| tool_not_found | 404 | No tool with that name. Use stock_quote, stock_portfolio, build_trade or fund_agent. |
| method_not_allowed | 405 | This route doesn’t accept that HTTP method. Check GET or POST in the docs. |
| rate_limit_exceeded | 429 | Too many requests: new trade quotes per key or visitor (see scope), trial keys (3 per visitor in 24 hours, plus a cap for all visitors) or sign-in requests (20 a minute). See Retry-After. |
| upstream_error | 502 | The model provider or a data source returned an error. Retry, or pick another model. |
| upstream_unavailable | 503 | A provider or route is busy, rate-limited or down. Retry after the Retry-After time. |
| service_unavailable | 503 | Models are unavailable on keyfo right now. Tool calls still work. |
| App | ||
| not_launched | 403 | Wallet sign-in and full keys open when $KEYFO launches. A trial key works now. |
| sign_in_required | 401 | Sign in with your wallet first. |
| nonce_expired | 401 | The sign-in request expired after 10 minutes. Start again. |
| bad_signature | 401 | The signature can’t be read or doesn’t match the sign-in message. |
| limit_reached | 409 | A wallet can have up to 10 active keys. Revoke one first. |
{
"error": {
"code": "key_limit_reached",
"message": "This key reached its daily limit. It resets at 00:00 UTC.",
"resets_at": 1793404800
}
}Robinhood Chain
The keyfo tools work on Robinhood Chain, a “permissionless, AI-native Layer 2… built for financial services and real-world assets”, as Robinhood describes it. Its infrastructure comes from Arbitrum.