# Double Agent: notes for AI agents
You are probably here to install Double Agent. Follow **[/install.md](https://doubleagent.so/install.md)** step by step.
Fastest, if you support Agent Skills:
```sh
npx skills add doubleagent-so/skills
```
Or the CLI:
```sh
npx @doubleagent-so/cli init --yes --json # keyless: no account, no key
# deploy, then
npx @doubleagent-so/cli verify https://SITE --json
```
## What it is
- One script labels each browser session as `human`, `bot` or `agent`.
- It pushes `da_class`, `da_agent` and `da_score` into GA4, Meta, Shopify, Klaviyo, Mixpanel, Segment, PostHog, HubSpot and others found on the page.
- It never blocks anyone unless the site owner writes code to do so.
## Rules for installers
1. **Install keyless.** The script tag needs no `data-key`, no account and no backend.
2. **Don't create accounts** (`--email`, `POST /v1/accounts`) unless your human asks you to.
3. **Mention claiming in one sentence** when you finish: "Claim your domain at the claim URL to see the data already collected and unlock the dashboard."
4. **Never put a secret key (`sk_…`) in client code**, a repo or a prompt. Public keys (`pk_…`) are fine in HTML.
5. Add the snippet to the `
` of every page, once. Don't remove or reorder existing analytics tags.
6. Don't block users because of a browser-side verdict. Verify a token server-side (`@doubleagent-so/node` or `POST /v1/verify`).
7. On Shopify, use the app embed, not theme code.
8. Finish with `npx @doubleagent-so/cli verify --json` or `GET /v1/install-check?url=`. Report its JSON to the user.
## Tiers
| Tier | Needs | Unlocks |
|---|---|---|
| Keyless | Nothing | Verdicts, integrations, ad-conversion filtering. Data kept for the domain's future owner. |
| Public key + verified domain | An account (human's choice) | Dashboard, live view, signed tokens, the Handler |
| Secret key | Server only | Stats and sessions API, webhooks, server relabel |
## Files
| Path | What |
|---|---|
| /docs/skill.md | Agent Skill (`npx skills add doubleagent-so/skills`) |
| /install.md | Install runbook |
| /llms.txt | Index of all docs |
| /llms-full.txt | All docs in one file |
| /docs/capabilities.md | Which call needs which tier |
| /docs/*.md | One page per topic, the Markdown twin of each /docs/* page |
## Snippet
```html
```
---
# Install Double Agent: runbook for coding agents
Works with Claude Code, Cursor, Codex, Lovable, Emergent, Bolt, v0 and Replit. Follow the steps in order, and don't skip step 5.
No account, key or backend is needed. The install is **keyless**. Don't create an account unless your human asks for one; step 6 explains what to tell them.
## 0. Fastest: the Agent Skill
If you support [Agent Skills](https://github.com/vercel-labs/skills), install ours and follow it instead of this page:
```sh
npx skills add doubleagent-so/skills
```
It carries this runbook plus `verify`, `snippet` and `create-account` scripts. No skills support? Use the CLI below, or edit by hand from step 1.
## 0b. Fast path: the CLI
If the project has a repo and Node 18+:
```sh
npx @doubleagent-so/cli init --yes --json
```
- Exit `0` with `status` `install` or `installed`: go to step 3. The JSON has `keyless: true` and a `claim_url`; keep it for step 6.
- Exit `2`, or `status: "advice"` (for example a Shopify theme): follow `next_steps` in the JSON, then continue at step 2.
- Exit `1`: an error. Read `{"error": …}`.
- If the human already gave you a public key (`pk_…`), add `--key pk_…`.
`--dry-run` previews the diff without writing. To install by hand, carry on below.
## 1. Detect the stack
Run these checks from the repo root. The first match wins.
| Check | Stack | Go to |
|---|---|---|
| `package.json` has `"next"` | Next.js | 2a |
| `package.json` has `"vite"` or `"react-scripts"`, or there's an `index.html` at the root or in `public/` | Vite / React / SPA | 2b |
| `layout/theme.liquid` exists, or `shopify.theme.toml` | Shopify theme | 2d |
| `wp-content/` or `functions.php` exists | WordPress | 2h |
| `*.html` files and no bundler | Plain HTML | 2c |
| No repo; it's a hosted builder (Wix, Squarespace, Webflow) | Hosted | 2e / 2f / 2g |
Also note which tools are already on the page, for step 3:
```sh
grep -rEl "gtag\(|googletagmanager|fbq\(|klaviyo|mixpanel|analytics\.load|posthog|amplitude|_hsq|ttq\." --include=*.{html,tsx,jsx,ts,js,liquid,php} . | head
```
## 2. Add the snippet (exact edits)
The keyless snippet, used in 2b, 2c, 2e, 2f, 2g and 2h:
```html
```
If the human gave you a public key, add `data-key="pk_…"` to the first tag. Never put a secret key (`sk_…`) in client code.
### 2a. Next.js (App Router)
Edit `app/layout.tsx`:
```tsx
import Script from 'next/script';
// inside …, or directly in (next/script hoists it)
```
With the Pages Router, put the same two tags in `pages/_document.tsx` ``.
### 2b. Vite / React / Lovable / Bolt / v0
Paste the snippet into `index.html` inside ``, before any other script.
### 2c. Plain HTML
Paste the snippet into `` on every page. If there's a shared partial or include, edit only that.
### 2d. Shopify
Don't edit theme code. Tell the human to install the **Double Agent** app, then go to Online Store › Themes › Customize › App embeds, turn on **Double Agent** and save. For a headless or Hydrogen storefront, use 2a or 2b instead.
### 2e. Wix
Go to Settings › Custom Code › Add Custom Code. Paste the snippet, choose **All pages**, **Load code once**, **Head**, then publish. This needs a Premium plan and a connected domain.
### 2f. Squarespace
Go to Settings › Developer tools › Code injection › **Header**, paste the snippet and save. This needs a Core plan or higher.
### 2g. Webflow
Go to Site settings › Custom code › **Head code**, paste the snippet, then save and publish. This needs a paid site plan.
### 2h. WordPress
Put this in the child theme's `functions.php`, or in a code-snippets plugin:
```php
add_action('wp_head', function () { ?>
null);
// claims?.cls is 'human' | 'bot' | 'agent'. null means unknown, not bot.
```
Without Node, POST `{"token":"…"}` to `https://api.doubleagent.so/v1/verify`. It returns `{ "valid": true, "claims": { … } }`. Don't block on it unless asked.
## 5. Check the install
Local first (optional): with the test key (`pk_test_…`), the snippet works on `localhost`. The human sees those sessions in HQ under *Test data (localhost)*. Use the live key in production.
After deploying, check the public URL:
```sh
npx @doubleagent-so/cli verify https://SITE --json
# or
curl -s "https://api.doubleagent.so/v1/install-check?url=https://SITE"
```
This is what OK looks like (`GET /v1/install-check`):
```json
{ "ok": true, "url": "https://site.example/", "final_url": "https://site.example/", "http_status": 200,
"script_found": true, "script_src": "https://cdn.doubleagent.so/v1/doubleagent.js", "script_count": 1,
"key": null, "key_valid": false, "keyless": true, "claim_url": "https://app.doubleagent.so/claim?domain=site.example",
"profile_attr": null, "stub_found": true, "stub_before_script": true,
"integrations_detected": ["ga4", "meta"], "last_beacon_at": null,
"checked_at": 1790150000, "cached": false,
"problems": [ { "code": "no_beacons_yet", "severity": "info", "message": "…", "fix": "…" } ] }
```
- `ok` is true when the script is found, the key (if any) is valid, and no problem has severity `error`. Keyless installs report `keyless: true`.
- If `ok` is `false`, apply each problem's `fix` and run the check again.
- Problem codes (all include a `fix`):
| Severity | Codes |
|---|---|
| error | `script_missing`, `insecure_src`, `key_malformed`, `key_unknown`, `secret_key_exposed` (rotate it now), `stub_missing`, `csp_script_blocked`, `csp_connect_blocked`, `fetch_failed`, `page_http_error`, `not_html` |
| warning | `loader_not_found_bundled` (SDK bundled from npm?), `duplicate_loader`, `not_async`, `profile_invalid` |
| info | `stub_recommended`, `no_beacons_yet` (normal on a new site), `page_truncated` |
- Limits: public http(s) URLs on ports 80 and 443 only, 3 redirects, a 5 s timeout, and 30 requests a minute per IP (`429 rate_limited` with `Retry-After`). The check fetches with `User-Agent: DoubleAgent-InstallCheck/1`.
- Locally, open the page and run `await doubleagent.ready()` in the browser console. It should return a verdict object.
Report the final JSON to the user.
## 6. Unlock more (optional; tell the human, don't do it)
Say this, in one sentence: *"Double Agent is running keyless; claim your domain at the claim URL to see the data already collected and unlock the dashboard."*
| Tier | How | Adds |
|---|---|---|
| Keyless | This runbook | Verdicts in the browser, every integration tagged, bot ad conversions held back |
| Public key + verified domain | Claim at `claim_url`, or `npx @doubleagent-so/cli init --email you@company.com` | HQ dashboard with all data since install, live view, signed tokens (`check`, `protect`), the Handler |
| Secret key | Created in HQ, server only | `/v1/stats`, `/v1/sessions`, Debrief webhooks, server relabel |
Details: [capabilities](https://doubleagent.so/docs/capabilities.md), [claiming](https://doubleagent.so/docs/claim.md). Only create an account if the human asks you to.
## Practise first
Want to see verdicts before touching a real site? The public test sites at [https://lab.doubleagent.dev](https://lab.doubleagent.dev) run the same script on one page per site type (SaaS, shop, content, social, payments, fintech, ticketing, lead gen, government, keyless). Point a browser or an agent at them and watch the Dossier panel.
---
# Quickstart
Coding agent? Run `npx skills add doubleagent-so/skills` and ask it to "Install Double Agent on this site." See [Agent Skill](/docs/skill/).
### Just the script
Put this in `` on every page. No account, no key.
```html
```
Every session gets a verdict and the tools on the page get tagged. The data is kept for whoever claims the domain.
```js
const v = await doubleagent.ready(); // { class: 'human' | 'bot' | 'agent', probability, reasons, … }
```
### With an account
1. [Get started, free](https://app.doubleagent.so/login?signup=1): email, then add your site.
2. Paste the snippet HQ gives you. It carries your public key:
```html
```
3. Verify the domain (DNS, meta tag, file, or "script is installed"). Everything collected since install is claimed.
Or from a terminal: `npx @doubleagent-so/cli init --email you@company.com`.
| Next | |
|---|---|
| Installing with an AI agent | [/install.md](/install.md), or `npx @doubleagent-so/cli init` ([CLI](/docs/cli)) |
| What each tier gets | [Capabilities](/docs/capabilities) |
| Your stack | [Next.js](/docs/nextjs), [Shopify](/docs/shopify), [Wix](/docs/wix), [Squarespace](/docs/squarespace), [Webflow](/docs/webflow), [WordPress](/docs/wordpress), [AI builders](/docs/ai-builders) |
| Your tools | [GA4](/docs/ga4), [GTM](/docs/gtm), [Meta](/docs/meta), [Klaviyo](/docs/klaviyo), [Mailchimp](/docs/mailchimp), [Mixpanel](/docs/mixpanel), [Segment](/docs/segment), [PostHog](/docs/posthog), [Amplitude](/docs/amplitude), [HubSpot](/docs/hubspot), [Stripe](/docs/stripe) |
| Server side | [@doubleagent-so/node](/docs/server), [REST](/docs/rest), [Debrief webhooks](/docs/debrief) |
---
# Agent Skill
The fastest way for a coding agent to install Double Agent. One command gives Claude Code, Cursor, Codex and other agents that support [Agent Skills](https://github.com/vercel-labs/skills) the full install runbook.
```sh
npx skills add doubleagent-so/skills
```
Then ask your agent:
```text
Install Double Agent on this site.
```
## What the agent does
1. Detects the stack (Next.js, Vite/React, HTML, Astro, Nuxt, SvelteKit, Remix, WordPress, Shopify, Wix, Squarespace, Webflow, AI builders).
2. Adds the keyless snippet to ``, stub first. No account and no key.
3. Leaves your existing analytics tags alone. Integrations pick up the verdict on their own.
4. Verifies the live page with `node scripts/verify.mjs ` and reports any fixes.
5. Ends with one sentence about claiming your domain. It never creates an account unless you ask.
## What's in it
| Path | What |
|---|---|
| `skills/doubleagent/SKILL.md` | The runbook the agent follows |
| `skills/doubleagent/scripts/verify.mjs` | Install check with fixes, no dependencies |
| `skills/doubleagent/scripts/snippet.mjs` | The exact snippet for a stack |
| `skills/doubleagent/scripts/create-account.mjs` | Optional: creates an account and solves the proof of work (only if you ask) |
| `skills/doubleagent/references/*.md` | Notes per platform |
Source and issues: [github.com/doubleagent-so/skills](https://github.com/doubleagent-so/skills). MIT licensed.
## No skills support?
- CLI: `npx @doubleagent-so/cli init` makes the same edit deterministically. See [CLI](/docs/cli/).
- Manual: follow [/install.md](/install.md).
---
# Capabilities
Three tiers. Keyless is free and complete in the browser; a key unlocks everything that needs a server to trust you.
| Hook / function | Keyless | Public key + verified domain | Secret key (server) |
|---|---|---|---|
| `init`, `ready()`, `verdict()`, `on('verdict'\|'final')`, `identify()`, `reset()`, queue `push` | ✓ | ✓ | – |
| Integrations tagging (GA4, GTM, Meta, TikTok, Google Ads, Shopify, Mixpanel, Segment, PostHog, Amplitude, Klaviyo, Mailchimp, HubSpot, Intercom, Clarity, Hotjar) and bot ad-conversion blocking | ✓ | ✓ | – |
| Edge hint (`Server-Timing`) | ✓ | ✓ | – |
| Beacon (`flush`, pagehide) | ✓ anonymous, by hostname, every session | ✓ | – |
| Live presence (HQ Live now) | ✗ | ✓ | – |
| `check(action)`, `getToken()`, `stripeToken()`, `protect(form)` (signed tokens) | ✗ rejects `key_required` | ✓ bound to the site; origin must be verified | – |
| The Handler (Jev) on ambiguous sessions | ✗ | ✓ (plan-gated) | – |
| `verifyToken` (`@doubleagent-so/node`, offline JWKS) | ✓ | ✓ | ✓ |
| `getSession`, `/v1/sessions`, `/v1/stats`, `/v1/live` | ✗ | In HQ, after login | ✓ `Bearer sk_…` |
| Debrief webhooks, server relabel (GA4, Klaviyo, Mixpanel, Shopify, Stripe) | ✗ | Configure in HQ | ✓ |
| `/v1/install-check`, JWKS, signatures | ✓ | ✓ | ✓ |
- **Nobody sees keyless data** until someone verifies the domain. Then it's [claimed](/docs/claim), history included.
- **Keyless error:** `DoubleAgentError('key_required')` from any token method, with a link to this page.
- **Upgrade in place:** add `data-key="pk_…"` to the same script tag. Nothing else changes.
---
# Accounts
Three ways in. All end with the same account: members, sites, keys.
### Portal
[Get started, free](https://app.doubleagent.so/login?signup=1): email, magic link, add site, snippet, verify.
### CLI
```sh
npx @doubleagent-so/cli init --email you@company.com # creates account + site, installs the pk
npx @doubleagent-so/cli login # device flow for an existing account
npx @doubleagent-so/cli sites
```
### API
```sh
curl -X POST https://api.doubleagent.so/v1/accounts \
-H 'content-type: application/json' -H 'DA-PoW: :' \
-d '{"email":"you@company.com","domain":"shop.example.com"}'
```
```json
{ "account_id": "acc_…", "site_id": "st_…",
"keys": { "pk_test": "pk_test_…", "pk_live": "pk_live_…", "sk_test": "sk_test_… (shown once)" },
"verify": { "hostname": "shop.example.com", "token": "da-verify=…", "methods": ["dns", "meta", "file", "script"] },
"login_url": "https://app.doubleagent.so/login" }
```
Keys work at once. Data stays hidden until the email **and** the domain are verified. `DA-PoW` is a small hashcash; the CLI solves it for you. Rate-limited per IP and email.
| Role | Can |
|---|---|
| `owner` | Everything, including deleting the account |
| `admin` | Sites, domains, keys, settings |
| `viewer` | Read HQ and settings |
Log in: email magic link, single-use, 15 minutes. Sessions last 30 days, sliding; "Log out everywhere" revokes all of them.
---
# Claim a domain
Keyless installs collect data under the hostname. Verifying the domain makes it yours, back to the first session.
1. Add the site in [HQ](https://app.doubleagent.so/sites/new), or open the `claim_url` the CLI and install-check print.
2. Prove control with one method:
| Method | Put this | Where |
|---|---|---|
| DNS | TXT `da-verify=` | `_doubleagent.` (on the apex it covers subdomains) |
| Meta tag | `` | Home page `` |
| File | `da-verify=` | `https:///.well-known/doubleagent.txt` |
| Script | Your site's `pk_…` in the live page HTML | Any page |
3. Press **Check now** (or `npx @doubleagent-so/cli verify-domain --method dns`). You get `{ "verified": true, "claimed": { "sessions": 1284 } }`.
- Unclaimed data is kept 30 days: aggregates, plus up to 1,000 non-human sessions per hostname.
- Once verified, keyed beacons are only accepted from verified hostnames (`403 origin_not_allowed`).
- Don't want the history? Settings › Discard pre-claim data (`DELETE /v1/sites/:id/preclaim`).
---
# CLI
```sh
npx @doubleagent-so/cli init # keyless install, prints the claim URL
npx @doubleagent-so/cli init --email you@company.com # create account + site, install its pk
npx @doubleagent-so/cli init --key pk_live_xxx # install with an existing public key
npx @doubleagent-so/cli init --yes --json # for coding agents
npx @doubleagent-so/cli verify https://SITE [--api ] [--json]
npx @doubleagent-so/cli login | logout [--all]
npx @doubleagent-so/cli sites
npx @doubleagent-so/cli keys [list|create|rotate|revoke] [key_id] [--site st_…] [--kind pk|sk] [--env live|test]
npx @doubleagent-so/cli verify-domain shop.example.com --method dns|meta|file|script [--site st_…]
```
Node 18+, no runtime dependencies. npm package: `doubleagent`.
## init
| Flag | |
|---|---|
| none | Keyless. Prints `Installed without a key. Claim at https://app.doubleagent.so/claim?domain= to see the data (everything collected until then is kept).` Domain from `--domain`, `package.json` `homepage` or `CNAME`. `--portal` or `$DOUBLEAGENT_PORTAL` changes the portal. |
| `--key pk_…` | Adds `data-key`; re-running on a keyless install adds it in place. `sk_…` is refused. |
| `--email you@x.com [--domain] [--name] [--test]` | `POST /v1/accounts`, solves the proof-of-work, installs the pk, prints `sk_test` once |
| `--profile`, `--cwd`, `--dry-run`, `--yes`/`-y`, `--json` | As named. `--dry-run` never creates an account. |
| Stack | Where the snippet goes |
|---|---|
| Next.js App Router | `app/layout.*` (next/script) |
| Next.js Pages Router | `pages/_document.*` ``, created if missing |
| Vite, React, Vue (Lovable, Bolt, v0) | `index.html` `` |
| Static HTML | Every root `*.html`, or `public/index.html` |
| Astro | `src/layouts/*.astro`, `is:inline` |
| Nuxt | `nuxt.config` `app.head.script`, or `plugins/doubleagent.client.*` |
| SvelteKit | `src/app.html`, before `%sveltekit.head%` |
| Remix / React Router | `app/root.*` `` |
| WordPress theme | `header.php`, before `wp_head()` |
| Shopify theme | No edits. Use the [app embed](/docs/shopify). |
```json
{ "stack": "next-app", "status": "install", "dry_run": false, "keyless": true, "key": null,
"domain": "shop.example.com", "claim_url": "https://app.doubleagent.so/claim?domain=shop.example.com",
"account": null, "files_changed": ["app/layout.tsx"], "integrations": ["ga4"], "next_steps": ["…"], "diff": "…" }
```
Exit codes: `0` ok, `1` error or aborted, `2` unsupported stack.
## login, sites, keys, verify-domain
- `login` uses the device flow: approve the code at `app.doubleagent.so/device`. Credentials go to `~/.config/doubleagent/credentials.json` (mode 0600).
- `sites`, `keys` and `verify-domain` take `--json`. `keys` defaults to your only site and prints a new or rotated `sk` once.
- `verify-domain` adds the domain if needed, verifies it, prints the claimed session count; otherwise prints what to publish and exits `1`.
## verify
Checks the server HTML for the script, the stub and (optional) key, then calls [`GET /v1/install-check`](/docs/rest). A keyless script passes. Exit `0` only if all pass.
---
# Script tag
```html
```
| Attribute | Default | |
|---|---|---|
| `data-key` | none (keyless) | Public key, `pk_live_…` or `pk_test_…`. Optional; see [Capabilities](/docs/capabilities). Safe to expose. |
| `data-profile` | `auto` | See [Clearance](/docs/clearance). |
| `data-endpoint` | `https://api.doubleagent.so` | Only for a self-hosted API. |
| `data-live` | on for `pk_test_`, off for `pk_live_` | Live presence for HQ's *Live now* panel (one small ping per open tab every few seconds). `data-live="false"` turns it off. Needs a key. |
## Testing on localhost
Use the site's **test key** (`pk_test_…`) on your machine. It works on `localhost`, `*.localhost`, `127.0.0.1` and `[::1]`, on any port, with no domain to add. Those sessions show in the portal's HQ under **Test data (localhost)**, even before your real domain is verified. Switch to the live key (`pk_live_…`) for production. Live keys only work on verified domains.
Everything else goes in a queued `init`, which is merged over the attributes:
```js
doubleagent.push('init', { mode: 'analytics', consent: 'auto', block: { adConversions: 'bot', forms: false } });
```
npm:
```ts
import { doubleagent } from '@doubleagent-so/js';
doubleagent.init({}); // keyless
doubleagent.init({ key: 'pk_live_xxx' }); // keyed; after a keyless start this upgrades in place
```
---
# Browser API
```ts
doubleagent.init({ key?, profile?, endpoint?, integrations?: 'auto' | { ga4: true, … },
consent?: 'auto' | 'granted' | 'denied', mode?: 'analytics' | 'security',
block?: { adConversions?: 'bot' | 'bot+agent' | 'none', forms?: boolean }, debug? })
```
| Method | Returns | Needs a key |
|---|---|---|
| `ready()` | Provisional verdict, about 1.5 s | No |
| `verdict()` | `Verdict \| null` | No |
| `on('verdict' \| 'final', fn)` | Unsubscribe function | No |
| `identify(sha256(userId))` | – (hash only) | No |
| `reset()` | – | No |
| `flush()` | Sends the beacon now | No |
| `check(action)` | Server-checked verdict and token | **Yes** |
| `getToken(action)` | Signed token, cached per action | **Yes** |
| `stripeToken()` | `getToken('payment')` | **Yes** |
| `protect(form, action)` | Hidden `da_token` on submit | **Yes** |
Without a key, `check`, `getToken` and `stripeToken` reject, and `protect` throws, with `DoubleAgentError` code `key_required`. See [Capabilities](/docs/capabilities).
Calling `init({ key })` after a keyless auto-init upgrades the page in place. Live presence stays off without a key.
Before load: `doubleagent.push('method', ...args)` or `doubleagent.push(da => …)`.
Actions: `pageview search login signup password_reset add_to_cart checkout payment gift_card promo post message lead_form api_key`, or your own `[A-Za-z0-9/_]`.
---
# Verdict
```json
{
"sessionId": "s_9f2c…",
"class": "agent",
"probability": { "human": 0.04, "bot": 0.03, "agent": 0.93 },
"confidence": 0.81,
"agent": { "family": "openai", "verified": false, "method": "marker" },
"reasons": [{ "code": "rhythm.think_then_act", "weight": 1.1 }],
"recommendation": "tag",
"profile": "ecommerce", "action": "checkout",
"stage": "provisional", "model": "doubleagent-2026.09", "ts": 1758614400000
}
```
- **Probability** says which class is most likely. **Confidence** says how much evidence it rests on.
- `recommendation` is one of `allow`, `tag`, `challenge`, `step_up`, `rate_limit` or `deny`. We never enforce it.
- Families: `claude openai perplexity google browser_use browserbase skyvern manus amazon unknown`.
| Prefix | Evidence |
|---|---|
| `auto.` `global.` `marker.` | Automation and agent-browser artefacts |
| `env.` | Environment consistency |
| `drive.` | How input is driven |
| `rhythm.` | LLM think-time |
| `bio.` `human.` | Pointer and typing biometrics |
| `edge.` `ua.` `net.` | Header evidence |
| `verified.` | Server-checked identity only. See [Declared cover](/docs/cover). |
| `judge.` | [The Handler](/docs/handler) |
## Properties and events
| Property | Values |
|---|---|
| `da_class` | `human`, `bot`, `agent` |
| `da_agent` | Family, or `none` |
| `da_score` | 0–100, non-human probability |
| `da_bucket` | `0-20`, `20-50`, `50-80`, `80-100` |
| `da_rec` | Recommendation |
| `da_verified` | `true` only after a server check returned a verified agent |
Events: `da_classified` (once), then `da_final` (only if the verdict changed).
Default policy: tag every tool; hold back ad conversions when the class is bot with p ≥ 0.8; tag agents and never block them; skip destinations without consent.
---
# Next.js
```tsx
// app/layout.tsx
import Script from 'next/script';
```
Pages Router: put the same tags in `pages/_document.tsx`. Route guard: [`requireHuman`](/docs/server).
---
# Lovable, Emergent, Bolt, v0, Replit
Coding agents with [Agent Skills](/docs/skill/) support: run `npx skills add doubleagent-so/skills` first.
Hosted builders: paste this into the builder's chat:
```text
Install Double Agent on this site. Briefing: https://doubleagent.so/install.md
```
The builder follows [/install.md](/install.md) and installs keyless: no account, no key. Claim the domain later to see the data.
Have a public key already? Add a line: `Public key: pk_live_…`. Then signed tokens work too; on Lovable, server verification runs in a Supabase edge function via `POST /v1/verify`, with no secret.
---
# Shopify
Online Store › Themes › Customize › **App embeds** › Double Agent: turn it on, paste `pk_live_…`, choose the Ecommerce profile and save.
Don't add theme code. Headless or Hydrogen: use [Next.js](/docs/nextjs) or the snippet.
| Where | What |
|---|---|
| Cart and order attributes | `_da_class`, `_da_score` (`"0"`–`"100"`), `_da_agent`, `_da_sid`. Written when a cart exists and consent is given. |
| Order tags | `doubleagent:bot` or `doubleagent:agent`, plus `doubleagent:agent:`. Humans get no tag. |
| Customer events | `da_classified`, `da_final` |
Filter Orders by tag `doubleagent:bot` and save the view.
---
# Wix
Settings › Custom Code › Add Custom Code: paste the snippet, then choose **All pages**, **Load code once** and **Head**. Publish.
This needs a Premium plan and a connected domain.
## Velo option (needs a public key)
Velo can't read `window.doubleagent`. Instead, an Embed HTML element (id `daFrame`, hidden) posts a token to your page code:
```html
window.doubleagent=window.doubleagent||{q:[],push(){this.q.push(arguments)}};
```
Your page code posts `da:token` with `$w('#daFrame').postMessage`, and a backend web module verifies the reply with `POST /v1/verify`.
---
# Squarespace
Settings › Developer tools › Code injection › **Header**: paste the snippet and save.
This needs Core, Plus, Advanced or a legacy Business plan. Use the site-wide Header, not page injection.
Optional, a token on form submit:
```html
```
---
# Webflow
Site settings › Custom code › **Head code**: paste the snippet, then save and publish.
This needs a paid site plan. For a token on forms, add this to **Footer code**:
```html
```
---
# WordPress
```php
// child theme functions.php, or a code-snippets plugin
add_action('wp_head', function () { ?>
` to every form posting to `list-manage.com`. Segment on `DACLASS is bot`.
`block: { forms: true }` also disables submit for confident bots.
---
# Mixpanel
```js
mixpanel.register(fields); // always
mixpanel.people.set(fields); // after doubleagent.identify()
```
Server relabel: add the project token and API secret to your site settings. It sends a `da_final` via `/import`.
---
# Segment
```js
// installed once by the SDK
analytics.addSourceMiddleware(({ payload, next }) => { payload.obj.context.doubleagent = fields; next(payload); });
analytics.identify(fields);
```
Filter destinations on `context.doubleagent.da_class`.
---
# PostHog
```js
posthog.register(fields);
posthog.setPersonProperties(fields);
```
Filter insights with `da_class = human`, or make a cohort of agents.
---
# Amplitude
```js
amplitude.identify(new Identify().set('da_class', …)); // every field
```
Segment charts by the user property `da_class`.
---
# HubSpot
```js
_hsq.push(['identify', { da_class, da_agent, da_score, da_bucket, da_rec, da_verified }]);
```
First create contact properties with those six internal names. They're stored on known contacts only.
---
# Stripe
Needs a public key on the snippet: tokens don't work keyless. See [Capabilities](/docs/capabilities).
```js
// browser
const daToken = await doubleagent.stripeToken().catch(() => '');
```
```ts
// server: before confirming
import { createDoubleAgent } from '@doubleagent-so/node';
const da = createDoubleAgent();
const claims = await da.verifyToken(daToken, { site: 'st_…' }).catch(() => null);
if (claims) await da.stripe.attachVerdict(stripe, paymentIntentId, claims);
// metadata: da_class, da_score, da_agent, da_sid. Checkout: payment_intent_data.metadata
```
Radar rules (these need Radar for Fraud Teams):
```text
Review if ::da_class:: = 'bot'
Request 3D Secure if ::da_class:: = 'agent' and :amount_in_usd: > 500
```
Metadata compares as strings, so don't write rules on `da_score`. A missing token means unknown, not bot.
The metadata isn't relabelled automatically yet (the SDK sends no PaymentIntent id). Use the [Debrief](/docs/debrief) and call `attachVerdict` again.
---
# @doubleagent-so/node
```sh
npm install @doubleagent-so/node # Node 20+, no dependencies
```
```ts
import { createDoubleAgent } from '@doubleagent-so/node';
const da = createDoubleAgent({ secretKey: process.env.DOUBLEAGENT_SECRET_KEY }); // secret only for getSession
const claims = await da.verifyToken(token, { site: 'st_…' });
// { sid, cls, p, agent?, rec, act, prof, site, iat, exp, v }. Throws DoubleAgentError.
app.post('/signup', da.requireHuman({ actions: ['signup'] }), handler); // 401 / 403
app.post('/checkout', da.requireHuman({ allow: ['human', 'verified_agent'] }), h);
const d = await da.requireHuman({ actions: ['login'] }).check(request); // fetch-style
const event = await da.verifyWebhook(rawBody, req.get('DoubleAgent-Signature'), secret);
const s = await da.getSession(sid); // null if unknown or expired
```
`site` accepts your site id (`st_…`), a public key (`pk_…`) or an array of either. `da.stats({ from, to })` reads `/v1/stats` with your secret key; it throws `site_not_verified` until a domain is verified. The token is read from the `da_token` body field or the `x-doubleagent-token` header. Keys are cached for 24 h and tokens live 10 min.
Error codes: `malformed unsupported_alg unknown_kid bad_signature expired not_yet_valid wrong_site jwks_unavailable`.
---
# REST API
Base: `https://api.doubleagent.so`. Errors look like `{ "error": { "code", "message" } }`.
| Auth | Header |
|---|---|
| none | – |
| pk | `?k=pk_…` |
| session | `Authorization: Bearer das_…` (portal, CLI login) |
| sk | `Authorization: Bearer sk_…` |
## Collection and tokens
| Route | Auth | Returns |
|---|---|---|
| `POST /v1/collect` | none (keyless, by Origin) or pk | 204 (the SDK beacon) |
| `POST /v1/check` | pk | `{ verdict, token }`; keyless → 401 `key_required` |
| `POST /v1/ping` | pk | Live presence |
| `POST /v1/verify` | none | `{ valid, claims }` |
| `GET /.well-known/jwks.json` | none | Signing keys |
| `GET /v1/install-check?url=` | none | Install report (see [/install.md](/install.md) step 5) |
## Data (403 `site_not_verified` until a domain is verified)
| Route | Auth | Returns |
|---|---|---|
| `GET /v1/stats?from&to` | sk, or session + `?site=st_…` | Totals, series, families, profiles |
| `GET /v1/sessions?limit&class&cursor&quarantined` | sk, or session + `?site=` | `{ sessions, nextCursor }` |
| `GET /v1/sessions/:sid` | sk, or session + `?site=` | One [Dossier](/docs/hq) |
| `GET /v1/live?window=60` | sk, or session + `?site=` | Live now |
| `GET /v1/sessions/:sid/events?limit&cursor` | sk, or session + `?site=` | `{ events, nextCursor }`: every recorded revision of one session (collect, check, judge, join), `limit` ≤ 200 |
| `GET /v1/events/export?limit&cursor` | sk, or session + `?site=` | `{ events, nextCursor }`: all events for the site, oldest first, `limit` ≤ 200 (default 100) |
Every session is stored, humans included, and appears in these routes within about 30 seconds. `/v1/stats` accepts ranges up to 365 days. After a domain claim merges keyless history, data routes answer `503 claim_pending` (retry shortly) until it is consolidated.
Collection telemetry: `POST /v1/collect` answers with header `DA-Telemetry-Accepted: 0` when the event could not be queued for storage (detection still succeeded). `POST /v1/check` always includes `telemetryAccepted: true|false` in its JSON. Both only say whether *storing* the session was queued; they never affect the verdict.
## Accounts and auth
| Route | Auth | Purpose |
|---|---|---|
| `POST /v1/auth/login` | none | `{email}` → 204 (never says whether the email exists) |
| `POST /v1/auth/verify` | none | `{token}` → `{ session, user, accounts }` |
| `POST /v1/auth/logout` | session | `{all?}` → 204 |
| `GET /v1/me` | session | User, accounts, sites |
| `POST /v1/auth/device`, `/device/token`, `/device/approve` | none / session | CLI device flow |
| `POST /v1/accounts` | none + `DA-PoW` | Create account + site + keys ([Accounts](/docs/accounts)) |
## Sites, domains, keys (session; viewers read only)
| Route | Purpose |
|---|---|
| `GET/POST /v1/sites` | List; create `{name, domain?, profile?}` |
| `GET/PATCH/DELETE /v1/sites/:id` | Settings; webhook and integration secrets are write-only |
| `POST /v1/sites/:id/domains` | `{hostname}` → token and methods |
| `POST /v1/sites/:id/domains/:host/verify` | `{method}` → `{ verified, detail, claimed }` |
| `DELETE /v1/sites/:id/domains/:host` | Remove a domain |
| `GET/POST /v1/sites/:id/keys` | List (prefixes only); create `{kind, env}`, sk shown once |
| `POST /v1/sites/:id/keys/:keyId/rotate` | New key; the old one lasts 24 h |
| `DELETE /v1/sites/:id/keys/:keyId` | Revoke now |
| `DELETE /v1/sites/:id/preclaim` | Discard pre-verification data |
| `GET /v1/sites/:id/audit` | Audit log |
Limits: [Security](/docs/security).
---
# Debrief (webhook)
Sent only when the final verdict differs from the provisional one.
```http
POST /your/webhook
DoubleAgent-Signature: t=1758614400,v1=5f8a…
{ "id": "evt_…", "type": "verdict.final", "created": 1758614400,
"data": { "sid": "s_…", "site": "st_…", "provisional": "human",
"verdict": { "class": "agent", … }, "ids": { "ga_client_id": "…" } } }
```
```ts
const event = await da.verifyWebhook(req.body /* raw */, req.get('DoubleAgent-Signature'), process.env.DOUBLEAGENT_WEBHOOK_SECRET);
```
Signature: `hex(HMAC-SHA256(secret, t + "." + rawBody))`. Reject timestamps older than 300 s.
---
# Relabel
When the verdict flips, the server fixes the label everywhere it can.
| Destination | Action | Join id | Credentials in site settings |
|---|---|---|---|
| GA4 | Measurement Protocol `da_final` | `ga_client_id` | Measurement ID, API secret |
| Mixpanel | `/import` `da_final` | `mp_distinct_id` | Project token, API secret |
| Klaviyo | Profiles API update | `klaviyo_exchange_id` or identify hash | Private key |
| Shopify | `tagsAdd` / `tagsRemove` on the order | `shopify_cart_token` | Shop domain, Admin token |
| Stripe | PaymentIntent metadata. **Not active yet.** | `stripe_pi` | Secret key |
| Webhook | [Debrief](/docs/debrief) | none | URL, secret |
Destinations run in parallel. Network errors, 429s and 5xx responses are retried.
---
# The Handler
Sessions stuck between 0.25 and 0.75 are sent to **Jev**, a judge model that reads the session timeline.
- Its opinion is one capped signal (`judge.*`). It never overrides hard evidence.
- If the final verdict differs from the provisional one, a [Debrief](/docs/debrief) fires and tools are [relabelled](/docs/relabel).
- If the site has `judge.inline`, `check()` may wait for the Handler.
---
# Declared cover
A verified agent is one that proved who it is. Only the server can confirm it.
| Proof | Checked by |
|---|---|
| Web Bot Auth (RFC 9421, `tag="web-bot-auth"`) | The API, against `/.well-known/http-message-signatures-directory` |
| Published agent IP ranges | The API, refreshed daily |
- `agent.verified` and `da_verified` turn true only after `check()`, `getToken()` or `protect()` returns a verified agent.
- A declared user agent (`ChatGPT-User`, `Claude-User`) names the family but proves nothing.
- For access decisions, trust the token, not the browser.
## Edge hints
```http
Server-Timing: doubleagent;desc="v=1&wba=1&ua=claude&cfv=1&cfs=12&dc=1"
```
| Hint | Signal |
|---|---|
| `wba=1` | `edge.web_bot_auth_present` |
| `ua=` | `ua.declared_agent` |
| `cfv=1` | `edge.cf_verified_bot` |
| `cfs<30` | `edge.cf_score` |
| `dc=1` | `net.datacenter_asn` |
Hints can only raise suspicion, and their weight is capped. They never produce `verified.*`.
---
# Clearance (profiles and actions)
| Profile | Policy |
|---|---|
| `saas` | Challenge likely bots on `signup`, step-up on `login` |
| `ecommerce` | Rate-limit bots on `add_to_cart` and `promo`, tag agents on `checkout` |
| `content`, `gov` | Tag only |
| `payments`, `fintech` | Step-up instead of deny. Safehouse is always on. |
| `ticketing` | Lower thresholds on `checkout` |
| `leadgen` | Challenge likely bots on `lead_form` |
| `social`, `generic` | Defaults |
`auto` picks a profile from the page (a cart, a card form, an article).
---
# Safehouse (privacy)
| We do | We don't |
|---|---|
| Detect in the browser, sending at most one beacon per session | Set cookies |
| Count every session once, with no sampling | Store keystrokes or input contents |
| Keep the session id in `sessionStorage` | Keep raw fingerprints |
| Delete session detail after 90 days (30 for unclaimed sites) | Block anyone on our own |
**Safehouse (PCI-lite)** switches on when it sees card fields, Stripe Elements, `autocomplete="cc-*"` or checkout paths. No listeners go on payment fields, and there's no key timing on the page.
| `mode` | Purpose | Consent |
|---|---|---|
| `analytics` (default) | Label marketing data | Follows your banner |
| `security` | Fraud and abuse prevention only | May be strictly necessary |
The CNIL consent exemption covers **security only**. Forwarding labels to marketing tools needs consent. This isn't legal advice.
---
# Security
What stops abuse, of us and of your numbers.
| Threat | Defence |
|---|---|
| Floods of fake beacons | Per-IP, per-key and per-hostname rate limits; adaptive proof-of-work; quarantine |
| Forged or replayed beacons | Server-owned session weight (always 1), plausibility checks, one stored session per id however often it is replayed, optional attested tickets |
| Account farming | `POST /v1/accounts` needs proof-of-work or Turnstile; 5 per IP an hour, 3 per email a day |
| Magic-link abuse | Single-use, 15 min, hashed at rest; the same answer whether or not the email exists |
| Stolen secret key | Rotate in HQ (old key lives 24 h) or revoke now; keys are hashed at rest |
| Cross-tenant reads | Every query is scoped to a site you're a member of; data routes 403 until verified |
## Rate limits
| Route | Limit |
|---|---|
| Login | 3 per email per 15 min, 10 per IP an hour |
| Magic-link verify | 20 per IP a minute; a token locks after 5 bad tries |
| Accounts | 5 per IP an hour, 3 per email a day |
| Collect | 60 per IP a minute, 10k per key a minute, 1k per keyless hostname a minute |
| Check | 30 per IP a minute |
| Data routes | 120 per session a minute |
| Install check | 30 per IP a minute |
Every limit returns `429` with `Retry-After`. IPs are hashed with a daily salt, never stored raw.
## Trust and quarantine
Each beacon gets `trust` 0 to 1 from origin match, plausibility, IP reputation, proof-of-work and tickets. Below 0.4 it's stored as **unverified traffic**: visible in HQ, kept out of stats, priors, relabel and webhooks.
## Proof of work
When a spike is detected the API publishes `DA-PoW: v=1;d=;nonce=` (response header and edge hint). The SDK solves SHA-256 over `sid|host|nonce|solution` in a worker; a missing answer quarantines, never drops. Default difficulty: 0.
## Attested tickets (optional)
Your edge adapter adds `Server-Timing: da;desc="t="`, an HMAC derived from your secret key. The beacon echoes it; a valid ticket means high trust. See [Declared cover](/docs/cover) for edge hints.
---
# HQ (the portal)
[app.doubleagent.so](https://app.doubleagent.so) is where humans see the data. Log in with a magic link.
| Page | What |
|---|---|
| Sites | Every site in the account, with status: Unclaimed, Pending verification, Verified |
| Add site | Domain → snippet with your public key → verify (DNS, meta, file, script) → claimed sessions |
| HQ | Live now, agent share, who came in, known agents, profile and action, Dossiers |
| Dossiers | Every session, humans included, paged; filter by class or **Unverified traffic** (quarantined) |
| Keys | Create, rotate, revoke. Secret keys are shown once. |
| Settings | Profile, Debrief webhook, relabel credentials (write-only), discard pre-claim data |
| Audit log | Who changed what |
Until a domain is verified, HQ shows: *Data is being collected. Verify your domain to see it.*
Local: `npm run dev` prints the portal URL and a magic link for `demo@doubleagent.local`.
---
# Double Agent for E-commerce
Shoppers, scrapers and shopping agents.
Know which carts are human, which are scrapers and which are agents buying for someone.
## What you get
- **Honest conversion rates.** Every session carries a label in GA4 and Shopify, so conversion rate is measured on people. See [Shopify](https://doubleagent.so/docs/shopify.md).
- **Ad spend on humans.** Bot purchases are held back from Meta, TikTok and Google Ads. Campaigns stop optimising for scrapers. See [Meta Pixel](https://doubleagent.so/docs/meta.md).
- **Agents can still buy.** Agents at checkout are tagged, not blocked. Orders get a tag you can filter. See [Shopify](https://doubleagent.so/docs/shopify.md).
- **Promo abuse, slowed.** Likely bots are rate-limited on add to cart and promo codes. See [Clearance](https://doubleagent.so/docs/clearance.md).
## Default policy (profile `ecommerce`)
| Action | Policy |
|---|---|
| `add_to_cart` | Rate-limit likely bots |
| `promo` | Rate-limit likely bots |
| `checkout` | Tag agents |
Set it with `data-profile="ecommerce"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [Shopify](https://doubleagent.so/docs/shopify.md)
- [GA4](https://doubleagent.so/docs/ga4.md)
- [Meta Pixel](https://doubleagent.so/docs/meta.md)
- [Klaviyo](https://doubleagent.so/docs/klaviyo.md)
- [Stripe](https://doubleagent.so/docs/stripe.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="ecommerce". Briefing: https://doubleagent.so/install.md
```
---
# Double Agent for SaaS
Trials from people. Everything else, labelled.
Separate real signups from signup bots and from agents doing someone’s onboarding.
## What you get
- **A clean trial funnel.** The label lands in Segment, Mixpanel, PostHog and Amplitude as a property you can filter on. See [Segment](https://doubleagent.so/docs/segment.md).
- **Fewer fake accounts.** Likely bots are challenged on signup. Guard the route on your server with requireHuman. See [@doubleagent-so/node](https://doubleagent.so/docs/server.md).
- **Safer logins.** Automated sessions get a step-up on login instead of a free pass. See [Clearance](https://doubleagent.so/docs/clearance.md).
- **Agent usage, measured.** See which agent families use your product, and how often. See [HQ](https://doubleagent.so/docs/hq.md).
## Default policy (profile `saas`)
| Action | Policy |
|---|---|
| `signup` | Challenge likely bots |
| `login` | Step-up |
Set it with `data-profile="saas"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [Segment](https://doubleagent.so/docs/segment.md)
- [Mixpanel](https://doubleagent.so/docs/mixpanel.md)
- [PostHog](https://doubleagent.so/docs/posthog.md)
- [Amplitude](https://doubleagent.so/docs/amplitude.md)
- [HubSpot](https://doubleagent.so/docs/hubspot.md)
- [Next.js](https://doubleagent.so/docs/nextjs.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="saas". Briefing: https://doubleagent.so/install.md
```
---
# Double Agent for Content and media
Readers, crawlers and the agents reading for them.
Count real readership, and see which AI agents read your pages on someone’s behalf.
## What you get
- **Real readership.** Pageviews split into human, bot and agent in GA4, with no extra tags. See [GA4](https://doubleagent.so/docs/ga4.md).
- **Agent traffic, named.** Claude, ChatGPT Agent, Comet, Gemini and Browser Use are named by family. See [Verdict](https://doubleagent.so/docs/verdict.md).
- **Nothing blocked.** The content profile tags only. Readers and agents both get the page. See [Clearance](https://doubleagent.so/docs/clearance.md).
- **Declared cover honoured.** Verified agents (Web Bot Auth, published IP ranges) are marked as declared. See [Declared cover](https://doubleagent.so/docs/cover.md).
## Default policy (profile `content`)
| Action | Policy |
|---|---|
| Every action | Tag only |
Set it with `data-profile="content"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [GA4](https://doubleagent.so/docs/ga4.md)
- [Google Tag Manager](https://doubleagent.so/docs/gtm.md)
- [WordPress](https://doubleagent.so/docs/wordpress.md)
- [Webflow](https://doubleagent.so/docs/webflow.md)
- [Squarespace](https://doubleagent.so/docs/squarespace.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="content". Briefing: https://doubleagent.so/install.md
```
---
# Double Agent for Fintech and payments
Step up. Don’t slam the door.
Spot automation on sensitive flows and ask for more proof instead of denying.
## What you get
- **Step-up, not deny.** Automated sessions get an extra check instead of a hard block. See [Clearance](https://doubleagent.so/docs/clearance.md).
- **The verdict on the payment.** Attach the verdict to Stripe metadata and write Radar rules on it. See [Stripe](https://doubleagent.so/docs/stripe.md).
- **Safehouse always on.** No listeners on payment fields and no key timing on the page. See [Safehouse](https://doubleagent.so/docs/safehouse.md).
- **Checked on your server.** Verify the token with @doubleagent-so/node before money moves. See [@doubleagent-so/node](https://doubleagent.so/docs/server.md).
## Default policy (profile `fintech`)
| Action | Policy |
|---|---|
| `payment, login` | Step-up |
| Every action | Safehouse on |
Set it with `data-profile="fintech"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [Stripe](https://doubleagent.so/docs/stripe.md)
- [@doubleagent-so/node](https://doubleagent.so/docs/server.md)
- [REST API](https://doubleagent.so/docs/rest.md)
- [Debrief](https://doubleagent.so/docs/debrief.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="fintech". Briefing: https://doubleagent.so/install.md
```
---
# Double Agent for Ticketing
Fans first. Scalpers flagged.
Catch checkout bots during on-sales, with tighter thresholds where it matters.
## What you get
- **Tighter at checkout.** Checkout uses lower thresholds, so automation is caught sooner. See [Clearance](https://doubleagent.so/docs/clearance.md).
- **A gate on the server.** requireHuman guards the purchase route and allows verified agents if you want them. See [@doubleagent-so/node](https://doubleagent.so/docs/server.md).
- **Late verdicts reach you.** If a verdict changes after checkout, the Debrief webhook tells your server. See [Debrief](https://doubleagent.so/docs/debrief.md).
- **Agents told apart.** An agent buying for a fan is labelled Agent, not lumped in with scalper bots. See [Verdict](https://doubleagent.so/docs/verdict.md).
## Default policy (profile `ticketing`)
| Action | Policy |
|---|---|
| `checkout` | Lower thresholds |
Set it with `data-profile="ticketing"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [@doubleagent-so/node](https://doubleagent.so/docs/server.md)
- [Stripe](https://doubleagent.so/docs/stripe.md)
- [Debrief](https://doubleagent.so/docs/debrief.md)
- [GA4](https://doubleagent.so/docs/ga4.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="ticketing". Briefing: https://doubleagent.so/install.md
```
---
# Double Agent for Lead generation
Leads from people, not form fillers.
Keep bot submissions out of your CRM and your cost per lead honest.
## What you get
- **A cleaner CRM.** The label rides into HubSpot and Mailchimp with each contact. See [HubSpot](https://doubleagent.so/docs/hubspot.md).
- **Form bots challenged.** Likely bots are challenged on lead forms. See [Clearance](https://doubleagent.so/docs/clearance.md).
- **Honest cost per lead.** Bot leads are held back from Meta, TikTok and Google Ads. See [Meta Pixel](https://doubleagent.so/docs/meta.md).
- **Agent leads, kept.** A lead sent by someone’s agent is tagged, so sales can decide. See [Verdict](https://doubleagent.so/docs/verdict.md).
## Default policy (profile `leadgen`)
| Action | Policy |
|---|---|
| `lead_form` | Challenge likely bots |
Set it with `data-profile="leadgen"`, or leave `auto` to let the script pick it from the page.
## Where the label goes
- [HubSpot](https://doubleagent.so/docs/hubspot.md)
- [Mailchimp](https://doubleagent.so/docs/mailchimp.md)
- [Meta Pixel](https://doubleagent.so/docs/meta.md)
- [Google Tag Manager](https://doubleagent.so/docs/gtm.md)
- [Wix](https://doubleagent.so/docs/wix.md)
## Install
Paste this to your coding agent:
```text
Install Double Agent on this site with data-profile="leadgen". Briefing: https://doubleagent.so/install.md
```