Monetizable Keyterms

Scored, linkable entities on the inference response. Enable Monetizable Keyterms on a FreeRouter key, optionally send a link_request sibling (the same object on every API shape), and read keyterms or keyterms_error next to the model payload.

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:

  1. Strips link_request so the LLM gateway never sees it.
  2. 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.
  3. Fills the operator's destination template once per surviving candidate and attaches keyterms sorted by score, best first. On streams, emits one extra SSE event before data: [DONE].
Inference always wins. A missing 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

  1. 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.
  2. 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.

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.

FieldRequiredDescription
max_keytermsNoInteger 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
    }
  ]
}
FieldDescription
keytermThe entity span, verbatim from the reply. Usually multi-word — hence keyterms, not keywords.
urlDestination URL with the term filled in. Hyperlink this span to it.
occurrencesHow many times the span appears in the reply. Link the first occurrence, or all of them.
score0–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:

CodeWhat it means
invalid_link_requestlink_request was not an object, or max_keyterms was not an integer.
response_too_shortThe reply is too short to hold an entity. Common on greetings and one-liners.
destination_not_configuredThe operator has not set the destination template. Nothing to link to yet.
extractor_errorThe 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:

MacroFilled 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 seeWhat it means
No keyterms or keyterms_error fieldMonetizable 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_shortReply under ~40 characters. Expected on short answers.
keyterms_error.extractor_errorThe extractor model call failed or timed out. Inference still succeeded — retry the request or check operator config.
Generic words linkedReport it: the extractor prompt scores bare categories ~0, so generics in output mean the prompt or cutoff needs tuning (see the operator runbook).
Next API Reference