Monetizable Keyterms
How it works
When a model reply mentions products — brands, product names, model numbers — Monetizable Keyterms turns those mentions into links you can monetize. Enable the toggle on a FreeRouter key and keep calling the same endpoint. FreeRouter:
- Strips
link_requestso the LLM gateway never sees it. - Waits for the LLM call to finish, then sends the assistant reply text (not the inbound messages) to a small extractor model — score reflects specificity: exact model number outranks product line, which outranks bare brand. Generic nouns never qualify.
- Fills the operator's destination template once per surviving candidate and attaches
keytermssorted by score, best first. On streams, emits one extra SSE event beforedata: [DONE].
link_request, a short reply, or an extractor failure never fails the LLM call. You still get the model response. Problems surface as keyterms_error, or as keyterms: [] when nothing specific appeared. If the LLM call itself fails (4xx/5xx), FreeRouter does not run extraction.
FreeRouter never rewrites the assistant text. Your app decides which terms to hyperlink before rendering, using the url on each candidate.
Set it up
- On API Keys, enable Monetizable Keyterms when creating a key or on an existing key. The checkbox is visible only when the operator has enabled the feature (
FR_KEYTERMS_ENABLED) and configured the extractor plus destination template. - Call inference as usual. Extraction runs automatically on every reply long enough to hold an entity.
The dashboard playground does not send link_request (playground runs are never logged). Use curl or your app to exercise keyterms — see the example below.
The link_request object
link_request is a sibling of the usual request body — it is not part of the OpenAI, OpenRouter, Anthropic, or Google shape. All four keys take the same object. Do not nest it inside messages or contents.
It is optional: omit it and FreeRouter uses defaults (up to 5 keyterms). Send it to cap the list.
| Field | Required | Description |
|---|---|---|
max_keyterms | No | Integer 1–10, default 5. Alias: maxKeyterms. |
Example — OpenAI shape
curl https://api.freerouter.com/v1/chat/completions \
-H "Authorization: Bearer $FREEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Best travel headphones?" }],
"link_request": { "max_keyterms": 3 }
}'
The same link_request object works on POST /v1/messages (Anthropic) and POST /v1/models/{model}:generateContent (Google).
The keyterms response
On success, the inference JSON gains a keyterms array as a sibling of choices (OpenAI / OpenRouter), the Anthropic envelope, or the Google envelope — sorted by score, best first.
{
"keyterms": [
{
"keyterm": "Sony WH-1000XM5",
"url": "https://xyz.example/go?term=Sony%20WH-1000XM5&pid=ws_123&frapi_uuid=fr_live_abc",
"occurrences": 2,
"score": 0.95
}
]
}
| Field | Description |
|---|---|
keyterm | The entity span, verbatim from the reply. Usually multi-word — hence keyterms, not keywords. |
url | Destination URL with the term filled in. Hyperlink this span to it. |
occurrences | How many times the span appears in the reply. Link the first occurrence, or all of them. |
score | 0–1 hyperlink-worthiness by specificity. Already sorted; the array is cut below 0.3. |
The keyterms_error response
Extraction problems never fail inference — they arrive as keyterms_error: { code, message } next to the model payload:
| Code | What it means |
|---|---|
invalid_link_request | link_request was not an object, or max_keyterms was not an integer. |
response_too_short | The reply is too short to hold an entity. Common on greetings and one-liners. |
destination_not_configured | The operator has not set the destination template. Nothing to link to yet. |
extractor_error | The extractor call failed, timed out, or is unconfigured. The model reply still succeeded. |
An empty list (keyterms: []) is not an error — the reply simply named nothing specific enough to link.
Streaming
Token chunks are unchanged. After the upstream stream finishes, FreeRouter emits one extra SSE event (data: {"keyterms":[…]} or data: {"keyterms_error":{…}}) then data: [DONE]. OpenAI-compatible SDKs that only read choices[].delta ignore it. Extraction runs after the full reply is known, so the trailing event arrives one extractor call after the last token.
Destination template
Each candidate url is filled from the operator's KEYTERM_DESTINATION_TEMPLATE. Three macros are replaced (URL-encoded); anything else in {braces} is left untouched:
| Macro | Filled with |
|---|---|
{keyterm} | The entity span. |
{the_pub_id} | The workspace UUID that owns the calling key — the publisher identity. |
{frapi_uuid} | The calling key's public prefix (fr_live_…), never its secret. |
Privacy
link_request carries no PII — just an optional cap. It is stripped before the body is forwarded to the LLM gateway and is not stored in request logs. The extractor sees only the assistant reply text; the extractor call itself is a normal inference request on the operator's key.
Troubleshooting
| What you see | What it means |
|---|---|
No keyterms or keyterms_error field | Monetizable Keyterms is off for that FreeRouter key (or the feature flag is off operator-side), or the LLM call failed before extraction. |
keyterms: [] | Extraction ran and nothing cleared the specificity cutoff. Hide hyperlinking for this reply. |
keyterms_error.response_too_short | Reply under ~40 characters. Expected on short answers. |
keyterms_error.extractor_error | The extractor model call failed or timed out. Inference still succeeded — retry the request or check operator config. |
| Generic words linked | Report it: the extractor prompt scores bare categories ~0, so generics in output mean the prompt or cutoff needs tuning (see the operator runbook). |