We build custom applications 5x faster and cheaper đ
Book a Free Consultation
Stuck on an error? Book a 30-minute call with an engineer and get a direct fix + next steps. No pressure, no commitment.
Short answer: Build a small externally hosted skill service that your OpenClaw agent will call (registered and configured via ClawHub), authenticate to Exa using the method Exa supports (OAuth for user-level access or API key / client-credentials for server-to-server), proxy queries from the skill to Exaâs Web Search API with proper scopes/headers, validate webhooks and signatures, store secrets in ClawHub or environment variables, and instrument logs/metrics so you can debug authentication, rate limits, and parsing errors. All integration steps are explicit: register the skill in ClawHub, configure environment variables and auth flows, implement the REST calls to Exa, and move any durable state outside the agent runtime.
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json());
// Environment variables (set via ClawHub or your secret manager)
const EXA_BASE = process.env.EXA_BASE\_URL; // // e.g. https://api.exa.example
const EXA_API_KEY = process.env.EXA_API_KEY; // // or use token-based auth
app.post('/skill/search', async (req, res) => {
// // Expect JSON like { query: "kubernetes ingress", max\_results: 3 }
const { query, max\_results = 5 } = req.body;
if (!query) return res.status(400).json({ error: 'missing query' });
try {
// // Call Exa's search endpoint -- replace path with Exa's real path
const url = new URL('/v1/search', EXA\_BASE);
url.searchParams.set('q', query);
url.searchParams.set('limit', String(max\_results));
const resp = await fetch(url.toString(), {
method: 'GET',
headers: {
'Authorization': `Bearer ${EXA_API_KEY}`,
'Accept': 'application/json'
},
// // optionally set a short timeout at HTTP client level
});
if (!resp.ok) {
// // bubble up provider error for logging and debugging
const body = await resp.text();
console.error('Exa error', resp.status, body);
return res.status(502).json({ error: 'upstream\_error', status: resp.status });
}
const data = await resp.json();
// // Normalize Exa's response into a compact shape the agent expects
const results = (data.results || []).slice(0, max\_results).map(r => ({
title: r.title,
snippet: r.snippet,
url: r.url
}));
return res.json({ results });
} catch (err) {
console.error('search failed', err);
return res.status(500).json({ error: 'internal\_error' });
}
});
const port = process.env.PORT || 3000;
app.listen(port, () => { console.log('Skill backend listening on', port); });
Notes on the example: replace EXA_BASE_URL and the path /v1/search with whatever Exa's real API uses. The code uses a bearer API key; for OAuth you would obtain an access token and set Authorization: Bearer <access\_token> instead.
POST https://auth.exa.example/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE_FROM\_EXA
&redirect\_uri=https%3A%2F%2Fyour-service.example%2Foauth%2Fcallback
&client_id=YOUR_CLIENT\_ID
&client_secret=YOUR_CLIENT\_SECRET
POST https://auth.exa.example/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=search.read
&client_id=YOUR_CLIENT\_ID
&client_secret=YOUR_CLIENT\_SECRET
After you receive access tokens, call Exa with Authorization: Bearer <access_token>. Implement refresh flows for refresh_token grant\_type when applicable.
Speak oneâonâone with a senior engineer about your noâcode app, migration goals, and budget. In just half an hour youâll leave with clear, actionable next stepsâno strings attached.
1
Short answer: set the Connector API key where the OpenClaw connector expects it (either in the ClawHub connector credential entry or as the environment variable the agent/runtime uses), restart the runtime or re-authenticate the CLI, and verify the key is valid and unexpired â a 401 means the runtime isnât sending a valid token or the key lacks required scopes.
Do these in order:
curl -i -H "Authorization: Bearer " "https://<EXA_WEBSEARCH_URL>/search?q=test"
2
Map Exa Web Search JSON to OpenClaw's ResultSchema by extracting canonical fields (id, title, url, snippet/summary, published_at, source, score), casting types, normalizing arrays to lists of Result objects, and attaching the original payload to a raw or metadata field for provenance.
// map Exa response to OpenClaw ResultSchema
function mapExaToResultSchema(exaJson){
return (exaJson.hits||[]).map(hit => ({
id: hit.id || hit.url,
title: hit.title || hit.headline,
url: hit.url || hit.link,
summary: hit.snippet || hit.summary || '',
published_at: new Date(hit.timestamp||hit.published).toISOString(),
score: Number(hit.score || 0),
raw: hit // original payload for provenance
}));
}
3
Direct answer: implement exponential backoff with full jitter, honor the server's Retry-After header when present, shift excess traffic into a queue or rate-limiter outside the agent runtime, and use conservative circuit-breaker thresholds so OpenClawâs CircuitBreaker doesnât trip spuriously.
Apply retries with exponential backoff + jitter, respect Retry-After, log and surface 429s without immediate hard failures, and push bursty work to an external queue/token-bucket so the runtime sees a steady rate.
// <b>//</b> simple fetch with Retry-After and jittered backoff
async function callSearch(url, opts){
const max=5;
for(let i=0;i<max;i++){
const res=await fetch(url,opts);
if(res.status!==429) return res;
const ra=res.headers.get('retry-after');
const serverDelay=ra?parseInt(ra,10)*1000:0;
const expo=Math.min(1000*(2**i),30000);
const jitter=Math.random()*expo;
await new Promise(r=>setTimeout(r, serverDelay || jitter));
}
throw new Error('TooManyRequests');
}
4
Enable telemetry by turning on tracing in the Web Search by Exa skill configuration in ClawHub (or set the skill manifest telemetry flag) and include the X-Claw-Trace header on requests from your agent/runtime. Read traces by capturing the X-Claw-Trace value returned in response headers and by inspecting OpenClaw runtime logs or the ClawHub telemetry viewer for the same trace ID.
// set trace id and call Web Search by Exa
const traceId = `claw-${Date.now()}`
// fetch call with header
const res = await fetch('https://exa.example/api/web-search', {
method: 'POST',
headers: { 'Content-Type':'application/json','X-Claw-Trace': traceId, 'Authorization': `Bearer ${process.env.EXA_KEY}` },
body: JSON.stringify({ query:'example' })
})
// read echoed trace and body
const echoed = res.headers.get('X-Claw-Trace')
const data = await res.json()
This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. Weâll discuss your project and provide a custom quote at no cost.Â