Ad rails for agent-sourced leads
When a request on your site is completed, confirmed by the person on your card or by their own agent with their go-ahead, the lead is already in your inbox, and on its way to your CRM if you subscribe a lead webhook. This page covers the third place it can go: your ad platforms and your analytics, so the leads an agent brought you show up as conversions you can bid on and as a source you can compare with the rest.
Everything here runs from the same step that completes the lead. Nothing is sent before the request is complete, and nothing the person typed is sent at all.
What crosses, and what never does
Every event below carries the same seven fields, and only these:
| Field | Values | Derived from |
|---|---|---|
tool_name | request_quote, request_consultation, book_appointment, request_callback, check_fit, get_pricing, start_trial, book_demo, other | the tool the agent called, folded into this fixed list |
service_category | your vertical slug (health verticals: the family only, e.g. elective-health) | your configured vertical |
urgency_bucket | now, this_week, this_month, planning | the urgency, timeline or deadline the person stated |
timing_bucket | today, this_week, this_month, flexible | the window they asked for |
value_band | low, mid, high, unknown | the value estimate against your vertical’s thresholds |
agent_client | chatgpt, claude, gemini, copilot, perplexity, browser_webmcp, bridge, express, other | the agent that acted |
agent_sourced | true | always, so you can report agent-sourced leads separately |
The lead reference, the person’s name and contact details, what they wrote, the facts they stated and any dollar figure never cross. The allowlist is enforced in code: a build with a field outside this list fails its tests.
Level 1: AIAgentAction, on the page and from the server
When the request is completed, the beacon fires one event to every pixel already on your page, and the server sends the same event through the platform’s server API with the same event_id, so each platform keeps exactly one. A health practice’s page fires none, and a criminal-defense request sends nothing at all (see the per-vertical policy below). When the person’s own agent completed the request from its own servers, there is no page to fire on, so only the server half runs:
| Platform | Page (beacon) | Server (mirror) | Dedup key |
|---|---|---|---|
| Meta | fbq('trackCustom', 'AIAgentAction', …, { eventID }) | Conversions API | eventID / event_id |
| TikTok | ttq.track('AIAgentAction', …, { event_id }) | Events API | event_id |
| Google (gtag) | ai_agent_action event | — | — |
| Microsoft UET | ai_agent_action event | — | — |
event_id is ia_ followed by 16 hex characters: an opaque, deterministic key derived from your account, the lead and the funnel stage. It is derived, not stored, so a retried confirm or a later catch-up sweep produces the same key and cannot double count.
You need: the beacon on the page with data-pixel-enrichment (the same opt-in that enables the AIResearchVisit enrichment event), your pixels loaded, and Meta or TikTok connected in Audience Studio for the server mirror. Without the connection the page half still fires.
Level 2: lead-funnel conversions with value
Each funnel stage is uploaded as a conversion so the platforms can optimize toward the leads that turn into business, not just the ones that submit:
| Stage | Fires when | Google Ads | Meta | TikTok | Microsoft |
|---|---|---|---|---|---|
lead_submitted | the request is completed | Submit lead form | Lead | SubmitForm | Submit lead form |
lead_qualified | completion, when the qualification verdict is “may qualify” | Qualified lead | QualifiedLead | QualifiedLead | Qualified lead |
booked | a booking lands, or you report a booked outcome | Book appointment | Schedule | Schedule | Book appointment |
closed_won | you report a closed_won outcome | Converted lead | Purchase | CompletePayment | Converted lead |
Value. The value you report with the outcome is used as is. Before that, the value is your vertical’s estimate weighted by the qualification score; it is logged in shadow until you turn value weighting on.
Google Ads conversion actions. Name your conversion actions exactly as in the table, or map each stage to an action id in your account settings under ads.google_conversion_actions ({ "lead_submitted": "customers/…/conversionActions/…", … }).
Matching. A conversion matches on the click id captured when the person arrived (gclid, fbclid, ttclid, msclkid) or, for Enhanced Conversions for Leads, on the hashed email or phone the person confirmed. Hashed identifiers are attached only when the person granted marketing consent at confirm (email_marketing for the email hash, sms_marketing for the phone hash), or when your account records that your contact consent covers conversion measurement (ads.enhanced_conversions_consent = "contact"). No consent, no identifiers; the click id alone still matches. An agent’s relay never grants marketing consent, so a request completed by the person’s own agent carries hashed identifiers only under ads.enhanced_conversions_consent = "contact".
Level 5: GA4 generate_lead with agent_sourced
The server sends your own GA4 property one Measurement Protocol event per stage, so agent-sourced leads sit next to every other source in the reports you already use:
| Stage | GA4 event |
|---|---|
lead_submitted | generate_lead |
lead_qualified | qualify_lead |
booked | working_lead |
closed_won | close_convert_lead |
Each carries the seven fields above plus lead_stage, event_id, and value / currency when a value exists. Register agent_sourced (and any of the buckets you want to slice by) as event-scoped custom dimensions in GA4 Admin and the lead-source split appears in Explorations. The event’s client_id is the _ga cookie id the page relays at confirm, so the lead attaches to the visitor’s existing session; without the cookie a stable synthetic id is used per lead.
Where the credentials go. GA4 needs a Measurement ID (G-XXXXXXXXXX) and a Measurement Protocol API secret (GA4 Admin → Data streams → your web stream → Measurement Protocol API secrets → Create). Both live on your account settings under ga4:
{ "measurement_id": "G-XXXXXXXXXX", "api_secret": "…", "debug": false }
api_secret_encrypted (the secret stored encrypted at rest) is preferred over the plaintext field when both are present. The API secret can only post events to that stream; it cannot read anything. There is no dashboard field for this yet: send both values to your Inception Agents contact and they are set on your account. Set debug: true to route events to GA4’s validation endpoint while you check them in DebugView (validation-only: those events are not recorded).
Per-vertical policy
What the rails may do follows your vertical, and the protected classes cannot be widened by configuration:
| Vertical class | Lead conversions | Enhanced Conversions | Audience policy | Network mirror |
|---|---|---|---|---|
| Health (HIPAA-tagged verticals and the elective-health family) | Google Ads only; no Meta, TikTok or Microsoft lower-funnel events, and no AIAgentAction on the page’s pixels | with consent | none | off |
| Housing (fair-housing verticals: multifamily, senior living) | every platform | with consent | none | off |
| Law firms | Google Ads always; Meta, TikTok and Microsoft when the vertical allows lower-funnel events | with consent | practice area and location only | the practice area alone |
| Criminal defense (the practice, and any criminal-defense request at a law firm) | none: nothing reaches an ad platform or GA4 | none | none | off |
| Everything else | Google Ads always; Meta, TikTok and Microsoft when the vertical allows lower-funnel events | with consent | as configured for the vertical | on |
The audience column records policy only: the rails on this page build no audience. An unknown or unreadable vertical resolves to the conservative default (Google Ads only, no audiences). Health and housing verticals never mirror to the shared Inception Agents network pixels, because a service category on a shared pixel is itself a protected signal. A law firm’s network events carry only the slug of one of its own practices (legal-family), never the words of a query or a place, and a firm that practises criminal defense sends nothing.
Verify it
- The lead’s ledger. Every completed stage appends an
ad_rails_ranevent to the lead’s event history with theevent_id, the policy class and a per-platform result (synced,skipped:<reason>,failed:<reason>), so you can see exactly what fired for a given lead. - Meta Test Events. With the beacon and Conversions API both connected, confirm a request of your own on the card on your page: Events Manager → Test Events shows one browser event and one server event for
AIAgentActionwith the sameevent_id, deduplicated. (The go-live live test is flagged TEST and never reaches ad platforms, so it does not show here.) - GA4 DebugView. With
debug: true, complete a request of your own and open DebugView:generate_leadarrives withagent_sourced = trueand the buckets as parameters. - Lead-source P&L.
get_trace_attribution(andGET /api/v1/trace/attribution) returns aleadSourcestable —agent_webmcp,agent_bridge,agent_call,paid,organic,direct— with leads, verified, booked, closed-won and value per source, so agent-sourced leads can be compared with your other channels.agent_bridgealso counts leads from your hosted/mcpand your express pages.