@ooky/sdk
v0.6.2
Published
Ooky SDK — middleware for serving AI brand intelligence and capturing AI-bot analytics from your Node, Next.js, or Vercel Edge app.
Maintainers
Readme
@ooky/sdk
Drop-in middleware for serving AI brand intelligence and capturing AI-bot analytics from your Node, Next.js, or Vercel Edge app — without DNS changes or Cloudflare workers.
When an AI bot (GPTBot, ClaudeBot, Perplexity, etc.) hits any page on your site, the SDK fires a non-blocking event to Ooky. When a request asks for one of the well-known AI paths (/llms.txt, /.well-known/ai-manifest.json, /agents.md, …), the SDK serves the latest manifest you've published from the Ooky dashboard.
Install
npm install @ooky/sdkYou'll need a domain registered in the Ooky dashboard and an API key from Integrations → SDK → Generate API Key. The key looks like ooky_sk_<random> and is shown once — store it as an environment variable.
Reveal-once key: Ooky shows the key only at the moment of generation. After your SDK fires its first successful event, the plaintext is purged server-side (only a SHA-256 hash remains, used for ongoing authentication). If you lose the key, rotate it — there's no "show key again" path. This matches the standard set by Stripe, Linear, and GitHub.
Quickstart
Express
import express from "express";
import { ookyMiddleware } from "@ooky/sdk/express";
const app = express();
app.use(ookyMiddleware({
apiKey: process.env.OOKY_API_KEY,
domain: "acme.com",
}));
// your routes...Next.js
// middleware.ts
import { ookyMiddleware } from "@ooky/sdk/next";
export default ookyMiddleware({
apiKey: process.env.OOKY_API_KEY,
domain: "acme.com",
});
export const config = {
matcher: ["/llms.txt", "/llms-full.txt", "/agents.md", "/.well-known/:path*", "/(.*)"],
};Works in both the Node and Edge runtimes — no code change.
Vercel Edge / Web Fetch
// middleware.ts (Vercel Edge)
import { ookyEdge } from "@ooky/sdk/edge";
export default ookyEdge({
apiKey: process.env.OOKY_API_KEY,
domain: "acme.com",
});
export const config = { matcher: "/(.*)" };What gets intercepted
The SDK responds to these paths with the latest published manifest:
| Path | What gets served |
|---|---|
| /llms.txt | Markdown summary for LLM crawlers |
| /llms-full.txt | Extended markdown with all page sections |
| /.well-known/ai-manifest.json | Full JSON brand manifest (global + per-page) |
| /ai-manifest.json | Same as above (alternate path) |
| /agents.md | Markdown agent guide |
| /.well-known/mcp | MCP server descriptor (GET) / tool invocation (POST) |
| /mcp | Same as above (alternate path — some platforms intercept /.well-known/*) |
Every other request passes through to your app unchanged.
MCP tool invocation
POST /mcp (and /.well-known/mcp) speaks two protocols — pick whichever
your client uses:
Standard MCP — JSON-RPC 2.0 (what real MCP clients use: Claude, MCP Inspector, ChatGPT connectors). Send
initialize,tools/list, thentools/call:{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_brand_info", "arguments": { "section": "about" } } }The SDK answers with a single JSON response (no SSE stream required). Notifications (
notifications/*) get202 Accepted; an unparseable body returns a JSON-RPC parse error (-32700, HTTP 200 per spec).Legacy Ooky protocol —
{ "tool": "get_brand_info", "arguments": { "section": "about" } }→{ "result": … }, kept for Worker-tier compatibility.
Both answer get_brand_info from the published manifest (same cache and
stale-on-error behavior as the other paths). Product tools
(search_products, …) are Worker-tier only; the SDK returns a tool-not-found
error for them.
What gets logged
For every request (manifest or not), the SDK checks the User-Agent against the bot registry. When a known AI bot is detected, it fires a fire-and-forget POST to /api/ingest/events with:
{
"event_id": "<uuid>",
"timestamp": "<ISO 8601>",
"bot": { "name": "GPTBot", "verified": false, "ua_string": "<full UA>" },
"request": { "page_path": "/pricing", "method": "GET" }
}The event scope (which domain it belongs to) is determined server-side from your API key — you cannot accidentally log events for a different customer's domain.
AI referral attribution: when a human arrives from an AI platform —
detected via the Referer header (chatgpt.com, perplexity.ai, claude.ai,
gemini.google.com, …) or utm_source (?utm_source=chatgpt) — the SDK fires
an ai_referral event instead, powering the dashboard's attribution views.
Same platform list as the Worker tier.
All other human traffic produces no events.
Configuration options
ookyMiddleware({
// Required
apiKey: "ooky_sk_...",
domain: "acme.com",
// Optional — defaults are right for production
apiBase: "https://api.ooky.ai/api", // Ooky API root
cdnBase: "https://api.ooky.ai/api/public/manifest", // Manifest source (default = apiBase + "/public/manifest")
bots: undefined, // Override the bot registry; default ships with major AI bots
autoRefreshBots: true, // Periodically refresh bot UA list from /api/public/bots
fetchTimeoutMs: 10000, // Hard timeout on every upstream fetch
manifestCacheTtlMs: 300000, // In-memory manifest cache TTL (0 disables)
maxEventsPerMinute: 300, // Token-bucket cap on event POSTs (Infinity disables)
onError: (err, ctx) => {}, // Surface swallowed failures (e.g. a 401 = rotated key)
awaitEvents: "auto", // Express on Lambda/Netlify: await events before responding (survives the freeze)
});| Option | Type | Default | Notes |
|---|---|---|---|
| apiKey | string | — | Required. Per-domain Bearer token from the dashboard. |
| domain | string | — | Required. Must match the verified domain in Ooky. |
| apiBase | string | https://api.ooky.ai/api | Override for self-hosted Ooky or staging. |
| cdnBase | string | ${apiBase}/public/manifest | Manifest source. By default the SDK fetches from Ooky's public manifest endpoint. Override to put your own CDN (Cloudflare, CloudFront, Fastly) in front. |
| bots | Array<{name, pattern, category}> | Built-in default list | Ships with the major AI bots. Override only if you have custom UA patterns. |
| autoRefreshBots | boolean | true | Refresh from /api/public/bots once an hour (ETag-aware). Disable for fully offline use. |
| fetchTimeoutMs | number | 10000 | Abort upstream fetches (manifest, registry, events) after this many ms so a slow Ooky API can never hang your request path. |
| manifestCacheTtlMs | number | 300000 | Manifest responses are cached in-memory per process. On upstream failure (network error or 5xx), a stale copy up to 24h old is served instead of an error. Set 0 to disable. |
| onError | (error, context) => void | silent | Called for every failure the SDK swallows: event POST rejections and non-2xx responses (a 401 means your key was rotated/revoked), manifest fetch failures, registry refresh failures. context is { op, status?, kind?, throttled? }. Wire it to your logger so a dead integration is visible: onError: (e, ctx) => logger.warn("ooky", ctx.op, e.message). |
| maxEventsPerMinute | number | 300 | Token-bucket cap on event POSTs — a bot storm can't turn your server into an unbounded POST source. Drops are reported through onError (at most once per 10s, with a count). Pass Infinity to disable. |
| awaitEvents | 'auto' \| boolean | 'auto' | Express adapter only. On a freeze-after-response serverless host (AWS Lambda, Netlify Functions, GCP Cloud Run / Cloud Functions) the process freezes the instant it responds, killing an un-awaited event POST — so events silently never arrive. 'auto' detects this (AWS_LAMBDA_FUNCTION_NAME · K_SERVICE · FUNCTION_TARGET) and awaits the event before responding (bounded by fetchTimeoutMs); on long-running servers it stays fire-and-forget. Force with true/false. The Next/Edge adapter handles this via event.waitUntil() instead. |
TypeScript declarations ship with the package (@ooky/sdk, /express, /next, /edge are all typed) — no @types/* install needed.
Performance & resilience
- Manifest responses are cached in-memory for 5 minutes, with concurrent cold-cache requests deduped into a single upstream fetch.
- If the Ooky API is unreachable or erroring, the SDK serves the last good copy (up to 24h old) — a transient Ooky outage never breaks your
/llms.txt. - Every upstream fetch carries a hard 10s timeout (
AbortSignal.timeout), so your request path can never hang on Ooky. - The manifest response also carries
Cache-Control: public, max-age=300, s-maxage=600— your CDN/edge will serve repeat requests without hitting your origin at all. - Event firing uses
fetch(..., { keepalive: true })so it survives the response cycle without delaying it. On Vercel Edge / Next middleware the SDK registers the event POST withevent.waitUntil()automatically. On AWS Lambda (Express),keepaliveisn't enough — the whole process freezes after the response — so the Express adapter awaits the event there instead (seeawaitEvents). - Bot detection is a substring check against an in-memory list — sub-millisecond per request.
Troubleshooting
"I installed it but no events show up"
- Set the
onErroroption to log swallowed failures — a repeatedrecordEventerror withstatus: 401means the key was rotated or revoked. - Confirm your domain is verified and the integration method is set to
sdk(orwordpress) in the dashboard. - Check that
process.env.OOKY_API_KEYis actually set in your runtime — log it once at boot. - Hit your site with a bot UA:
curl -H "User-Agent: GPTBot/1.0" https://your-site.com/and watch the dashboard's AI Sessions page. - If your app is behind a CDN that strips
User-Agent, the SDK can't see the bot. Check your CDN config.
"/llms.txt returns 404"
- The middleware only intercepts paths the SDK knows about. Make sure your framework's matcher passes those paths to the middleware before falling through to your routes.
- If you've published the manifest in the dashboard, also check the manifest source is reachable from your server:
curl https://api.ooky.ai/api/public/manifest/<your-domain>/llms(or yourcdnBaseoverride).
"Events fail with 401 Unauthorized"
- The API key has been revoked or rotated. Generate a new one from the dashboard and update the env var.
"Manifest is stale"
- The HTTP cache is honoring 5 min freshness. Ooky purges the CDN on publish, but your CDN may also cache. Force a fresh fetch by clearing your edge cache for the well-known paths.
Security & key lifecycle
The SDK runs server-side only — Node middleware or the Edge runtime. The API key never reaches the browser. Key handling follows the same lifecycle as Stripe / Linear / GitHub:
| Stage | Where the key lives |
|---|---|
| Generated | Returned once in the dashboard. Plaintext + SHA-256 hash stored server-side. |
| Customer copies it | Customer pastes into their env (OOKY_API_KEY). |
| First event lands | Server drops the plaintext from the DB. Only the hash remains. |
| Ongoing auth | Server hashes the incoming Bearer token and compares against the stored hash in constant time. |
| Lost / leaked | Rotate via Integrations → SDK → Rotate API Key. Old key 401s immediately. |
The package is published with npm provenance attestations signed by GitHub Actions OIDC. You can verify the published tarball came from this repo's release-sdk.yml workflow:
npm view @ooky/sdk dist.attestationsIf you're using a private registry mirror, mirror the provenance bundle too — most package proxies (Artifactory, Verdaccio, GitHub Packages) preserve it.
Recommended rotation cadence
- Every 90 days for production deployments.
- Immediately if you suspect leak (committed to a public repo, server compromise, contractor offboarding).
- Set a calendar reminder; Ooky will surface an in-dashboard warning at 90 days from generation.
Where the key must never appear
- ✅ Server env vars (
process.env.OOKY_API_KEY) — yes. - ❌
next.config.js,vite.config.js, or any file shipped to the client — no. - ❌ Git commits — never. If you push it by accident, rotate the key first, then clean git history.
- ❌ Logging — the SDK does not log the key. Don't log
process.envblindly either.
What this SDK does NOT do
- It does not crawl your site or generate the manifest — that happens in the dashboard.
- It does not modify HTML responses or rewrite content for bots. (For that, use the Worker or Full DNS integration.)
- It does not block bots. Your
robots.txtand any rate limiting still apply.
License
MIT.
