apptvty
v0.3.2
Published
Server-side analytics and AEO (Agent Experience Optimization) SDK for websites. Logs agentic traffic, exposes a structured query endpoint for AI agents, and serves relevant ads on agent queries.
Maintainers
Readme
apptvty
AI traffic analytics and Agent Experience Optimization (AEO) for Node.js websites.
Apptvty makes your website visible to, and queryable by, AI agents — the crawlers and language models (GPTBot, ClaudeBot, Perplexity, etc.) that are increasingly the primary consumers of web content.
- Detects and classifies AI traffic that standard analytics tools miss entirely
- Exposes a
/queryendpoint so AI agents can get structured, sourced answers from your site - Earns USDC for your site when sponsored ads are served in query responses
- Two lines to integrate into an existing Next.js or Express app
npm install apptvtyNode.js >= 18 required.
Quick start
Next.js (App Router)
Step 1 — Log all traffic
// middleware.ts
import { withApptvty } from 'apptvty/nextjs';
export default withApptvty({
apiKey: process.env.APPTVTY_API_KEY!,
siteId: process.env.APPTVTY_SITE_ID!,
});
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Step 2 — Expose the query endpoint
// app/query/route.ts
import { createNextjsQueryHandler } from 'apptvty/nextjs';
export const GET = createNextjsQueryHandler({
apiKey: process.env.APPTVTY_API_KEY!,
siteId: process.env.APPTVTY_SITE_ID!,
});That's it. Every request is now logged, AI crawlers are classified, and AI agents can query your site at https://yoursite.com/query?q=your+question.
Express
import express from 'express';
import { createExpressMiddleware, createExpressQueryHandler } from 'apptvty/express';
const app = express();
const config = {
apiKey: process.env.APPTVTY_API_KEY!,
siteId: process.env.APPTVTY_SITE_ID!,
};
// Log all requests
app.use(createExpressMiddleware(config));
// Expose the query endpoint
app.get('/query', createExpressQueryHandler(config));CLI setup (humans and agents)
The fastest way to get credentials and scaffold files:
# Interactive (human)
npx apptvty init
# Non-interactive (agent / CI) — prints JSON to stdout
npx apptvty init --domain mysite.com --framework nextjs --non-interactiveTrigger a re-crawl after publishing new content:
# Reindex your site (reads APPTVTY_SITE_ID and APPTVTY_API_KEY from .env.local / .env)
npx apptvty migrateCLI commands:
| Command | Description |
|---------|-------------|
| init | Register site and scaffold integration files (default) |
| migrate | Trigger immediate re-crawl/reindex of your site |
CLI flags:
| Flag | Description |
|------|-------------|
| --domain | Your site's domain, e.g. mysite.com (init only) |
| --framework | nextjs, express, or other (init only, auto-detected) |
| --non-interactive | No prompts; JSON output for scripting/agents |
| --api-url | Override API base URL (staging / self-hosted) |
Agent-mode JSON output:
{
"site_id": "site_abc123",
"api_key": "ak_live_...",
"company_id": "co_xyz...",
"wallet_address": "0x...",
"dashboard_url": "https://dashboard.apptvty.com/claim?token=...",
"env_file": ".env.local",
"env_vars": {
"APPTVTY_SITE_ID": "site_abc123",
"APPTVTY_API_KEY": "ak_live_..."
}
}The CLI writes credentials to .env.local (Next.js) or .env (Express/other) and scaffolds middleware.ts and app/query/route.ts if they do not already exist.
Package exports
| Import | Contents |
|--------|----------|
| apptvty | Core client, logger, crawler detector, query handler (framework-agnostic) |
| apptvty/nextjs | Next.js App Router middleware and route handler |
| apptvty/express | Express/Connect middleware and route handler |
| apptvty/setup | register() and migrate() for programmatic setup and reindex |
Configuration
All middleware and handler functions accept an ApptvtyConfig object:
interface ApptvtyConfig {
apiKey: string; // Required. Your site's API key (ak_...)
siteId: string; // Required. Your site ID from the dashboard
baseUrl?: string; // Optional. Uses APPTVTY_API_URL env if set.
batchSize?: number; // Default: 50 — logs per flush
flushInterval?: number; // Default: 5000 — ms between auto-flushes
debug?: boolean; // Default: false — log errors to console
queryPath?: string; // Default: '/query' — path of AEO endpoint
}Set these from environment variables so credentials are never in source code:
APPTVTY_API_KEY=ak_live_...
APPTVTY_SITE_ID=site_...
APPTVTY_API_URL=https://api.apptvty.com # Optional. Default is api.apptvty.com; override for self-hosted only.See .env.example for a copy-paste template.
Production: The SDK uses https://api.apptvty.com by default. You do not need to set APPTVTY_API_URL for production.
Using the SDK with a self-hosted backend
Only if you run apptvty-backend yourself (e.g. Serverless in your AWS account), point the SDK at that API:
- Deploy the backend (from the backend repo):
cd apptvty-backend npx sls deploy --stage dev - Get the API URL from the stack output:
Use the ApiGatewayRestApiUrl (e.g.npx sls info --stage devhttps://xxxx.execute-api.us-west-2.amazonaws.com/dev). - Override the default (api.apptvty.com) in one of two ways:
- Environment: Set
APPTVTY_API_URLto that URL (no trailing slash). - In code: Pass
baseUrl(orapiUrlforregister/migrate).
- Environment: Set
The query endpoint
When an AI agent visits https://yoursite.com/query, the SDK responds with a self-describing discovery page so the agent knows how to use it:
{
"version": "1.0",
"endpoint": "https://yoursite.com/query",
"description": "Query this site's content using natural language.",
"usage": {
"method": "GET",
"parameters": {
"q": { "type": "string", "required": true, "description": "Natural language question" },
"lang": { "type": "string", "required": false, "description": "Preferred language (ISO 639-1)" }
},
"example": "https://yoursite.com/query?q=What+does+this+site+do"
},
"capabilities": ["rag", "sponsored_ads"],
"rate_limit": "60 requests per minute"
}When called with ?q=<question>:
{
"success": true,
"version": "1.0",
"query": "What does this site do?",
"answer": "Apptvty is an AI traffic analytics platform ...",
"sources": [
{
"url": "https://yoursite.com/about",
"title": "About Apptvty",
"snippet": "Apptvty logs and classifies AI crawler traffic ...",
"relevance": 0.94
}
],
"confidence": 0.91,
"sponsored": {
"label": "Sponsored",
"text": "Get deeper AI analytics with AEO Pro — 14-day free trial",
"url": "https://...",
"advertiser": "AEO Pro",
"impression_id": "imp_..."
},
"metadata": {
"request_id": "req_...",
"response_time_ms": 340,
"tokens_used": 420,
"site_id": "site_...",
"timestamp": "2026-03-06T12:00:00.000Z"
}
}The sponsored field is present only when ads are enabled for your site and a matching ad exists. Ads are clearly labeled and serve as a revenue stream for site owners — no action required by the agent beyond passing the field through to the end user.
Error response:
{
"success": false,
"error": {
"code": "QUERY_TOO_LONG",
"message": "Query exceeds the 500-character limit.",
"request_id": "req_...",
"timestamp": "2026-03-06T12:00:00.000Z"
}
}Advertiser funding (X402)
If you are an agent or company paying for ads, Apptvty supports autonomous pre-paid credits via USDC on Base.
- Get the deposit address: Run
npx apptvty initor check theplatformDepositAddressfield in theregister()response. - Send USDC: Transfer the desired amount of USDC (Base network) to that address.
- Sync your balance: Call the wallet sync endpoint with your transaction hash:
Note: On-chain verification takes ~30-60 seconds.curl "https://api.apptvty.com/v1/wallet/sync?tx_hash=0x..."
Response headers always include:
Content-Type: application/jsonCache-Control: no-storeX-Robots-Tag: noindex
Analytics for coding agents
The SDK exposes methods so coding agents (e.g. Cursor, Claude Code) can fetch logs, activity, and errors without human intervention — similar to how aws logs tail works for AWS CLI.
import { ApptvtyClient } from 'apptvty';
const client = new ApptvtyClient({
apiKey: process.env.APPTVTY_API_KEY!,
siteId: process.env.APPTVTY_SITE_ID!,
});
// Wallet balance
const wallet = await client.getSiteWallet();
// Ad Campaign Insights (for advertisers)
const insights = await client.getCampaignInsights('camp_123');
console.log(`Top site: ${insights.top_publishers[0].domain}`);| Method | Returns |
|--------|---------|
| getSiteStats() | 30-day overview (requests, AI %, crawlers, queries) |
| getSiteDailyStats(days?) | Per-day breakdown |
| getSiteActivity(limit?) | Recent requests (path, crawler, status) |
| getSiteQueries(limit?) | Recent agent queries |
| getSiteCrawlers(days?) | Crawler type breakdown |
| getSiteWallet() | Balance, earnings, spend |
| getCampaignInsights(id) | Ad performance (impressions per site, daily spend) |
Local logs dashboard
Deploy a localized analytics portal on your own domain using the framework-agnostic dashboard handler.
Next.js (App Router)
// app/api/apptvty/logs/route.ts
import { createNextjsDashboardHandler } from 'apptvty/nextjs';
export const GET = createNextjsDashboardHandler({
apiKey: process.env.APPTVTY_API_KEY!,
siteId: process.env.APPTVTY_SITE_ID!,
});Your dashboard is now available at https://yoursite.com/api/apptvty/logs.
Crawler detection
The SDK identifies 50+ AI crawlers by user-agent string. Access detection directly:
import { detectCrawler, getKnownCrawlerNames } from 'apptvty';
const info = detectCrawler('GPTBot/1.1');
// {
// isAi: true,
// name: 'GPTBot',
// organization: 'OpenAI',
// confidence: 0.95,
// detectionMethod: 'exact_match'
// }
const names = getKnownCrawlerNames();
// ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'BingBot', ...]CrawlerInfo fields:
| Field | Type | Description |
|-------|------|-------------|
| isAi | boolean | Whether the traffic is from an AI/LLM system |
| name | string \| null | Crawler name, e.g. "GPTBot" |
| organization | string \| null | Organization, e.g. "OpenAI" |
| confidence | number | Detection certainty, 0.0 – 1.0 |
| detectionMethod | string | exact_match / pattern_match / heuristic / none |
Crawlers recognized (partial list): GPTBot, OpenAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-Web, Anthropic-AI, Google-Extended, GoogleOther, Bingbot, PerplexityBot, YouBot, DuckDuckBot, Meta-ExternalAgent, LinkedInBot, AppleBot, TwitterBot, CohereAI, AI2Bot, MistralBot.
Programmatic registration
For agents and scripts that need to register a site without the CLI:
import { register, RegistrationError } from 'apptvty/setup';
try {
const result = await register({
domain: 'mysite.com',
framework: 'nextjs', // 'nextjs' | 'express' | 'other'
agentId: 'my-agent', // optional — analytics on which agent registered
});
console.log(result.apiKey); // 'ak_live_...'
console.log(result.siteId); // 'site_...'
console.log(result.walletAddress); // '0x...' or null
console.log(result.dashboardUrl); // Dashboard claim link (valid 30 days)
console.log(result.setup.envVars); // { APPTVTY_SITE_ID, APPTVTY_API_KEY }
} catch (err) {
if (err instanceof RegistrationError) {
// err.code: 'DOMAIN_TAKEN' | 'REGISTRATION_FAILED' | ...
console.error(err.code, err.message);
}
}RegisterOptions:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| domain | string | Yes | Site domain, e.g. mysite.com |
| framework | string | No | nextjs, express, or other |
| agentId | string | No | Identifies the registering agent (for analytics) |
| apiUrl | string | No | Override API base URL for staging/self-hosted |
RegisterResult:
| Field | Type | Description |
|-------|------|-------------|
| siteId | string | Site identifier |
| apiKey | string | API key for SDK config |
| companyId | string | Company identifier |
| walletAddress | string \| null | Crossmint wallet for USDC earnings |
| dashboardUrl | string | Dashboard claim link (valid 30 days) |
| claimTokenExpiresAt | string | ISO timestamp of link expiry |
| setup.envVars | object | Env vars to write to your .env file |
| setup.files | object \| undefined | Optional scaffold file contents |
Registration is idempotent per domain. Registering the same domain twice throws RegistrationError with code: 'DOMAIN_TAKEN'.
Programmatic migrate (reindex)
Trigger an immediate re-crawl after publishing new content:
import { migrate, MigrateError } from 'apptvty/setup';
try {
const result = await migrate({
siteId: process.env.APPTVTY_SITE_ID!,
apiKey: process.env.APPTVTY_API_KEY!,
apiUrl: 'https://api.apptvty.com', // optional — for staging/self-hosted
});
console.log(result.message); // "Reindex started. Your site content will be updated within a few minutes."
} catch (err) {
if (err instanceof MigrateError) {
console.error(err.code, err.message);
}
}The crawl runs asynchronously; the endpoint returns 202 immediately.
Advanced: framework-agnostic usage
Use ApptvtyClient and RequestLogger directly for custom frameworks:
import { ApptvtyClient, RequestLogger, detectCrawler, createQueryHandler } from 'apptvty';
const config = { apiKey: 'ak_...', siteId: 'site_...' };
const client = new ApptvtyClient(config);
const logger = new RequestLogger(client, config);
// In your request handler:
const crawlerInfo = detectCrawler(req.headers['user-agent'] ?? '');
logger.enqueue({
site_id: config.siteId,
timestamp: new Date().toISOString(),
method: req.method,
path: req.url,
status_code: res.statusCode,
response_time_ms: elapsedMs,
ip_address: req.socket.remoteAddress ?? '',
user_agent: req.headers['user-agent'] ?? '',
referer: req.headers.referer ?? null,
is_ai_crawler: crawlerInfo.isAi,
crawler_type: crawlerInfo.name,
crawler_organization: crawlerInfo.organization,
confidence_score: crawlerInfo.confidence,
});
// Flush on shutdown
process.on('SIGTERM', () => logger.flush());Request logging behavior
- Logs are batched in memory and flushed every
flushIntervalms (default 5s) or whenbatchSizeentries accumulate (default 50) - Logging is non-blocking — failures never propagate to the request handler
- The logger automatically flushes on
SIGTERM,SIGINT, andbeforeExit - The flush timer is unreferenced — it will not keep a process alive after all other work is done
Paths skipped by middleware (never logged):
/_next/— Next.js build assets/api/_*— Internal Next.js API routes/favicon.ico- Static file extensions:
.svg,.png,.jpg,.jpeg,.gif,.webp,.ico,.woff,.woff2,.ttf,.css,.js.map
Analytics dashboard
Visit your dashboardUrl after registration to:
- View AI vs human traffic split over time
- See which crawlers are visiting and how often
- Browse agent queries and the answers served
- Track USDC earnings from sponsored ads
- Manage API keys and site settings
The link is valid for 30 days. Add an email address in the dashboard to establish a permanent login.
Testing
npm test # Unit + integration (hits real dev API)
npm run test:unit # Unit tests only (mocked API)
npm run test:integration # Integration tests against deployed dev APIIntegration tests use https://api.apptvty.com by default: they register a new test site, send real logs, and verify analytics and migrate. No env vars required. Needs network access.
Publishing the SDK (npm)
The SDK is an npm package. “Deploy” means publishing to the registry so users can npm install apptvty.
Prerequisites
- npm account (npmjs.com)
- Logged in:
npm login
Release steps
Bump version in
package.json(or usenpm version):npm version patch # 0.1.0 → 0.1.1 # or: npm version minor # 0.1.0 → 0.2.0Run tests (optional but recommended):
npm run test:unit npm run test:integration # uses api.apptvty.comPublish:
npm publishprepublishOnlyrunsnpm run build && npm run typecheckautomatically before the package is uploaded.
For a scoped package (e.g. @apptvty/sdk), make sure package.json has "name": "@apptvty/sdk" and use:
npm publish --access publicLicense
MIT
