MCP server
Connect Claude, Cursor or any MCP client: the Streamable HTTP endpoint, the stdio bridge, and the nine tools.
Arettic is an MCP server. Claude Desktop, Claude Code, Cursor or your own agent can ask which tool works for a task, buy the result, and read the receipt through nine tools. This page covers the two ways to connect, the client configs, every tool with its arguments, a typical session, test mode, errors, and the server card.
Before launch, agent keys go to design partners; everyone else can join the waitlist. The @arettic/mcp npm package is published at launch. The hosted endpoint and the tools on this page are the ones the package will forward to.
What you need
An agent key. Test keys start with sk_test_ and are free: they buy from mock tools and use no credits. Live keys start with sk_live_ and buy real results with your org's credits. Org keys (ok_…) are for the org API and don't work here, because the MCP tools act as an agent.
To get a key, sign in to the dashboard, create an org, and add an agent in test mode. The key is shown once. The quickstart walks through it, and Authentication and keys explains the three kinds of credential.
Two ways to connect
The hosted server speaks Streamable HTTP at https://api.arettic.com/mcp. A client that supports Streamable HTTP with a custom header connects to it directly. A client that only runs local stdio servers uses the bridge, @arettic/mcp, a small Node program that forwards every tools/list and tools/call to the hosted server.
Both paths give the same tools with the same names, descriptions and schemas, because the bridge defines none of its own. Pick the hosted endpoint when you can send a header; pick the bridge when your client wants a command to run.
The hosted endpoint
Send JSON-RPC messages as POST https://api.arettic.com/mcp. Three headers matter:
Authorization: Bearer sk_test_…(orsk_live_…). Without it the answer is HTTP 401 with the error code unauthenticated. An org key is refused the same way.Content-Type: application/json.Accept: application/json, text/event-stream. The MCP transport requires both media types; without them the answer is HTTP 406.
The server is stateless: it builds a fresh session for every POST. There is no Mcp-Session-Id header, no initialize handshake is needed before tools/call, and every request carries the key. Responses are plain JSON, never a stream. GET /mcp and DELETE /mcp answer HTTP 405 with Allow: POST and the error code method_not_allowed, because there are no server-initiated streams and no sessions to end.
Each key can make 600 requests a minute to /mcp. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy; over the limit you get HTTP 429 with Retry-After. See Rate limits and quotas.
curl https://api.arettic.com/mcp \
-H "Authorization: Bearer $ARETTIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recommend","arguments":{"task_type":"find_email"}}}'{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"task_type\": \"find_email\",\n \"region\": \"GLOBAL\",\n \"sort\": \"score\",\n \"test_mode\": true,\n …\n}"
}
]
},
"jsonrpc": "2.0",
"id": 1
}Every tool answers with one text content item, and that text is JSON: the same body the matching REST endpoint returns. Parse it. When the API refused the call, the result also carries isError: true and the text is the API's error body (see Errors and retries). tools/list returns the nine tools with their JSON Schemas.
The stdio bridge
@arettic/mcp runs as a child process of your MCP client. It reads your key from the ARETTIC_API_KEY environment variable, connects to the hosted server, and forwards every tools/list and tools/call. It needs Node 18 or newer. The package is published to npm at launch; until then the command below has nothing to download.
ARETTIC_API_KEY=sk_test_… npx -y @arettic/mcp
| Variable | Required | Default | What it does |
|---|---|---|---|
| ARETTIC_API_KEY | yes | An agent key: sk_test_… or sk_live_…. | |
| ARETTIC_API_URL | no | https://api.arettic.com | The API host. The bridge talks to /mcp on it. |
If the key is missing, is an org key, or the API refuses it, the bridge writes one line to stderr saying what to do and exits with code 1. It never prints the key. If the API is merely unreachable at startup, the bridge still starts and retries on the first call. All logging goes to stderr; stdout carries only MCP messages.
Use the bridge from code
The package exports createStdioProxy, so you can embed the bridge in your own process or serve it on another MCP transport. Options: apiKey (required), baseUrl (default https://api.arettic.com), timeoutMs (default 5 minutes per request), log (a function; default stderr) and fetch.
import { createStdioProxy } from "@arettic/mcp";
const proxy = createStdioProxy({ apiKey: process.env.ARETTIC_API_KEY! });
await proxy.start(); // serves on stdin/stdout; pass any MCP Transport to serve elsewhereClient configs
Each block below carries a placeholder key; paste your own. The stdio blocks need Node on the machine that runs the client. A test key is the safe default: swap in a live key when you're ready to spend credits.
Claude Desktop
Open Settings, then Developer, then Edit Config, and add this to claude_desktop_config.json. Claude Desktop starts the bridge for you.
{
"mcpServers": {
"arettic": {
"command": "npx",
"args": [
"-y",
"@arettic/mcp"
],
"env": {
"ARETTIC_API_KEY": "sk_test_…"
}
}
}
}Claude Code
# The stdio bridge claude mcp add arettic --env ARETTIC_API_KEY=sk_test_… -- npx -y @arettic/mcp # Or the hosted endpoint directly claude mcp add --transport http arettic https://api.arettic.com/mcp --header "Authorization: Bearer sk_test_…"
Or commit a .mcp.json to your project with the same mcpServers block as the Claude Desktop config, so the whole team gets the server.
Cursor
Add the server to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project). Either the bridge:
{
"mcpServers": {
"arettic": {
"command": "npx",
"args": [
"-y",
"@arettic/mcp"
],
"env": {
"ARETTIC_API_KEY": "sk_test_…"
}
}
}
}or the hosted endpoint:
{
"mcpServers": {
"arettic": {
"url": "https://api.arettic.com/mcp",
"headers": {
"Authorization": "Bearer sk_test_…"
}
}
}
}Other clients
Any client that supports Streamable HTTP with a custom header can use https://api.arettic.com/mcp with Authorization: Bearer <agent key>; most accept a url plus headers block like the Cursor one. A client that can only launch local servers uses the command block: the bridge works with any host that starts stdio servers.
Call it from code
Your own agent can talk to the hosted endpoint with any MCP client library, or with plain HTTP. Both samples below run recommend, then execute, then fetch the receipt when there is one. If you'd rather skip MCP, the SDKs call the REST API directly.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://api.arettic.com/mcp"), {
requestInit: { headers: { Authorization: `Bearer ${process.env.ARETTIC_API_KEY}` } },
});
const mcp = new Client({ name: "my-agent", version: "1.0.0" });
await mcp.connect(transport);
// Every tool answers with one text item holding JSON.
const body = (r: { content?: unknown }) => JSON.parse((r.content as { text: string }[])[0].text);
const rec = await mcp.callTool({ name: "recommend", arguments: { task_type: "find_email" } });
const top = body(rec).options.find((o: { purchasable: boolean }) => o.purchasable);
const bought = await mcp.callTool({
name: "execute",
arguments: {
tool_id: top.tool_id,
input: { first_name: "Emily", last_name: "Carter", domain: "example.com" },
},
});
const outcome = body(bought);
if (bought.isError) throw new Error(`${outcome.error.code}: ${outcome.error.message}`);
console.log(outcome.status, outcome.result); // passed { email: "[email protected]", … }
if (outcome.receipt_id) {
// Live keys only: test purchases write no receipt.
const receipt = await mcp.callTool({
name: "get_receipt",
arguments: { receipt_id: outcome.receipt_id },
});
console.log(body(receipt));
}
await mcp.close();import json
import os
import urllib.request
API = "https://api.arettic.com"
KEY = os.environ["ARETTIC_API_KEY"]
def call_tool(name, arguments, rpc_id=1):
body = json.dumps({
"jsonrpc": "2.0",
"id": rpc_id,
"method": "tools/call",
"params": {"name": name, "arguments": arguments},
}).encode()
req = urllib.request.Request(
f"{API}/mcp",
data=body,
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=300) as res:
result = json.load(res)["result"]
data = json.loads(result["content"][0]["text"])
if result.get("isError"):
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
return data
rec = call_tool("recommend", {"task_type": "find_email"})
top = next(o for o in rec["options"] if o["purchasable"])
bought = call_tool("execute", {
"tool_id": top["tool_id"],
"input": {"first_name": "Emily", "last_name": "Carter", "domain": "example.com"},
}, rpc_id=2)
print(bought["status"], bought.get("result"))
if bought.get("receipt_id"): # live keys only
print(call_tool("get_receipt", {"receipt_id": bought["receipt_id"]}, rpc_id=3))In the Python sample an HTTP-level refusal (401, 405, 406, 429) raises urllib.error.HTTPError; a tool error comes back inside the result and raises the RuntimeError. The MCP SDK client behaves the same way: transport failures throw, tool errors come back as results with isError.
The nine tools
Tool names, argument names and descriptions come from the hosted server, and tools/list returns them with JSON Schemas. Each tool runs the same code as its REST endpoint, so its answer has the same fields; the pages linked below describe those fields one by one. Money is in credits: 1 credit is $0.001, and every amount comes back as { "credits": "38", "usd": "0.038" }.
| Tool | What it does | Same as |
|---|---|---|
| list_task_types | The task types, each with its pass rule, input and output. | GET /v1/task-types |
| recommend | Ranked tools for a task: score, every score input, price per success, pass rule. | POST /v1/recommend |
| get_tool | One tool in detail: scores by region, history, latest benchmark, schemas. | GET /v1/tools/{id} |
| execute | Run a tool (one input, or up to 1,000 as inputs; over 25 runs as a job); charged only if the result passes its check. Test keys use mock providers. | POST /v1/execute |
| get_job | Status and per-item results of a batch of more than 25 items. | GET /v1/jobs/{id} |
| get_receipt | The receipt for a purchase: cost, check, outcome and refund. | GET /v1/receipts/{id} |
| open_dispute | Dispute a charged item within 7 days (by receipt_id and item_index). Decided within 48 hours against the stored result; upheld means a refund. | POST /v1/disputes |
| get_approval | Status of a purchase waiting for approval. | GET /v1/approvals/{id} |
| get_balance | The agent's budget and the org's credit balance. | GET /v1/balance |
list_task_types
No arguments. Returns task_types, one entry per task type with task_type, pass_rule (for example find_email@v1), passes_if, input and output, plus the open-data license. The same list is on Pass rules.
recommend
Ranked tools for one task. Send task_type or a plain-language task. Ranking is by score, then price when scores are within 2 points; whether a tool can be bought never changes its rank, it's the purchasable field. Up to 10 options come back. Every field is described on Recommend.
| Argument | Type | Required | Notes |
|---|---|---|---|
| task_type | string | one of task_type or task | One of find_email, verify_email, enrich_company, enrich_person, web_search, extract_url. |
| task | string, up to 500 characters | one of task_type or task | Plain language, mapped to a task type by a keyword classifier. The answer says what it chose in mapped_from_task. If nothing matches, the error is unknown_task_type. Each free-text call uses one of your plan's daily free-text lookups. |
| region | string, up to 10 characters | no | A region code such as US; default GLOBAL. Tools that cover GLOBAL always match. |
| max_price | number | no | Most credits per success you'll accept. Costlier tools are left out. |
| min_score | number, 0 to 100 | no | Tools scoring below this are left out. |
| sort | string | no | score (default), price, value or latency. Within 2 points of score, price breaks the tie. |
{ "name": "recommend", "arguments": { "task_type": "find_email" } }{
"task_type": "find_email",
"region": "GLOBAL",
"sort": "score",
"test_mode": true,
"formula_version": "v1",
"ranking": "score, then price when scores are within 2 points; purchasability never changes rank",
"options": [
{
"tool_id": "mock-find-email",
"name": "Mock Email Finder",
"provider": "Arettic Mock Provider",
"score": 56.53,
"score_week": "2026-09-28",
"score_inputs": { "A": 0.4902, "S": 0.5958, "P": 0.4902, "R": 0.7225, "L": 1, "D": 0 },
"sample_size": 10,
"price_per_success": { "credits": "38", "usd": "0.038" },
"success_rate": 0.5958,
"p50_latency_ms": 5,
"pass_rule": "find_email@v1",
"purchasable": true,
"regions": ["GLOBAL", "US"]
}
]
}With a task instead of a task_type, the answer adds mapped_from_task with from, confidence and alternatives. A live key sees real tools here; a test key sees the mock ones.
get_tool
One tool in detail. The only argument is tool_id (a string of up to 80 characters: the id from recommend, such as mock-find-email). Returns tool (name, provider, task type, regions, purchasable, price_per_success, prices_by_plan, the pass_rule with its text, the input and output schemas, test_mode, live_calls_available), scores by region with every score input, score_history, and latest_benchmark. Unknown ids answer unknown_tool. See Public data and Scores.
execute
Buys a result. The tool is run, the result is checked against the task type's published pass rule, and you're charged only if it passes. The full flow, every status and every field are on Execute.
| Argument | Type | Required | Notes |
|---|---|---|---|
| tool_id | string, up to 80 characters | yes | From recommend. |
| input | object | one of input or inputs | One input in the task type's input shape (see list_task_types). |
| inputs | array of objects, up to 1,000 | one of input or inputs | Up to 25 run now and answer with per-item results. More than 25 answer status: "queued" with a job_id for get_job. With a test key every size runs now. |
| max_price | number | no | The most you'll pay in total, in credits. If the price (price per success times inputs) is above it, nothing runs: price_above_max. |
| fallback | boolean | no | If an item fails, try the next-ranked tool once. Both attempts go on the receipt. |
| approval_id | string, up to 40 characters | no | From a status: "approval_required" answer, once an owner has approved it. |
| idempotency_key | string, up to 200 characters | no | The same key with the same request returns the same answer and never charges twice. The bridge adds one (mcp-…) when you don't send one. |
{
"name": "execute",
"arguments": {
"tool_id": "mock-find-email",
"input": { "first_name": "Emily", "last_name": "Carter", "domain": "example.com" }
}
}{
"test_mode": true,
"tool_id": "mock-find-email",
"task_type": "find_email",
"check_version": "v1",
"charged": { "credits": "0", "usd": "0.000" },
"status": "passed",
"result": { "email": "[email protected]", "verification_status": "valid" },
"would_have_charged": { "credits": "38", "usd": "0.038" }
}{
"test_mode": true,
"tool_id": "mock-verify-email",
"task_type": "verify_email",
"check_version": "v1",
"charged": { "credits": "0", "usd": "0.000" },
"status": "failed",
"reason": "status:unknown",
"would_have_charged": { "credits": "0", "usd": "0.000" }
}The status of one input is passed, partial (charged pro rata, with a reason) or failed (a reason, no result, nothing charged). A live key also gets execution_id, receipt_id, refunded and the real charged amount. A batch of up to 25 answers status: "completed" with a summary and per-item items. Two answers ask you to come back later: status: "queued" (with job_id, items, max_charge, poll and a message) when the batch is over 25 items, and status: "approval_required" (with approval_id, reason, amount, expires_at and a message) when the purchase is over the agent's approval threshold or monthly budget.
get_job
The only argument is job_id. Returns job_id, status (queued, running, done, failed or expired), items, progress, created_at, started_at and finished_at. Once the job has its receipt the answer also carries receipt_id, tool_id, task_type, check_version, charged, refunded, summary and the per-item items with results. A job you didn't start answers not_found. See Batches and jobs.
get_receipt
The only argument is receipt_id. Returns the receipt: what was bought, from which provider, what it cost, the result's hash, the check and its version, the outcome and the refund, per item. Any agent of the org can read the org's receipts. Receipts never change; refunds from upheld disputes are added on top. Every field is on Receipts.
open_dispute
Disputes a charged item. Only charged items can be disputed, within 7 days of the purchase, one dispute per item. Arettic decides within 48 hours against the stored copy of the result; a dispute not decided in time is upheld, and an upheld dispute refunds the item's charge. Test keys can't dispute: there was no charge. See Disputes.
| Argument | Type | Required | Notes |
|---|---|---|---|
| receipt_id | string, up to 40 characters | yes | The receipt the item is on. |
| item_index | integer, 0 or more | no | Which item on the receipt. Default 0. |
| reason | string | yes | wrong_result, invalid_result, stale_result, duplicate_charge or other. |
| evidence | string, up to 4,000 characters | when reason is other | What's wrong with the result. |
Returns dispute_id, status (open at first), receipt_id, item_index, execution_id, tool_id, reason, evidence, charged, refunded, opened_at, decide_by and decided_at. An item that wasn't charged answers not_disputable; one older than 7 days answers dispute_window_closed.
get_approval
The only argument is approval_id, from an approval_required answer. Returns approval_id, status (pending, approved, rejected, expired or used), reason (over_threshold or over_budget), agent, tool, items, amount, expires_at, decided_at and decision_note. Approvals expire 24 hours after they're requested. Poll it until the status changes, then call execute again with the same tool_id and input plus the approval_id. See Budgets and approvals.
get_balance
No arguments. Returns the org's balance and the agent's monthly budget.
{
"org_balance": {
"paid": { "credits": "0", "usd": "0.000" },
"trial": { "credits": "0", "usd": "0.000" },
"total": { "credits": "0", "usd": "0.000" }
},
"agent_spent_month": { "credits": "0", "usd": "0.000" },
"agent_budget": { "credits": "50000", "usd": "50.000" },
"agent_budget_left": { "credits": "50000", "usd": "50.000" }
}A typical session
Three calls: recommend to pick a tool, execute to buy the result, get_receipt to see what happened. A test key covers the first two exactly as shown; the receipt needs a live key.
- Call
recommendwith the task type. Take the first option whosepurchasableis true and keep itstool_idandprice_per_success. - Call
executewith thattool_idand oneinput. Readstatus:passedgivesresultandcharged;failedgivesreasonand charges nothing. If the answer isapproval_required, pollget_approvaluntil it'sapproved, then callexecuteagain with theapproval_id. If it'squeued, pollget_jobwith thejob_id. - With a live key the answer includes
receipt_id. Callget_receiptwith it. The receipt andget_balanceagree: the balance drops by exactlycharged.
{ "name": "recommend", "arguments": { "task_type": "verify_email" } }{
"name": "execute",
"arguments": { "tool_id": "mock-verify-email", "input": { "email": "[email protected]" } }
}{
"test_mode": true,
"tool_id": "mock-verify-email",
"task_type": "verify_email",
"check_version": "v1",
"charged": { "credits": "0", "usd": "0.000" },
"status": "passed",
"result": { "email": "[email protected]", "status": "valid" },
"would_have_charged": { "credits": "6", "usd": "0.006" }
}With a live key, recommend ranks the real verify_email tools and execute charges the price of the one you chose. The answer then carries the ids you need for the receipt:
{
"status": "passed",
"execution_id": "0f6a9d2c-5b1e-4c7a-9e3d-2a8b7c6d5e4f",
"receipt_id": "7c2e4b9a-1d3f-4a5b-8c6d-9e0f1a2b3c4d",
"tool_id": "hunter-verify-email",
"task_type": "verify_email",
"check_version": "v1",
"charged": { "credits": "7", "usd": "0.007" },
"refunded": { "credits": "0", "usd": "0.000" },
"result": { "email": "[email protected]", "status": "valid" }
}{ "name": "get_receipt", "arguments": { "receipt_id": "7c2e4b9a-1d3f-4a5b-8c6d-9e0f1a2b3c4d" } }{
"receipt_id": "7c2e4b9a-1d3f-4a5b-8c6d-9e0f1a2b3c4d",
"created_at": "2026-09-29T10:15:07.412Z",
"tool_id": "hunter-verify-email",
"job_id": null,
"idempotency_key": "mcp-2b6d0f2e-0c4b-4a8e-9d1f-3c5e7a9b1d2f",
"request_hash": "…",
"check_version": "v1",
"charged": { "credits": "7", "usd": "0.007" },
"refunded": { "credits": "0", "usd": "0.000" },
"summary": { "items": 1, "passed": 1, "partial": 0, "failed": 0 },
"items": [
{
"index": 0,
"execution_id": "0f6a9d2c-5b1e-4c7a-9e3d-2a8b7c6d5e4f",
"tool_id": "hunter-verify-email",
"provider": "hunter",
"outcome": "captured",
"charged": { "credits": "7", "usd": "0.007" },
"refunded": { "credits": "0", "usd": "0.000" },
"pricing_mode": "per_success",
"input_hash": "…",
"result_hash": "…",
"check_version": "v1",
"settled_at": "2026-09-29T10:15:07.398Z"
}
]
}The three hashes are SHA-256 hex digests of the request, the item's input and the result, shortened here. The idempotency_key is the one the bridge generated; a call over the hosted endpoint without a key shows null there. Ids, times and prices are examples; live prices are on Pricing.
Test mode
With a test key, recommend and execute see the mock tools: one per task type, ids starting with mock-. No provider is called and no credits move. The answer carries test_mode: true, charged of zero, and would_have_charged: what a live purchase of the same result would have cost. The input decides the outcome, so you can rehearse both paths:
| Task type | Input | Outcome |
|---|---|---|
| any | any field contains provider-error | Provider error: status: "failed", reason: "provider_error", nothing charged. |
| any | any field contains nomatch | No result: fails. |
| find_email | domain ends in .catchall.test | Catch-all address: fails with reason: "catch_all". |
| verify_email | email starts with unknown | Unknown status: fails with reason: "status:unknown". |
| enrich_company | domain is missing-fields.test | Industry missing: fails. |
| enrich_person | company_domain contains nocontact | No contact field: fails. |
| web_search | query contains few results | 2 of 5 results: partial, charged pro rata. |
| extract_url | url contains blocked | Captcha page: fails. |
Everything else passes. For example, mock-verify-email passes [email protected] and fails [email protected]; mock-find-email returns [email protected] for Emily Carter at example.com. Batches work in test mode too and always run inline, up to 1,000 inputs, so a test key never gets a job_id. Test purchases write no receipt, so there is no receipt_id to look up, and open_dispute answers invalid_input ("Test-mode purchases are free; there's nothing to dispute"). Approvals only happen for live keys, so get_approval has nothing to show either.
To go live, create a live agent in the dashboard and swap the key. Nothing else changes. The full test-mode reference, including how mock tools are scored, is on Test mode.
Errors and retries
Tool errors
When the API refuses a call, the tool result has isError: true and its text is the API's error body: code, message, doc_url and retryable. doc_url points at the entry for that code on Error codes; retryable says whether trying again can help. A rate limit adds retry_after_seconds.
{
"error": {
"code": "unknown_task_type",
"message": "Couldn't map that task to a task type. Send task_type as one of: find_email, verify_email, enrich_company, enrich_person, web_search, extract_url",
"doc_url": "https://arettic.com/docs/errors#unknown_task_type",
"retryable": false
}
}The codes you'll meet most through MCP: invalid_input (a field is missing or malformed; the message names it), unknown_task_type, unknown_tool, not_found (a receipt, job or approval that isn't yours), insufficient_credits, over_budget, price_above_max, tool_paused, provider_error (nothing charged; retry or use fallback: true), request_in_progress (retry in a few seconds with the same idempotency_key) and trial_limit.
Protocol errors
A call to a tool that doesn't exist, or arguments that don't match the schema, is answered by the MCP layer rather than the API. The result still has isError: true, but its text is a plain sentence such as MCP error -32602: Tool nope not found or MCP error -32602: Input validation error: Invalid arguments for tool recommend: …. Nothing ran and nothing was charged; fix the call. The bridge passes these through unchanged.
HTTP errors
| Status | When | Body |
|---|---|---|
| 401 | No key, an org key, or a revoked key. | unauthenticated |
| 403 | The agent has an IP allowlist and this address isn't on it. | ip_not_allowed |
| 405 | GET or DELETE on /mcp. | method_not_allowed, with Allow: POST |
| 406 | The Accept header lacks application/json or text/event-stream. | A JSON-RPC error from the transport |
| 415 | The Content-Type header isn't application/json. | A JSON-RPC error from the transport |
| 429 | Over 600 requests a minute on this key. | rate_limited, with Retry-After |
What the bridge does for you
- If the API can't be reached, the tool result is
isError: truewith the code connection_failed. If the API doesn't answer within 5 minutes, the request is aborted and the code is timeout; the purchase may still be running, so it isn't retried. - If the connection drops during a call, the bridge reconnects and retries that call once. Reads are always retried.
- An
executesent without anidempotency_keygets one generated (mcp-…), so the retry replays the first purchase instead of buying twice. If the call still fails, the error body includes thatidempotency_keyso you can retry it yourself. open_disputeis retried only if the first attempt never reached the API, since a repeat would just report that the item already has a dispute.- A rate limit (429) isn't retried; the error carries
retry_after_seconds. - Send your own
idempotency_keyonexecuteto make your own retries safe too.
{
"error": {
"code": "connection_failed",
"message": "Couldn't reach the Arettic API at https://api.arettic.com (fetch failed). To retry without paying twice, call execute again with idempotency_key \"mcp-2b6d0f2e-0c4b-4a8e-9d1f-3c5e7a9b1d2f\".",
"retryable": true,
"idempotency_key": "mcp-2b6d0f2e-0c4b-4a8e-9d1f-3c5e7a9b1d2f"
}
}The server card
GET https://api.arettic.com/.well-known/mcp.json describes the server for clients and directories: the endpoints, how to authenticate, where to get a key, the tools, and where the docs and the OpenAPI document are. No key is needed, and it allows cross-origin reads.
curl https://api.arettic.com/.well-known/mcp.json
{
"name": "arettic",
"title": "Arettic",
"version": "0.3.0",
"description": "Ask which data tool works for a task, then buy the result through one key and pay only if it passes a published check.",
"endpoints": {
"streamable_http": "https://api.arettic.com/mcp",
"stdio": {
"command": "npx",
"args": [
"-y",
"@arettic/mcp"
],
"env": [
"ARETTIC_API_KEY"
],
"status": "published at launch"
}
},
"auth": {
"type": "bearer",
"header": "Authorization",
"format": "Bearer sk_test_… or sk_live_…",
"get_a_key": "https://arettic.com/docs"
},
"tools": [
{
"name": "list_task_types",
"status": "available",
"description": "The task types, each with its pass rule, input and output."
},
{
"name": "recommend",
"status": "available",
"description": "Ranked tools for a task: score, every score input, price per success, pass rule."
},
{
"name": "get_tool",
"status": "available",
"description": "One tool in detail: scores by region, history, latest benchmark, schemas."
},
{
"name": "execute",
"status": "available",
"description": "Run a tool (one input, or up to 1,000 as inputs; over 25 runs as a job); charged only if the result passes its check. Test keys use mock providers."
},
{
"name": "get_job",
"status": "available",
"description": "Status and per-item results of a batch of more than 25 items."
},
{
"name": "get_receipt",
"status": "available",
"description": "The receipt for a purchase: cost, check, outcome and refund."
},
{
"name": "open_dispute",
"status": "available",
"description": "Dispute a charged item within 7 days (by receipt_id and item_index). Decided within 48 hours against the stored result; upheld means a refund."
},
{
"name": "get_approval",
"status": "available",
"description": "Status of a purchase waiting for approval."
},
{
"name": "get_balance",
"status": "available",
"description": "The agent's budget and the org's credit balance."
}
],
"docs": "https://arettic.com/docs",
"openapi": "https://api.arettic.com/openapi.json"
}endpoints.stdio.status says published at launch until the npm package is out. version is the hosted server's version, the same one initialize reports in serverInfo.
Where next
- Quickstart: from zero to a receipt, including how to get a key.
- Test mode: the mock tools in full.
- Recommend and Execute: every field of the two calls that matter most.
- Receipts, Disputes, Batches and jobs and Budgets and approvals: the rest of the tools, field by field.
- SDKs: the same calls without MCP.
- Error codes: every code, what it means and what to do.