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

POST/v1/chat/completions

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?"
      }
    ]
  }'

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
curl "https://keyfo.xyz/v1/tools/stock_quote?ticker=NVDA" \
  -H "Authorization: Bearer YOUR_KEYFO_KEY"
Over MCP, your agent calls the tools itself.

Streaming

Set stream to true to get the reply as server-sent events. The stream ends with data: [DONE].

cURL
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?"
      }
    ]
  }'
Response
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.

Request
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
curl -X POST https://keyfo.xyz/api/trial
200 OK
{
  "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
curl https://keyfo.xyz/v1/key \
  -H "Authorization: Bearer YOUR_KEYFO_KEY"
200 OK
{
  "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

Example models with ID and context size
ModelidContext
GPT-4.1 Miniopenai/gpt-4.1-mini
GPT-6.1 Terra Proopenai/gpt-6.1-terra-pro
GPT-6.1 Terraopenai/gpt-6.1-terra
Claude Haiku 5.5anthropic/claude-haiku-5.5
Claude Sonnet 5.5anthropic/claude-sonnet-5.5
Gemini Omni 1.2 Flashgoogle/gemini-omni-1.2-flash
Gemini Omni 1.1 Flashgoogle/gemini-omni-1.1-flash
Grok 4.7x-ai/grok-4.7
Grok 4.6x-ai/grok-4.6
DeepSeek V4.1 Flashdeepseek/deepseek-v4.1-flash
DeepSeek V4 Pro 0423deepseek/deepseek-v4-pro
Qwen3.8 Max Primeqwen/qwen3.8-max-prime

—

cURL
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"

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
curl "https://keyfo.xyz/v1/tools/stock_quote?ticker=NVDA" \
  -H "Authorization: Bearer YOUR_KEYFO_KEY"
200 OK
{
  "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
curl -G https://keyfo.xyz/v1/tools/stock_portfolio \
  --data-urlencode wallet=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
  -H "Authorization: Bearer YOUR_KEYFO_KEY"
200 OK
{
  "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
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
  }'
200 OK
{
  "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
  • avalanche
  • linea
  • scroll
  • zksync
  • mode
  • zora
  • mantle
  • unichain
  • 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
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"
  }'
200 OK
{
  "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
curl https://keyfo.xyz/v1/stocks
200 OK
{
  "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.

  1. Take each item of calls in the order given. A sale may start with an approval.
  2. Check valid_until. If it has passed, build the trade again.
  3. Sign and send it from the wallet address on Robinhood Chain (chain ID 4663), with to, data and value as returned, and gas when present.
  4. Wait for its receipt before the next one. An approval has to confirm before the swap.
  5. 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.

sign.mts
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.

agent.mts
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.

Error codes, HTTP status and what to do
codeHTTPmeaning
API and MCP
bad_request400A 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_route400LI.FI has no route for this trade right now. Change the amount or retry later.
no_holding400The wallet holds none of this stock token, so there is nothing to sell.
insufficient_eth400The wallet holds less ETH than the buy needs with gas. The message gives both amounts. Fund it with fund_agent.
invalid_api_key401 REST403 MCPKey missing, wrong, revoked or expired. Check it or create a new one in the app.
key_limit_reached402Today’s allowance is used up. It resets at 00:00 UTC, see resets_at.
insufficient_balance402Your allowance for today can’t cover this model call. Tool calls are free and still work.
model_not_allowed403Trial keys use low-cost models only. See GET /v1/models with your key.
model_not_found404Unknown model ID. See GET /v1/models.
not_found404No such stock token, key or page.
tool_not_found404No tool with that name. Use stock_quote, stock_portfolio, build_trade or fund_agent.
method_not_allowed405This route doesn’t accept that HTTP method. Check GET or POST in the docs.
rate_limit_exceeded429Too 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_error502The model provider or a data source returned an error. Retry, or pick another model.
upstream_unavailable503A provider or route is busy, rate-limited or down. Retry after the Retry-After time.
service_unavailable503Models are unavailable on keyfo right now. Tool calls still work.
App
not_launched403Wallet sign-in and full keys open when $KEYFO launches. A trial key works now.
sign_in_required401Sign in with your wallet first.
nonce_expired401The sign-in request expired after 10 minutes. Start again.
bad_signature401The signature can’t be read or doesn’t match the sign-in message.
limit_reached409A wallet can have up to 10 active keys. Revoke one first.
402
{
  "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.

robinhood.com/us/en/crypto/chain