Quickstart

Two ways in: connect the MCP server to an agent you already have, or call the API directly. Both use the same discovery, payment, verification, and result storage. Your agent can invoke tools directly; Frames F1 adds managed planning and answers.

All you need first is a Frames account — sign up at https://frames.ag. Every new account starts on the Free plan (3,000 credits/month, paid tools included, no card). Upgrading to a paid tier (Starter / Pro / Ultra) happens in Billing via Stripe Checkout. Runs are authorized against your plan and your credit balance; 1 credit = $0.001.

1. MCP — for an existing agent

No API keys to manage or configure. Add the Frames MCP server URL to your client (Claude, ChatGPT, Cursor, or any remote-MCP-capable host) and authenticate with your Frames account:

https://api.frames.ag/mcp

Your client registers itself and opens the Frames OAuth flow in the browser: sign in to your Frames account (if you aren't already) and click Approve. The connection is bound to your account's workspace, so every tool call runs under your plan, budget, and credit balance. No anonymous paid execution.

Start with frames_search_tools, inspect the input schema, then call frames_invoke_tools with an idempotency key and an explicit spending cap. Pass max_price_usd to search to exclude known prices above a per-tool ceiling. Hits marked price_status: "unknown" still need a probe; this search ceiling does not replace the invocation spending cap. Discovery and saved-result reads are free. Direct invocation bills each delivered call its price_usd plus the platform fee (a percentage of the price plus a fixed per-call fee); refunded calls bill nothing. The live terms and the max_usd sizing rule are in frames_get_usage → direct_invoke_pricing and in the frames_invoke_tools description — size max_usd to cover each call's full billed amount, not just its price_usd. F1 managed delegation also includes model costs. Use billing.charged_credits when reporting what was charged.

| Tool | Purpose | |---|---| | frames_run_capability | Delegate a task to Frames F1, which plans, buys data, and writes an answer. | | frames_get_run | Fetch a run (status, result, billing, usage, receipts) by id. | | frames_cancel_run | Request cancellation of a run belonging to your project. | | frames_get_receipt | Fetch the payment receipts (paid tool calls) for one run, plus what the run was billed. | | frames_get_tool_results | FREE — costs no credits. | | frames_get_usage | Fetch your project's credit balance, the share of the monthly grant still remaining, and open reserve holds. | | frames_search_tools | FREE — costs no credits. | | frames_get_tool | FREE — costs no credits. | | frames_probe_tools | FREE — costs no credits, never spends. | | frames_invoke_tools | PAID. |

invoke_ready: true means a live, payable tool with a usable input schema and a finite price quote. Liveness alone is insufficient. A missing readiness marker means inspect the descriptor and probe before building arguments. For variable-price tools, keep an explicit max_usd cap.

If an invocation returns status: "running", poll frames_get_run. For a body_ref, pass its artifact seq to frames_get_tool_results. Follow next_offset with offset until it is null; concatenate response.preview from each page and parse JSON when encoding is "json". Pages read the already-purchased artifact. They never invoke the seller again. The saved-body storage limit is 1,000,000 characters; larger seller bodies retain a truncation storage.truncated: true and a marker describing the original size. Finishing all pages does not recover discarded data. Offsets and limits count UTF-16 code units; concatenate page strings before interpreting split Unicode characters.

Headless agents (no browser)

Frameworks and scripts that can't drive interactive OAuth authenticate with a Frames API key directly — the same fk_… key the API uses, sent as a bearer on /mcp:

# any MCP client that supports static headers
Authorization: Bearer fk_YOUR_KEY

Copy-paste installs:

# Claude Code
claude mcp add --transport http frames https://api.frames.ag/mcp
# headless (API key, no browser):
claude mcp add --transport http frames https://api.frames.ag/mcp \
  --header "Authorization: Bearer fk_YOUR_KEY"
// Claude Desktop / Cursor (mcpServers config)
{ "mcpServers": { "frames": { "url": "https://api.frames.ag/mcp" } } }
# stdio-only clients — bridge with mcp-remote
npx mcp-remote https://api.frames.ag/mcp

Agents that don't speak MCP at all get the same discovery surface as plain REST — see “Tool discovery over REST” below.

2. Tool discovery over REST

The MCP discovery tools have REST twins (same auth as /v1, same JSON shapes) for function-calling agents in any framework:

curl -X POST https://api.frames.ag/v1/tools/search \
  -H "Authorization: Bearer fk_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"queries":["twitter user info","tweet history"]}'
# → { "search_id": "srch_…", "partial": false, "searches": [{ "query": "…", "status": "ok", "candidate_count": 10 }], "hits": [ { "id", "title", "payment", "invoke_ready", "price_usd", "input_schema", … } ] }

curl -X POST https://api.frames.ag/v1/tools/probe \ -H "Authorization: Bearer fk_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"ids":["frames.twitter.post.api-user-info"]}' ```

Machine-readable definitions: GET /v1/tools/spec. These schemas are generated from the same definitions used by MCP and REST validation. Replace {id} and {run_id} in paths with URL-encoded input values. Other GET inputs are query parameters; POST inputs are JSON. Apply query_defaults before explicit inputs. examples/tool-client.ts imports the live spec and includes invoke, waitForRun, and readResult helpers. invoke requires a spending cap and a stable idempotency key. Transient read failures and keyed invocation failures are retried at most twice. readResult returns {body, complete, storage}; check complete before treating the body as the full seller response. examples/tool-workflow.ts offers the same lifecycle helpers for MCP clients through a (toolName, args) => parsedResult adapter.

const client = await connectTools(apiKey);
const run = await client.invoke([{ id: toolId, args }], {
  maxUsd: 0.05, idempotencyKey: batchKey,
});
const done = await client.waitForRun(String(run.run_id));
// Inspect done.result.calls; for each body_ref:
const saved = await client.readResult(String(run.run_id), artifactSeq);
if (!saved.complete) console.log("The saved body reached its storage limit", saved.storage);

A polling timeout preserves the run ID. Resume polling that ID; keep the same batch key if retrying invocation. Approval and failed runs are returned for the caller to handle, never automatically reinvoked.

# Read the complete descriptor and your spending allowance.
curl https://api.frames.ag/v1/tools/frames.twitter.post.api-user-info \
  -H "Authorization: Bearer fk_YOUR_KEY"
curl https://api.frames.ag/v1/tools/usage \
  -H "Authorization: Bearer fk_YOUR_KEY"

Paid: replace the id and args with the contract returned by discovery.

curl -X POST https://api.frames.ag/v1/tools/invoke \ -H "Authorization: Bearer fk_YOUR_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: unique-logical-batch" \ -d '{"calls":[{"id":"TOOL_ID","args":{}}],"max_usd":0.05}'

Poll, inspect the saved-result index, then read one artifact page.

curl https://api.frames.ag/v1/runs/RUN_ID -H "Authorization: Bearer fk_YOUR_KEY" curl 'https://api.frames.ag/v1/runs/RUN_ID/tool-results?view=index' \ -H "Authorization: Bearer fk_YOUR_KEY" curl 'https://api.frames.ag/v1/runs/RUN_ID/tool-results?seq=0&offset=0&limit=60000' \ -H "Authorization: Bearer fk_YOUR_KEY" ```

Polling includes the same billing block as synchronous invocation once the run's charge is decided. It is absent while the run is still queued, executing or paused for approval, while settlement is pending, or if the ledger is unavailable; absence never means a zero charge. Keep polling until it is present; if status is requires_approval, polling alone never ends — approve or cancel the run, and the block appears once it is final. Unparameterized REST /tool-results reads retain the complete stored-artifact response for existing clients. A catalog outage returns a retryable error; a successful empty search means no matching tools were found.

3. Managed task execution — Frames F1

For programmatic access (your own code, OpenAI SDKs), create an API key in the dashboard under Developers → API access. Keys look like fk_… and the plaintext is shown once — store it.

Native: POST /v1/runs

curl https://api.frames.ag/v1/runs \
  -H "Authorization: Bearer fk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-unique-key-001" \
  -d '{
    "task": "Research 3 companies that may use x402, return evidence URLs",
    "budget": { "max_usd": 3 },
    "options": { "verification": "standard" }
  }'

Request fields (see src/run/create-run.ts):

Send Accept: text/event-stream to stream over SSE — answer tokens flush live as the model generates them (with tool/progress as reasoning deltas), not as a buffered dump. Fetch any run later with GET /v1/runs/:id (same auth; runs are project-scoped).

If your budget exceeds what the project's policy allows, the run is created paused and returned with HTTP 402 and status: "requires_approval" (no credits held). Resume the same run with POST /v1/runs/:id/approve { "max_usd": <number> } — the approved budget is still capped at your plan's per-run maximum — or drop it with POST /v1/runs/:id/cancel.

Other errors come back as a typed envelope { "error": { "type", "message", "code", "request_id" } } — notably 400 budget_too_small (with minimum_usd), 402 insufficient_credits and 403 no_subscription.

OpenAI-compatible: /v1/chat/completions and /v1/responses

Point an OpenAI SDK at the gateway and pass budget/verification in the frames extension field (see src/routes/openai-shim.ts). Using the Vercel AI SDK? See docs/ai-sdk.md — createOpenAICompatible + generateText / generateObject work against this surface as-is.

curl https://api.frames.ag/v1/chat/completions \
  -H "Authorization: Bearer fk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "frames-pro",
    "messages": [{ "role": "user", "content": "Find the current USDC supply on Base, with a source" }],
    "frames": { "budget_usd": 1, "verification": "standard" }
  }'

POST /v1/responses is the same shim over the Responses shape (input, instructions, text.format), with the same frames extension. Both surfaces return standard OpenAI usage plus frames_usage (the truth) and frames: { run_id, status, confidence, outcome? }, so you can always pull the full run (outcome.answers_task says whether a completed run actually answered the task). frames_usage.credits is what the run billed, from the ledger; it is null with settling: true while the charge has not settled yet (never a guessed 0) — poll GET /v1/runs/:id until it carries a billing block, then read billing.charged_credits (if status is requires_approval, approve or cancel the run first — the block appears once it is final). A final status (completed, failed, cancelled) alone is not enough: the block is omitted until settlement lands.

4. What you get back

Every run returns the answer and the accounting:

Project-level aggregates (balance, spend breakdown, undelivered-payment count) are in the dashboard, or GET /api/usage?project_id=… with a dashboard session.

Selecting a tool and checking an existing run

Search results are candidates. Check that the selected tool covers the requested subject and output: a topic-specific news feed is not general web search, public role inboxes are not verified person emails, and search snippets are not a research report. When description_truncated is true, fetch the full descriptor before choosing. invoke_ready confirms the callable contract, not task suitability.

To check an already-settled run through both production transports, including MCP reconnection and matching saved bodies, run:

FRAMES_API_KEY=... FRAMES_VERIFY_RUN_ID=... bun scripts/verify-tool-clients.mjs

This command reads an existing owned run and makes no new provider purchases.

Stopping work and inspecting evidence

Use frames_cancel_run({run_id}) over MCP or POST /v1/runs/:id/cancel over REST. An active run returns 202 with cancellation_requested: true. Poll until the run is terminal: the acknowledgement does not mean all work has stopped yet. New tool reservations stop; in-flight work finishes so payments and receipts remain accounted for. Completed work is billed within the original cap, and unused held credits are released. A paused run cancels immediately.

Managed runs without a caller-defined output schema include result.sources: links returned by successful tool calls, with tool_result_seq and an optional tool_id. These are provenance links, not a guarantee that every claim was independently verified. Raw provider bodies remain available through the result reader; model-facing HTML previews may omit markup and links.

Search hydrates the top five descriptors when descriptions are truncated or input schemas are missing, with a bounded two-second wait. Remaining truncated descriptions are marked explicitly. Review the output limitations and inputs before selecting a tool; search rank and invoke readiness do not establish that it satisfies every requirement.

Leading search hits also carry a bounded input_schema when the catalog has one. input_schema_source: "catalog" means advertised inputs; it does not establish liveness or invoke_ready. Oversized schemas are marked schema_oversized; fetch the full descriptor or probe before constructing arguments.

Search responses include searches for the executed angles (up to four): ok means candidates were retrieved, empty means that query returned none, partial means one catalog pool failed while another responded, and failed means the whole angle failed. candidate_count is before the gateway's price filtering. When partial is true, keep the surviving hits and retry the partial/failed angles before concluding that a required capability is missing. A total catalog outage remains a retryable error. Empty results do not prove that no suitable tool exists.

For a request such as finding and verifying a professional email:

  1. Search for person discovery and mailbox verification together using queries.
  2. Check both output scope and required inputs. A domain finder can supply an
  3. Probe the selected candidates. Allocate a cap to each step whose sum fits the
  4. Invoke discovery with a stable idempotency key, wait for completion, and read
  5. Report the verifier's actual status. Catch-all or unknown is not a confirmed

The same sequence works through connectTools() (REST) or an MCP callTool adapter with the helpers in examples/tool-workflow.ts. The executor regression suite tests this sequence with synthetic provider data; real provider coverage and delivery remain separate acceptance checks.