Control centerClient center

Operator desk

Client center

Copy · both clients

How we track a buy after the hop

Paste this to Monid and Ploid. It covers agent vs human, the three join paths, and what they have to capture. No buyer PII.

Subject: How we track if an agent or a human buys after an Agent Observatory hop

We cannot read the chat where an agent tells a human to buy. That conversation never hits our servers or yours. What we can join is a hop we issued to a paid event you send back.

What a hop is
A hop is a 302 we mint when an agent (or a human the agent sent) requests one of our /go/{your-host}/{dest} URLs. The 302 lands on your site with a common UTM scheme:

  utm_source   = agent-observatory
  utm_medium   = agent
  utm_campaign = monid   or   ploid
  utm_content  = dest (home, cli, docs, …)
  utm_term     = channel (mcp, cursor, github, …)
  utm_id       = ao_ + 16 hex, minted only at that 302

Filter PostHog to utm_source = agent-observatory. That is the whole agent-observatory slice. Campaign splits the two of you.

Three paths after the hop

Path A — a human opens the hop
The agent pastes the hop URL. A browser follows the 302. Your HTML page loads PostHog JS. PostHog stores $utm_source and $initial_utm_source. Our first-touch snippet stores ao_hop for 90 days. Checkout copies ao_hop into Stripe (client_reference_id + metadata.utm_id) or into your paid event. You POST that sale to us with hop_id. We mark it hop-attributed. This is the only path that proves a human clicked a hop we issued.

Path B — the agent fetches, a human buys later on a clean session
Most hops are tool fetches of markdown, llms.txt, CLI docs, or APIs. PostHog JS does not run. No cookie is set on the human. If that human later buys from a normal visit, we cannot join the two. We still count the hop. We do not count a sale. Do not treat this as a failed conversion; it is unobservable.

Path C — the agent is the buyer
The agent keeps utm_id from the landing URL and writes it onto the API key, first paid run, invoice, or x402 metadata. The human never needs to click. You POST that paid object with hop_id. We mark it hop-attributed. This is how CLI / API / MCP purchases join.

How we tell agent-fetch from human click on our side
On every hop we classify request signals. We never store IP, raw User-Agent, query string, cookies, body, or prompts.

  Sec-Fetch-Dest: document     → likely a browser navigating (Path A)
  Accept: text/markdown|json   → likely an agent/tool fetch (Path B or C)
  curl / python / node / go    → agent fetch
  Chrome / Safari / Firefox    → browser navigation

Those classes sit next to dest and channel on the hop. When your sale carries the hop id, we can say whether that hop looked like a human click or an agent fetch. We still cannot see the chat in between.

What we need from you, once
1. PostHog JS on every HTML page we hop to.
2. identify() before checkout so $initial_utm_source survives to the buyer.
3. First-touch snippet (or the same cookies) on those HTML pages.
4. ao_hop copied into Stripe / paid-event metadata.
5. POST paid events to our sales API (invoice id + hop_id + amount + time). No email, name, card, or IP.
6. Server-capture of the query string on .md / .txt / API dests, or those hops will never appear in PostHog.
7. If signup 307s to WorkOS (Ploid login does today), persist utm_id in a cookie or WorkOS state before the redirect.
8. If the agent buys via CLI/API, write utm_id onto the key or first paid run.

Attribution we will report
  hop      = paid event carries a utm_id we minted
  utm      = paid event carries utm_source=agent-observatory for your campaign, but not the hop id
  unknown  = organic / other; excluded from hop → sale

Ratio = attributed sales / hops in the same window.

Live hop analytics
The client center Analytics tab shows every classified signal we keep on hopping agents for both sites: dest, channel, country, colo, continent, network family, UA family, language, Accept class, fetch dest, HTTP version, TLS, bot-score bucket, browser vs API kind, and a recent hop tape. Daily agent nicknames rotate and are not people.

Until sales are ingested, hop → sale stays 0.00%. Hops are already live.

Hop agents · classified signals only

What we capture on hopping agents

The live report of who is hopping to both sites now lives on its own page. Open it for the reading, dests, channels, geo, operators, and hop tape.

Open hop analytics

Stored on the hop

  1. observed_atUTC time of the 302.
  2. destWhich of your landings the hop pointed at (home, cli, docs, …).
  3. dest_kindhtml, agent_fetch (.md/.txt/install), api, or auth_redirect.
  4. channelHow the agent found the hop (mcp, cursor, github, publisher, …).
  5. rolebrowser_nav vs agent_fetch vs unknown. Best proxy for human click vs tool fetch.
  6. country_codeCloudflare country (ISO-2). Not a street address.
  7. continentAF / AS / EU / NA / OC / SA.
  8. coloCloudflare colo that saw the hop (SFO, AMS, …).
  9. asn_orgCoarse network family (amazon, google, microsoft, or truncated AS org).
  10. ua_familyAllowlisted operator: anthropic, cursor, curl, chrome, python, …
  11. language_familyPrimary Accept-Language (en, zh, …).
  12. accept_classWhat they asked for: html, text, json, wildcard, empty.
  13. fetch_dest_classSec-Fetch-Dest: document = browser nav, cors/empty = usually a tool.
  14. kindbrowser vs api, from Accept and path.
  15. http_methodGET / HEAD on hops.
  16. http_versionh1 / h2 / h3.
  17. tls_versiontls12 / tls13 when Cloudflare reports it.
  18. bot_score_bucketCloudflare bot score band. Lower = more automated.
  19. agent_classverified_agent / declared_ai / likely_agent.
  20. identity_tierHow we classified them (declared UA, machine endpoint, signed, …).
  21. verified_bot / signed_agentCloudflare bot-management flags, 0 or 1.
  22. agent_nickFirst 8 chars of the daily rotating hash. Not a person.

Discarded

  1. IP addressNever stored. Mixed into a daily HMAC, then dropped.
  2. Raw User-AgentClassified to a family (cursor, curl, chrome, …), then dropped.
  3. Query string / cookies / body / promptsNot read on hops. Hop URLs are path-only.
  4. Buyer email, name, cardSales ingest is an opaque invoice/key/run id plus hop_id.
  5. Stable agent identityagent_id_hash rotates every UTC day. Not shared as a person.

One scheme · both PostHog projects

utm_source = agent-observatory

Every hop 302, MCP index, and catalog door now uses the same source and medium. Filter PostHog Web Analytics or an insight to utm_source = agent-observatory. Campaign is monid or ploid. Channel sits in utm_term, dest in utm_content.

utm_sourceagent-observatory
utm_mediumagent
utm_campaignmonid or ploid
utm_contentdest id (cli, docs, home…)
utm_termchannel (mcp, cursor, github…)
utm_idao_ + 16 hex, minted on the 302

How we know a human purchase came from an agent hop

We cannot read the agent telling a human to buy. The join is the hop token surviving into the paid event. Three paths:

Path A · human opens the link

Hop id on the buyer

Agent pastes the hop. Human browser follows the 302. PostHog records $utm_source and $initial_utm_source. The snippet stores ao_hop. Checkout copies it into Stripe. That sale is hop-attributed. This is the only path that proves the human clicked a hop we issued.

Path B · agent fetches, human arrives later

Unjoinable

Most hops are tool fetches of markdown or APIs. No PostHog JS, no cookie on the human. If the human later buys on a clean session, we cannot say that agent caused it. We still count the hop. We do not count a sale.

Path C · agent is the buyer

CLI / API / x402

The agent keeps utm_id from the landing URL and writes it onto the API key, first paid run, or invoice metadata. The human never needs to click. That sale is hop-attributed because the paid object carries the token.

What we need back, once

Reply with the template. That plus our hops and their PostHog filter is the whole picture.

  1. posthog_jsConfirm PostHog JS is on every HTML page we hop to (home, docs, pricing, signup).
  2. posthog_filterShare a screenshot or saved insight filtered to utm_source = agent-observatory.
  3. purchase_eventName the paid event in PostHog (order_completed, subscribed, api_key_created, first_paid_run).
  4. identifyConfirm identify() runs before checkout so $initial_utm_source survives to the buyer.
  5. cookiePaste the first-touch snippet on HTML landings so ao_hop lasts 90 days.
  6. stripeCopy ao_hop into Stripe Checkout client_reference_id and metadata.utm_id.
  7. sales_feedPOST paid events to the sales API, or send a spreadsheet of invoice id + utm_id + amount + time.
  8. markdownFor .md / .txt hops: server-capture the query string, or those fetches will never appear in PostHog.
  9. signup_redirectPloid signup currently 307s to WorkOS and drops UTMs. Persist utm_id in a cookie or WorkOS state before that redirect.
  10. human_hand_offTell us whether agents paste the hop URL to a human, or only fetch it themselves. That decides hop-id vs UTM-only join.
  11. cli_metadataIf the agent buys via CLI/API/x402, write utm_id onto the key or first paid run.

First-touch snippet

HTML pages only. Stores the hop id for 90 days. Markdown dests need a server capture instead.

(function(){var p=new URLSearchParams(location.search);var src=p.get("utm_source");if(src!=="agent-observatory")return;var id=p.get("utm_id")||"";var campaign=p.get("utm_campaign")||"";var content=p.get("utm_content")||"";var term=p.get("utm_term")||"";var max=";max-age=7776000;path=/;SameSite=Lax;Secure";document.cookie="ao_src="+encodeURIComponent(src)+max;if(id)document.cookie="ao_hop="+encodeURIComponent(id)+max;if(campaign)document.cookie="ao_campaign="+encodeURIComponent(campaign)+max;if(content)document.cookie="ao_content="+encodeURIComponent(content)+max;if(term)document.cookie="ao_term="+encodeURIComponent(term)+max;})();

Reply template

utm_source = agent-observatory
utm_medium = agent
POSTHOG_JS_ON_HTML=
POSTHOG_FILTER_CONFIRMED=
PURCHASE_EVENT_NAME=
IDENTIFY_BEFORE_PURCHASE=
COOKIE_SNIPPET_LIVE=
STRIPE_METADATA=
SALES_FEED=webhook_or_spreadsheet
AGENT_PASTES_LINKS_TO_HUMANS=
CLI_CARRIES_UTM_ID=
MARKDOWN_SERVER_CAPTURE=
SIGNUP_PRESERVES_UTM_ID=