Recommend
Ask which tool works for a task and get them ranked by score, then price.
POST /v1/recommend tells your agent which tool to use for a task. You send one of the task types, or the task in plain language. You get back the tools for that task, ranked by score, each with the price of one passing result and the check that result must pass.
The call is free: it uses no credits. It does count against your plan's daily lookup quota. Nothing is run or bought here. To buy a result, send the tool_id you pick to execute.
It needs an agent key, sk_test_… or sk_live_…, in the Authorization header (see Authentication). Test keys see mock tools. Live keys see the real catalog. Without a key you can still read the catalog and scores, unranked, through Public data.
Arettic is pre-launch. Keys go to design partners; everyone else can join the waitlist. The @arettic/sdk (npm), arettic (PyPI) and @arettic/mcp packages used on this page are published at launch.
Call it
curl -s https://api.arettic.com/v1/recommend \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"task_type": "find_email",
"region": "US",
"constraints": { "max_price": 50, "min_score": 40 },
"sort": "score",
"limit": 5
}'This asks for find_email tools that serve the US, cost at most 50 credits per passing result and score at least 40, five at most. With a test key the only match is the mock finder:
{
"task_type": "find_email",
"region": "US",
"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"]
}
]
}options[0].tool_id is what you send to execute. Check purchasable first: ranking ignores it, so the top option can be a tool that is listed for information only (see Info-only tools).
Request
POST https://api.arettic.com/v1/recommend with a JSON body. Headers: Authorization: Bearer <agent key> and Content-Type: application/json. Every field is optional, except that you must send task_type or task.
| Field | Type | Default | What it does |
|---|---|---|---|
| task_type | string | none | One of the task types, listed below. Required unless you send task. When both are sent, task_type is used and task is ignored. |
| task | string | none | The task in plain language, up to 500 characters (longer text is cut). Arettic maps it to a task type and says which one it chose. Counts as a free-text lookup as well as a score lookup. |
| region | string | GLOBAL | Where the results are needed, for example US. Upper-cased. An empty string means GLOBAL. |
| constraints.max_price | number | none | Price cap in credits (1 credit = $0.001). Tools whose price per success is above it are left out. Tools with no price stay in. |
| constraints.min_score | number | none | Lowest score to include, on the 0 to 100 scale. Tools with no score yet are left out whenever you set it, even at 0. |
| sort | string | score | score, price, value or latency (see How tools are ranked). Anything else falls back to score. |
| limit | number | 10 | How many options to return, 1 to 50. Decimals are rounded down, 0 becomes 1, and more than 50 becomes 50. |
Numbers are checked strictly. constraints.max_price, constraints.min_score and limit must be non-negative numbers, or the call answers invalid_input with a message that names the field, for example "limit must be a non-negative number". A body that isn't a JSON object answers invalid_input too. An unknown sort is not an error: it falls back to score.
The task types: find_email, verify_email, enrich_company, enrich_person, web_search, extract_url. Send task_type when you know it. It skips the mapping step and the free-text quota.
Response
| Field | Meaning |
|---|---|
| task_type | The task type the options are for. |
| mapped_from_task | Only when you sent task: how the text was mapped, as from, confidence and alternatives. See Free-text tasks. |
| region | The region used, upper-cased. GLOBAL when you sent none. |
| sort | The sort that was applied. |
| test_mode | true for a test key. Test keys see mock tools only. |
| formula_version | The version of the public score formula behind every score. Today v1. |
| ranking | The ranking rule as a sentence, so an agent reading the JSON knows how the list was ordered. |
| options | The ranked tools, at most limit of them. Empty when no tool matches. |
Each option
| Field | Type | Meaning |
|---|---|---|
| tool_id | string | The id you send to execute as tool_id. |
| name | string | The tool's name. |
| provider | string | The provider's name. |
| score | number or null | The tool's score, 0 to 100, from the public formula. null when the tool has no score yet. |
| score_week | string or null | The Monday (UTC) of the week the score was computed for, as YYYY-MM-DD. |
| score_inputs | object or null | Every input to the score: A, S, P, R, L and D, each between 0 and 1. The table below says what they are. |
| sample_size | number | Data points behind the score: benchmark cases plus live calls in the last 28 days. 0 without a score. |
| price_per_success | object or null | The pay-as-you-go price of one passing result, as { credits, usd }, both strings (1 credit = $0.001). max_price is compared with this number. Pro and Max orgs pay less: GET /v1/tools/{id} shows prices_by_plan, and execute charges your plan's price. null when the tool has no price. |
| success_rate | number or null | The same as score_inputs.S: the share of calls that passed the check. |
| p50_latency_ms | number or null | Median latency in the tool's latest benchmark run, in milliseconds. null when it has none. |
| pass_rule | string | The check a result must pass before you are charged, as task_type@version, for example find_email@v1. See Pass rules. |
| purchasable | boolean | Whether execute can buy it through Arettic. Never changes the rank. |
| regions | string[] | The regions the tool serves, for example ["GLOBAL", "US"]. |
| Input | Meaning |
|---|---|
| A | Accuracy: the share of benchmark cases where a correct result was delivered. |
| S | Pass rate: benchmark and live calls in the last 28 days, weighted by volume. |
| P | Audit precision: the share of audited passes confirmed correct. Uses A until 50 audits exist. |
| R | Reliability: 1 minus the tool's error and timeout rate across its benchmark cases and live calls in the last 28 days. |
| L | Speed: the task's median latency divided by this tool's latency, capped at 1. |
| D | Upheld disputes divided by passed results. This one is a penalty. |
The formula, its weights and its rules are on How scores work and at GET https://api.arettic.com/v1/formula.
How tools are ranked
The default order is by score, highest first. Two scores within 2 points of each other count as a tie: the cheaper tool comes first, and at the same price the higher score. Tools with no score yet come after every scored tool, cheapest first.
Whether a tool can be bought through Arettic never changes its rank. It is a field, purchasable, not a factor. In the API's own tests, a tool one point below the top scorer but cheaper ranks first, and a tool 30 points below ranks last even though it is sold here.
Constraints are applied before sorting and limit after it. So sort: "price" with limit: 1 gives you the cheapest tool that meets your constraints.
| Sort | Order |
|---|---|
| score | The default. Highest score first, with the tie rule above. Tools with no score come last, cheapest first. |
| price | Cheapest price per success first. Tools with no price come last. Equal prices keep the score order. |
| value | Highest score divided by price in credits first. Tools with no score or no price come last. Equal values keep the score order. |
| latency | Lowest p50_latency_ms first. Tools with no latency come last. Equal latencies keep the score order. |
Info-only tools
A tool is purchasable when it is curated for sale here and has a price. Every other listed tool is there for information only: you see its score and where it ranks, but execute refuses it with not_purchasable (HTTP 409). Its price_per_success may be null.
So take the first option with purchasable: true, not options[0]. An info-only tool is still worth reading: it shows how the tool you buy compares with the rest of the market.
Regions
region says where the results are needed. It is upper-cased, so us and US are the same. Leave it out for GLOBAL.
A tool is listed when its regions include your region or GLOBAL. Its score is the region's own score when one exists, otherwise its GLOBAL score; score_week and score_inputs come from whichever was used. The response repeats the region in region.
A region with no tools of its own, or a value that isn't a region at all, gives you the tools that serve GLOBAL with their GLOBAL scores.
Free-text tasks
When you send task instead of task_type, Arettic maps the text to a task type with a keyword classifier. It is deterministic and calls no model: the same text always gives the same type. It costs one free-text lookup from your daily quota, nothing else.
How it works: the text is lower-cased, extra spaces are collapsed, and anything after 500 characters is dropped. Each task type has a few keyword patterns with weights, for example "find … email" for find_email, "bounce" for verify_email, "search the web" for web_search. The type with the highest total wins, as long as that total is at least 2. Otherwise the call answers unknown_task_type (HTTP 400): "Couldn't map that task to a task type. Send task_type as one of: …".
The response always says what it chose, in mapped_from_task:
| Field | Meaning |
|---|---|
| from | Your text, trimmed, up to 500 characters. |
| confidence | The winner's total divided by the winner's plus the runner-up's, rounded to 2 decimals. 1 when only one type matched. |
| alternatives | Up to two other task types that also matched, best first. Empty when nothing else matched. |
If the type is wrong, or confidence is low, send task_type explicitly. For example, this text matches two types and the runner-up may be the one you meant:
curl -s https://api.arettic.com/v1/recommend \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{ "task": "find the email and job title of the CTO at acme.com" }'{
"task_type": "enrich_person",
"mapped_from_task": {
"from": "find the email and job title of the CTO at acme.com",
"confidence": 0.57,
"alternatives": ["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-enrich-person",
"name": "Mock Person Enrichment",
"provider": "Arettic Mock Provider",
"score": 62.87,
"score_week": "2026-09-28",
"score_inputs": { "A": 0.5958, "S": 0.5958, "P": 0.5958, "R": 0.7225, "L": 1, "D": 0 },
"sample_size": 10,
"price_per_success": { "credits": "45", "usd": "0.045" },
"success_rate": 0.5958,
"p50_latency_ms": 5,
"pass_rule": "enrich_person@v1",
"purchasable": true,
"regions": ["GLOBAL", "US"]
}
]
}Phrases from the API's own tests and where they map:
| Task | Maps to |
|---|---|
| find the email of the marketing head at acme | find_email |
| check if these emails will bounce | verify_email |
| company size and industry for 100 US SaaS firms | enrich_company |
| get the job title and linkedin of the CMO | enrich_person |
| search the web for articles about sales tax compliance tools | web_search |
| scrape the text from https://acme.com/about | extract_url |
| make me a sandwich | Nothing: unknown_task_type |
Daily quotas
Recommend is free, but each org has a daily number of lookups that depends on its plan. All the org's agents share it, and it resets at 00:00 UTC. Every call that reaches the catalog counts one score lookup, whether or not any tool matches. A call with task also counts one free-text lookup. That one is taken before the text is mapped, so a task that can't be mapped still counts, though it takes no score lookup.
| Plan | Name | Score lookups a day | Free-text lookups a day |
|---|---|---|---|
| payg | Pay as you go | 1,000 | 100 |
| team | Pro | 10,000 | 500 |
| enterprise | Max | No fixed limit | No fixed limit |
When a quota is used up, the call answers HTTP 429 with the code plan_limit. The message says which limit it was. retryable is false, and the SDKs never retry recommend, so wait for the reset, or send task_type instead of task if it was the free-text quota that ran out.
{
"error": {
"code": "plan_limit",
"message": "Daily limit of 1000 score lookups reached for your plan. It resets at 00:00 UTC.",
"doc_url": "https://arettic.com/docs/errors#plan_limit",
"retryable": false
}
}The per-agent rate limit applies on top of the quota: 600 requests a minute for each agent key, reported in the RateLimit-* headers. See Rate limits and quotas.
Test keys and mock tools
With a test key (sk_test_…) the catalog is the mock provider: one tool per task type, ids starting with mock-, serving GLOBAL and US. They are benchmarked and scored like real tools, so constraints and sorts behave the same way. The response has test_mode: true. A live key never sees a mock tool, and a test key never sees a real one.
| Task type | Tool id | Name | Price per success (credits) |
|---|---|---|---|
| find_email | mock-find-email | Mock Email Finder | 38 |
| verify_email | mock-verify-email | Mock Email Verifier | 6 |
| enrich_company | mock-enrich-company | Mock Company Enrichment | 30 |
| enrich_person | mock-enrich-person | Mock Person Enrichment | 45 |
| web_search | mock-web-search | Mock Web Search | 8 |
| extract_url | mock-extract-url | Mock Page Extractor | 3 |
So {"task_type": "find_email", "constraints": {"max_price": 10}} returns no options with a test key: the mock finder costs 38 credits. With a live key you see only the tools that are switched on in the real catalog, so a task type can have no options until its providers are live. What the mock tools return from execute is on Test mode.
SDK and MCP examples
Both SDKs read ARETTIC_API_KEY (and ARETTIC_API_URL) from the environment. Neither retries recommend, because it is a POST without an idempotency key. A refused call throws (TypeScript) or raises (Python) AretticApiError with the API's code. See SDKs.
TypeScript
import { Arettic } from "@arettic/sdk";
// Reads ARETTIC_API_KEY (and ARETTIC_API_URL) from the environment when you leave them out.
const arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });
const rec = await arettic.recommend({
task_type: "find_email",
region: "US",
constraints: { max_price: 50, min_score: 40 },
sort: "score",
limit: 5,
});
// Ranking ignores whether a tool can be bought here, so take the first purchasable option.
const tool = rec.options.find((o) => o.purchasable);
if (!tool) throw new Error("No purchasable tool for " + rec.task_type + " in " + rec.region);
console.log(tool.tool_id, tool.score, tool.price_per_success?.credits, tool.pass_rule);
// mock-find-email 56.53 38 find_email@v1
// Or map plain language. The answer says which task type it chose.
const mapped = await arettic.recommend({ task: "find the work email of Emily Carter at example.com" });
console.log(mapped.task_type, mapped.mapped_from_task?.confidence); // find_email 1Python
from arettic import Arettic
client = Arettic() # reads ARETTIC_API_KEY (and ARETTIC_API_URL, if set)
rec = client.recommend(
task_type="find_email",
region="US",
constraints={"max_price": 50, "min_score": 40},
sort="score",
limit=5,
)
# Ranking ignores whether a tool can be bought here, so take the first purchasable option.
tool = next((o for o in rec["options"] if o["purchasable"]), None)
if tool is None:
raise SystemExit(f"No purchasable tool for {rec['task_type']} in {rec['region']}")
print(tool["tool_id"], tool["score"], tool["price_per_success"]["credits"], tool["pass_rule"])
# mock-find-email 56.53 38 find_email@v1
# Or map plain language. The answer says which task type it chose.
mapped = client.recommend(task="find the work email of Emily Carter at example.com")
print(mapped["task_type"], mapped["mapped_from_task"]["confidence"]) # find_email 1MCP
The MCP server's recommend tool takes the same fields with two differences: max_price and min_score are top-level arguments (there is no constraints object), and there is no limit, so you get up to 10 options. task is limited to 500 characters, region to 10, and min_score to 100. The result is the same JSON as above, as one text content block. A refused call comes back as a result with isError: true and the API's error body. Setup is on MCP server.
{
"name": "recommend",
"arguments": {
"task_type": "find_email",
"region": "US",
"max_price": 50,
"min_score": 40,
"sort": "score"
}
}Errors
| Code | HTTP | When |
|---|---|---|
| invalid_input | HTTP 400 | The body isn't a JSON object, neither task_type nor task was sent, or constraints.max_price, constraints.min_score or limit isn't a non-negative number. The message names the field. |
| unknown_task_type | HTTP 400 | task_type isn't one of the task types, or task couldn't be mapped to one. |
| unauthenticated | HTTP 401 | No agent key, or a revoked one. Only sk_test_… and sk_live_… keys work here; org keys (ok_…) don't. |
| plan_limit | HTTP 429 | The org's daily score-lookup or free-text quota is used up. It resets at 00:00 UTC. |
| rate_limited | HTTP 429 | Too many requests from this agent in a short time. Wait for the number of seconds in Retry-After. |
Every error is { "error": { "code", "message", "doc_url", "retryable" } }. doc_url points at the code's entry on Error codes.