Star on GitHub

Search the web through your agent.

OpenAI-compatible API for live web search, URL scraping, and cited LLM summaries. Drop-in base_url replacement โ€” change one URL, send model="agentic-search", get cited answers. Or paste the SKILL.md into any agent and let it search natively.

OpenAI Compatible
Multi-Provider Search Backend
Zero Setup Skill
22s Max Latency
1Copy the SKILL.md and paste it into any AI agent
2Your agent instantly knows how to search, scrape, and cite the web โ€” zero setup
Try it now
Copy instructions for my agent
Works with every agent
โœฑClaude Code
โŒฌCodex
โ—†OpenCode
โ–ฒAider
โ—Harvey
โ—‡Hermes
+ any OpenAI-compatible agent
Copy the SKILL.md into your agent โ€” it learns the API in one paste.
---
name: agentic-search-api
description: |
  Use when an agent needs live web search, URL scraping/extraction, or
  cited research from the open web. Backed by an OpenAI-compatible chat
  endpoint that auto-routes to web search + URL scrape + LLM summary.
  Triggers: "search the web", "look up X", "find the latest on", "what
  changed in", "current events", "scrape this URL", "extract content
  from ", "cite sources for", "find videos/images/news about",
  "summarize https://...". Returns OpenAI-shape responses plus
  \`search_results[]\` and \`agentic_search.{path, answer_model,
  duration_seconds, balance_before, usage.steps[]}\` extensions.
version: 1.0.0
license: MIT
allowed-tools:
  - Bash
  - Read
  - WebFetch
author: Traylinx 
metadata:
  hermes:
    tags: [Search, Web, Scraping, Research, Chat, OpenAI-Compatible, Citations, Traylinx]
    related_skills: [duckduckgo-search, browse, sherlock, polymarket]
    category: research
    upstream: https://github.com/traylinx/agentic-search-engines-ts
    production_url: https://agentic-search-engines-ts.netlify.app
---

# agentic-search-api

OpenAI-compatible API that does **live web search + URL scraping + cited
LLM summaries**. Drop-in \`base_url\` replacement for any OpenAI SDK โ€”
change the URL, send \`model="agentic-search"\`, get cited answers.

| | |
|---|---|
| **Base URL** | \`https://agentic-search-engines-ts.netlify.app\` |
| **Auth** | \`Authorization: Bearer $TRAYLINX_API_KEY\` (get one at ) |
| **Primary endpoint** | \`POST /v1/chat/completions\` |
| **Compatibility** | OpenAI Chat Completions (any SDK works) |
| **Response** | OpenAI envelope + \`search_results[]\` + \`agentic_search.*\` |

---

## When to use this skill

Use it when the user asks an agent to:

- Answer a question that needs **current** information (news, releases, prices, scores).
- **Extract content** from a known URL (HTML / markdown / plain text).
- Do **cited web research** with sources and a per-step token breakdown.
- Find **videos**, **images**, or **news** about a topic.
- **Summarize a page** and return the summary with citations.
- Replace \`WebSearch\` / \`WebFetch\` with something that does both + LLM
  synthesis in a single call.

## When NOT to use it

- The data is local โ†’ read the file, don't hit the network.
- A plain \`curl\` or \`WebFetch\` is enough (public static HTML, no JS) โ†’ skip
  the API and save a round-trip.
- The user wants sub-second real-time data (live trades, ticking scores) โ†’
  this API has a 22s deadline and is "current" not "real-time".
- The user wants a free no-auth DuckDuckGo search โ†’ use the
  \`duckduckgo-search\` skill instead.
- The page is JS-heavy / behind login โ†’ use the \`browse\` skill (real
  Chrome via CDP).

---

## Authentication

Two modes โ€” pick one per request.

### User bearer (default โ€” use this unless you know you need A2A)

\`\`\`
Authorization: Bearer 
\`\`\`

Get a token at  โ†’ API Keys. Format:
\`sk-lf-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\`.

Store in env var, never in code:
\`\`\`bash
export TRAYLINX_API_KEY="sk-lf-XXXX-XXXX-XXXX-XXXX-XXXX"
\`\`\`

### Agent-to-Agent (Traylinx Sentinel)

For service-to-service calls. Three headers required:

\`\`\`
X-Agent-Secret-Token:      # 1h lifetime, get from Sentinel /oauth/token
X-Agent-User-Id:                # UUID of the agent owner
X-LLM-API-Key:                   # downstream user's LLM key
\`\`\`

Get an \`agent_secret_token\`:
\`\`\`bash
curl -X POST "https://api.makakoo.com/ma-authentication-ms/v1/api/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=a2a"
# response: { agent_secret_token: "...", expires_in: 7200 }
# agent_secret_token expires in 1h; re-fetch.
\`\`\`

---

## Endpoint 1: \`POST /v1/chat/completions\` โ€” **primary, use this**

OpenAI-compatible. The API reads the **last \`role:"user"\` message** in
\`messages\`, figures out the intent, and runs the right pipeline. The
SDK doesn't need to know about search/scrape at all.

### Request

| Field | Type | Required | Notes |
|---|---|---|---|
| \`model\` | string | yes | Send \`"agentic-search"\`. Value is **ignored** โ€” the router picks the backend. |
| \`messages\` | array | yes | OpenAI shape. Only the last \`role:"user"\` is inspected for intent. |
| \`stream\` | bool | no | Accepted but **ignored** โ€” full answer always returned in one shot today. |
| \`max_tokens\` | number | no | Honored by the summarization model. Default ~1000. |

### Intent detection (automatic)

| Last user message contains... | API does |
|---|---|
| a URL (\`https?://...\`) | scrape that URL + summarize |
| \`video\` / \`videos\` / \`watch\` | video search |
| \`image\` / \`images\` / \`photo\` / \`picture\` | image search |
| \`news\` | news-filter web search |
| \`latest\` / \`current\` / \`today\` / \`this week\` / \`this month\` / \`this year\` / \`now\` / \`newest\` / \`recent\` / \`2025\` / \`2026\` / \`president\` / \`prime minister\` | **freshness fallback** (single web-search model, faster, more up-to-date) |
| anything else | **orchestrator** (classify โ†’ search โ†’ scrape top 2 โ†’ LLM summary, 22s cap, rich citations) |

Inspect \`response.agentic_search.path\` to know which path fired.

### Response (OpenAI envelope + extensions)

\`\`\`json
{
  "id": "chatcmpl-1777016549490",
  "object": "chat.completion",
  "created": 1777016549,
  "model": "agentic-search",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "Kansas City Chiefs won Super Bowl LVIII 25โ€“22 in OT..."
    },
    "finish_reason": "stop"
  }],
  "usage": {"prompt_tokens": 3233, "completion_tokens": 584, "total_tokens": 3817},

  "search_results": [
    {
      "title": "List of Super Bowl champions - Wikipedia",
      "url":   "https://en.wikipedia.org/wiki/List_of_Super_Bowl_champions",
      "snippet": "Kansas City entered the 2023 NFL season as defending...",
      "provider": "brave",
      "date": "2 weeks ago",
      "scraped": true
    }
  ],

  "agentic_search": {
    "path": "orchestrator",                 // or "fallback"
    "answer_model": "openai/gpt-oss-120b",  // or "groq/compound-mini" on fallback
    "duration_seconds": 4.37,
    "balance_before": 63.66,                // wallet credits before this call (100 = $1 USD)
    "usage": {
      "prompt_tokens": 3233, "completion_tokens": 584, "total_tokens": 3817,
      "steps": [
        {"name":"classification","model":"openai/gpt-oss-20b", "prompt_tokens":272,  "completion_tokens":170, "total_tokens":442},
        {"name":"query_building","model":"openai/gpt-oss-20b", "prompt_tokens":190,  "completion_tokens":111, "total_tokens":301},
        {"name":"search",        "model":"search-engine",      "calls": 2},
        {"name":"scrape",        "model":"html2md",            "calls": 2},
        {"name":"summary",       "model":"openai/gpt-oss-120b","prompt_tokens":2771, "completion_tokens":303, "total_tokens":3074}
      ]
    }
  }
}
\`\`\`

\`response.usage\` (top-level) = sum of \`response.agentic_search.usage.steps\`.

### Examples

**curl:**
\`\`\`bash
curl -X POST https://agentic-search-engines-ts.netlify.app/v1/chat/completions \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agentic-search",
    "messages": [{"role": "user", "content": "Who won the last Super Bowl?"}]
  }'
\`\`\`

**Python (OpenAI SDK):**
\`\`\`python
from openai import OpenAI
import os

client = OpenAI(
    api_key=os.environ["TRAYLINX_API_KEY"],
    base_url="https://agentic-search-engines-ts.netlify.app/v1",
)

r = client.chat.completions.create(
    model="agentic-search",
    messages=[{"role": "user", "content": "Who won the last Super Bowl?"}],
)
print(r.choices[0].message.content)
# For the extensions (search_results, agentic_search):
data = r.model_dump()
print(f"{len(data.get('search_results', []))} sources, path={data['agentic_search']['path']}")
\`\`\`

**JavaScript (openai SDK):**
\`\`\`javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.TRAYLINX_API_KEY,
  baseURL: "https://agentic-search-engines-ts.netlify.app/v1",
});

const r = await client.chat.completions.create({
  model: "agentic-search",
  messages: [{ role: "user", content: "What changed in TypeScript 5.6?" }],
});
console.log(r.choices[0].message.content);
console.log(\`\${r.search_results?.length ?? 0} sources\`);
\`\`\`

**Scrape a specific URL:**
\`\`\`bash
curl -X POST https://agentic-search-engines-ts.netlify.app/v1/chat/completions \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agentic-search",
    "messages": [{"role": "user", "content": "scrape https://example.com and summarize"}]
  }'
\`\`\`

**Find videos:**
\`\`\`bash
curl -X POST .../v1/chat/completions \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"agentic-search","messages":[{"role":"user","content":"videos of golden retriever puppies"}]}'
\`\`\`

---

## Endpoint 2: \`POST /v1/search\` โ€” explicit flow

Use this when you want to **pick the pipeline yourself** instead of
letting the chat-completions router decide.

### Request

| Field | Type | Default | Notes |
|---|---|---|---|
| \`query\` | string | required | the question or directive |
| \`flow\` | enum | \`"agentic"\` | \`agentic\` \| \`search_only\` \| \`scrap_only\` \| \`ai_search_only\` |
| \`providers\` | string[] | \`["brave","google"]\` | search backends |
| \`result_filter\` | string[] | \`["web"]\` | \`web\` \| \`news\` \| \`images\` \| \`videos\` |
| \`items\` | number | \`10\` | max results |
| \`country\` | string | \`"US"\` | ISO country code |
| \`params\` | object | \`{}\` | flow-specific overrides โ€” see below |

### Flow types

| Flow | What runs | Needs \`params.url\`? | Notes |
|---|---|---|---|
| \`agentic\` | classify โ†’ search โ†’ scrape โ†’ summarize | no | Rich citations. 22s hard deadline. |
| \`search_only\` | Brave + Google in parallel, dedup, return raw hits | no | Cheapest. No LLM. |
| \`scrap_only\` | Scrape one URL | **yes** | \`params.url\` mandatory. |
| \`ai_search_only\` | Perplexity Sonar single-shot web search | no | Best for opinionated synthesis. |

### \`params\` for \`scrap_only\`

| Key | Type | Default | Notes |
|---|---|---|---|
| \`url\` | string | **required** | absolute URL to scrape |
| \`selector\` | string | \`"body"\` | CSS selector to extract |
| \`formats\` | string[] | \`["markdown"]\` | \`markdown\` \| \`html\` \| \`text\` |
| \`timeout\` | number | \`30000\` | ms |

### \`params\` for \`agentic\` (advanced overrides)

| Key | Type | Default | Notes |
|---|---|---|---|
| \`use_search\` | bool | \`true\` | run the web-search step |
| \`use_scrap\` | bool | \`true\` | scrape the top URLs |
| \`use_ai_search\` | bool | \`false\` | also call Perplexity in parallel |
| \`use_summary\` | bool | \`true\` | run the final LLM summary |
| \`providers\` | string[] | \`["brave","google"]\` | search backends for this run |
| \`result_filter\` | string[] | \`["news","general"]\` | filter search results |
| \`items\` | number | \`15\` | results per provider |
| \`max_tokens\` | number | \`800\` | summary length |
| \`url\` | string | โ€” | optional starting URL |
| \`selector\` | string | โ€” | optional CSS selector for the scrape step |
| \`timeout\` | number | \`25000\` | per-step timeout in ms |
| \`formats\` | string[] | \`["markdown","html"]\` | scrape output formats |
| \`max_chars_per_url\` | number | \`8000\` | truncate scraped pages at this length |
| \`max_urls\` | number | \`5\` | max URLs to scrape |
| \`search_context_size\` | string | \`"high"\` | \`"low"\` \| \`"medium"\` \| \`"high"\` |
| \`temperature\` | number | \`0.2\` | summary creativity (0 = deterministic) |
| \`stream\` | bool | \`false\` | accepted but ignored |

### Example

\`\`\`bash
curl -X POST https://agentic-search-engines-ts.netlify.app/v1/search \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What are the latest AI developments?",
    "flow": "agentic",
    "params": {"providers": ["brave","google","bing"], "items": 15, "max_tokens": 800}
  }'
\`\`\`

---

## Endpoint 3: \`POST /v1/scrap\` โ€” direct URL scrape

Skip the router. Scrape one URL.

### Request

| Field | Type | Default | Notes |
|---|---|---|---|
| \`url\` | string | required | absolute URL |
| \`selector\` | string | \`"body"\` | CSS selector to extract |
| \`formats\` | string[] | \`["markdown"]\` | \`markdown\` \| \`html\` \| \`text\` |
| \`timeout\` | number | \`30000\` | ms |

### Response

\`\`\`json
{
  "success": true,
  "data": {
    "content": "# Example Domain\n\nThis domain is for use in illustrative examples...",
    "html": null,
    "statusCode": 200,
    "metadata": {
      "url": "https://example.com",
      "executionTime": 0.45,
      "format": "markdown",
      "contentLength": 1234
    },
    "url": "https://example.com"
  },
  "metadata": {"url": "https://example.com", "executionTime": 0.45}
}
\`\`\`

### Example

\`\`\`bash
curl -X POST https://agentic-search-engines-ts.netlify.app/v1/scrap \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "formats": ["markdown"]}'
\`\`\`

---

## Common patterns

### Cited Q&A
\`\`\`python
r = client.chat.completions.create(
    model="agentic-search",
    messages=[{"role": "user", "content": "What's the latest on Wayland 1.24?"}],
)
data = r.model_dump()
answer = data["choices"][0]["message"]["content"]
sources = data.get("search_results", [])
print(answer)
for s in sources[:5]:
    print(f"  [{s['provider']}] {s['title']} โ€” {s['url']}")
\`\`\`

### Scrape one URL, no chat
\`\`\`python
import requests, os
r = requests.post(
    "https://agentic-search-engines-ts.netlify.app/v1/scrap",
    headers={"Authorization": f"Bearer {os.environ['TRAYLINX_API_KEY']}"},
    json={"url": "https://example.com", "formats": ["markdown", "html"]},
    timeout=30,
).json()
print(r["data"]["content"])
\`\`\`

### Cost / usage observability
\`\`\`python
data = r.model_dump()
path = data["agentic_search"]["path"]
answer_model = data["agentic_search"]["answer_model"]
duration = data["agentic_search"]["duration_seconds"]
balance_before = data["agentic_search"]["balance_before"]
for step in data["agentic_search"]["usage"]["steps"]:
    print(f"  {step['name']:18s} {step.get('model','?'):25s}", end="")
    if "calls" in step:
        print(f"  {step['calls']} calls")
    else:
        print(f"  {step['total_tokens']} tokens")
\`\`\`

### Force the freshness path
Use words like \`latest\`, \`current\`, \`today\`, \`this week\`, or a year
(\`2025\` / \`2026\`) in your question. The router picks the freshness
fallback automatically and uses a web-search-native model that's
always better for current events.

---

## Errors

| Status | Body | Cause | Fix |
|---|---|---|---|
| 400 | \`Messages array is required\` | empty body | send \`messages: [...]\` |
| 400 | \`No user message found\` | no \`role:"user"\` entry | add one |
| 401 | \`No authentication credentials provided\` | missing \`Authorization\` header | add it |
| 401 | \`Invalid token\` | bad / expired token | re-issue at traylinx.com |
| 401 | \`X-LLM-API-Key header required for agent authentication\` | A2A without LLM key | add \`X-LLM-API-Key\` |
| 401 | \`Invalid agent token\` | Sentinel rejected the signature | re-fetch agent_secret_token |
| 500 | \`Failed to route query\` | classifier crash | retry; file a bug if persistent |
| 200 | \`"I couldn't find an answer for \"\"."\` | both paths failed | rephrase; if you have a URL, try \`/v1/scrap\` directly |

The endpoint never streams today (\`stream: true\` accepted, ignored,
full answer in one response).

---

## Rate limits (per token)

| Tier | Requests / minute |
|---|---|
| Free | 60 |
| Pro | 600 |
| Enterprise | unlimited |

Response headers: \`X-RateLimit-Limit\`, \`X-RateLimit-Remaining\`,
\`X-RateLimit-Reset\`. The orchestrator counts as 1 request regardless
of internal steps.

---

## Health check

\`\`\`bash
curl -fsS https://agentic-search-engines-ts.netlify.app/health
# {"status":"ok","timestamp":"2026-06-02T10:19:04.534Z","version":"1.0.0"}
\`\`\`

No auth required. Use to confirm the host is up before doing real work.

---

## Hard rules for any agent using this skill

1. **Always pass the token in \`Authorization\`, never in the URL.** Tokens
   in URLs leak through server logs and referer headers.
2. **Never hardcode the base URL.** Read it from
   \`AGENTIC_SEARCH_API_BASE_URL\` env var, default to
   \`https://agentic-search-engines-ts.netlify.app\`.
3. **Never substitute a raw \`curl\` for this API** when the user asked
   for the search API โ€” you lose the intent router and the LLM summary.
4. **Multi-turn is not supported.** The router only inspects the **last
   user message**. Concatenate earlier turns into the last user message
   yourself if you need context.
5. **The 22s deadline is real.** Don't wrap the call in a 5s client
   timeout โ€” the orchestrator path needs the full budget.
6. **Inspect \`agentic_search.path\`** to know whether you got the rich
   orchestrator answer or the faster freshness fallback.

---

## Links

- **Production API:** 
- **Source / GitHub:** 
- **Traylinx (token issuer):** 
- **Sentinel A2A docs:** 
curl -fsS https://agentic-search-engines-ts.netlify.app/v1/chat/completions \
  -H "Authorization: Bearer $TRAYLINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agentic-search",
    "messages": [
      { "role": "user", "content": "What changed in Anthropic's API this week?" }
    ]
  }'
from openai import OpenAI

client = OpenAI(
    base_url="https://agentic-search-engines-ts.netlify.app",
    api_key="tsk-...",
)

resp = client.chat.completions.create(
    model="agentic-search",
    messages=[
        {"role": "user", "content": "Find the latest on GPT-5 release notes"},
    ],
)

print(resp.choices[0].message.content)
print()
print("Sources:", len(resp.search_results))
print("Path:", resp.agentic_search.path)  # "orchestrator" or "freshness_fallback"
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://agentic-search-engines-ts.netlify.app",
  apiKey: process.env.TRAYLINX_API_KEY,
});

const resp = await client.chat.completions.create({
  model: "agentic-search",
  messages: [
    { role: "user", content: "Scrape https://example.com/pricing and summarize" },
  ],
});

console.log(resp.choices[0].message.content);
console.log("Sources:", resp.search_results.length);
console.log("Path:", resp.agentic_search.path);