← All resources

How Pointcoin Added Web Search to Its AI in a Few Clicks

Pointcoin gives people free AI, paid for by ads. You send a prompt, two anonymous models answer side by side, and you earn rewards for picking the better response. The comparison is the product — the models stay unnamed so the vote is about the answer rather than the brand.

A chat product is only as current as whatever its models were trained on. Ask for this morning's forecast, last night's score, or what something costs today, and a model either hedges or answers from a snapshot that's months old. Most prompts don't care. The ones that do are the ones users notice immediately, and on a product where the whole interaction is judging an answer, a stale one is a bad round.

The usual fix is to build search into the app: pick a search API, write the tool definitions, run the tool loop, decide what happens when a search call fails, and keep all of that working as the model lineup changes. That's contained work when one model serves every request. Pointcoin answers each prompt with two models drawn from its routing rule, so app-side tools have to behave identically on whichever pair a turn lands on — and a vote is only fair if both sides had access to the same information.

Pointcoin already routed inference through FreeRouter, which meant the tools could go somewhere else: on the key. Turning on Parallel under Settings → MCP and enabling it on the key gave every model behind that key two tools — web_search and web_fetch — with no change to the request body and no app deploy.

Here's what that setup looks like, and the steps to do the same thing on your own key.

What Pointcoin runs

One FreeRouter key serves both sides of a turn. When a prompt needs something current, the model returns tool_calls, FreeRouter runs them against Parallel, feeds the results back to that same model on the same routing rule, and the finished answer comes back as an ordinary chat completion. Neither model is told anything the other wasn't offered.

flowchart LR
    U["User prompt"] --> App["Pointcoin app"]
    App --> Key["FreeRouter key: Parallel enabled"]
    Key --> A["Model A (anonymous)"]
    Key --> B["Model B (anonymous)"]
    A -.->|"tool_calls"| P["Parallel: web_search, web_fetch"]
    B -.->|"tool_calls"| P
    P -.-> Key
    A --> V["Two answers, user votes"]
    B --> V

The tools hang off the key, so both models in a turn are offered the same ones.

Why the tools sit on the key

1. Both models get the same shot at the question

Pointcoin's vote only means something if the two answers came from the same starting position. Tools attached to the key are offered on every request that key serves, so a turn is model A with search against model B with search — not whichever one the app happened to wire up.

It also survives changes to the lineup. Add a gateway, remap a model, change the split, and the tool list is still attached to the key rather than to any particular model host.

2. Enabling it was configuration

Two switches and a save: the server on in Settings → MCP, then Enable MCPs on the key. The next request picks it up. No new dependency in the app, no tool schema to maintain, no release to coordinate — the body Pointcoin sends to POST /v1/chat/completions is the same one it sent before.

3. A search problem stays a search problem

The tool loop fails open. If Parallel times out, rejects credentials, or is down, the request still returns a model reply — that turn just doesn't have search behind it. For a product where every prompt is a user waiting on two answers, a degraded answer beats a 5xx, and MCP never turns a working model call into one.

Setting it up

Five minutes in the dashboard. You need a FreeRouter workspace with a key that already serves traffic — if you're not there yet, Getting Started covers it.

  1. Settings → MCP. Open Settings and find the MCP section. Nothing is on by default. Turn on Parallel.
  2. Decide whose Parallel key to use. Parallel is optional BYOK: leave the field blank and FreeRouter's key handles it, or paste your own from parallel.ai to put usage on your own account. Either works. Some other servers — Monid, for one — are required BYOK and won't run at all until the workspace pastes a key.
  3. Save. Until you do, the key page will tell you to configure Settings first.
  4. API Keys → Enable MCPs. Each key opts in separately, so production can search while a staging key stays plain. Open the key's Extensions, click Enable MCPs, turn Parallel on, save.
  5. Check it in the Playground. Pick that key and ask something only the open web can answer. In Chat mode every tool call shows up inline as a chip — server, tool, duration, the arguments it sent, and a preview of what came back — which is the fastest way to confirm the model is actually reaching for search. Playground runs are never written to Logs.
  6. Optional: add a steering message. If you enable more than one server, a short steering message tells the model when to reach for which. It's appended to the system prompt on every request made with that key — workspace default under Settings, or per key. Your code still sends what it always sent.
  7. Call from your app. Same base URL, same key, same body. Don't add a tools array — tools you send yourself are yours to execute, and FreeRouter hands them straight back to you.

What the call looks like

Nothing about the request changes. This is an ordinary completion that happens to need today's data:

export FREEROUTER_API_KEY=fr_live_…
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": "What is the weather in Austin right now, and did the Longhorns win last night?" }
    ]
  }'

You get a normal chat completion back: { id, model, choices, usage }, with the answer in choices[0].message.content. There's no separate search object on the wire — the results were folded into the reply before it left. X-FreeRouter-Provider still names the gateway that served the model, same as any other request.

Streaming works too

A consumer chat product streams, and the tool loop runs on streamed requests on the OpenAI shapes. You receive one response: the turns are merged, so prose the model writes along the way ("let me check that") arrives in order while the tool calls FreeRouter ran stay hidden. Only the final turn carries a finish_reason, and there's exactly one data: [DONE].

// fr-search-stream.js — streamed completion with Parallel on the key.
// Run: FREEROUTER_API_KEY=fr_live_… node fr-search-stream.js
async function main() {
  const key = process.env.FREEROUTER_API_KEY;
  if (!key) throw new Error('Set FREEROUTER_API_KEY.');

  const res = await fetch('https://api.freerouter.com/v1/chat/completions', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'openai/gpt-4o-mini',
      stream: true,
      messages: [{ role: 'user', content: 'Who won last night, and what was the final score?' }],
    }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  console.log('provider', res.headers.get('x-freerouter-provider'));

  for await (const chunk of res.body) {
    for (const line of chunk.toString().split('\n')) {
      if (!line.startsWith('data: ')) continue;
      const payload = line.slice(6).trim();
      if (payload === '[DONE]') continue;
      const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
      if (delta) process.stdout.write(delta);
    }
  }
  process.stdout.write('\n');
}

main().catch((err) => { console.error(err.message); process.exit(1); });

The one shape that still can't stream at all is google — call :generateContent without it, or use an openai-shaped key for the same model.

flowchart TD
    S1["Settings → MCP: turn on Parallel"] --> S2["Own key or FreeRouter's"]
    S2 --> S3["Save"]
    S3 --> S4["API Keys → Enable MCPs → Parallel"]
    S4 --> S5["Playground: Chat mode, watch the tool chip"]
    S5 --> S6{"Did the model call web_search?"}
    S6 -->|yes| S7["Ship it — app body unchanged"]
    S6 -->|no| S8["Tool-capable model, or add a steering message"]

Workspace switch, key switch, one check in the Playground.

What the same switch gets you next

Web search is the broad one, and it already covers weather, scores, and prices. The catalog also carries dedicated weather and finance servers, and Monid for third-party data endpoints where each endpoint you enable becomes its own tool. Same two switches, same unchanged request body. New servers show up under Settings → MCP and stay off until you opt in.

Because the catalog is attached to the key, none of it is re-done when routing changes. Pointcoin can add a gateway, remap a model, or shift its split, and the search tools ride along — which is the same reason a key in front of one gateway is worth having early. For the Settings screens in more detail, the web search how-to walks through them.

Limits and failure modes

  • The model has to decide to search. FreeRouter offers the tools; it doesn't force a call. A weak tool-user, or a prompt the open web can't answer, gets a normal reply from memory. Use a tool-capable model and check in the Playground before blaming the config.
  • Results are as of this request. A score, a temperature, or a price is whatever the search returned on that call. It isn't a live feed or an official source, and two turns a minute apart can disagree.
  • Search costs a round trip. A turn that calls web_search waits on the search before the model finishes writing. It only applies to turns where the model reaches for it, but it is real latency on exactly the prompts users are watching.
  • FreeRouter doesn't judge the answers. Attaching tools doesn't rank the two models, score the responses, or decide the vote — that stays Pointcoin's product. Search changes what the models know, not how they're compared.
  • Catalog servers only. You can't point the workspace at an arbitrary MCP URL, and command / npx (stdio) servers aren't supported. Remote HTTP and SSE servers from the FreeRouter catalog are what's on offer.
  • Required-BYOK servers stay off until you paste a key. Parallel is optional BYOK, so it runs either way. A required one with no workspace key is simply left out of that request's tool list; everything else still runs.
  • Your own tools are never executed by us. If your client runs its own MCP servers or tool loop, those definitions ride the request untouched and their tool_calls come back for you to handle. FreeRouter only runs tools from servers attached to the key.

Next steps

Try the result for yourself at pointcoin.app — ask both models something that happened today and see which one handles it better.

To do the same on your own key: get a FreeRouter key, turn on Parallel under Settings → MCP, enable it on the key, and send the curl above.

Route your first request today.

Bring your own keys and start routing in minutes.

Get your key