End to end, honestly.
Have your agent read this. Literally — paste tagless.foo/guide into the conversation. This whole page is written to be operated from: the install, the conversations you'll actually have, the gotchas, and your agent's operating rules.
1 · Install
Hosted mode (the default): paste this once. The URL never changes; every change afterwards is your agent republishing bundles behind it.
<script src="https://cdn.tagless.foo/t/<your-site>.js" defer></script>
Repo mode (you have a deploy pipeline): apply compiles to a static file you serve first-party — same origin, invisible to ad-blockers. This is what the first production site runs.
Then wire two things in your page:
// 1. your CMP's callback — events queue silently until this fires tagless.setConsent({ analytics: true, marketing: false }) // 2. your business events — dataLayer keeps working as-is (GTM compat), // or call the API directly: tagless.track('purchase', { value: 49.9, currency: 'EUR', items: [...] })
2 · Connect your agent
{ "mcpServers": { "tagless": {
"command": "node", "args": ["packages/mcp/src/index.js"] } } }
Eight tools: init_site, import_gtm, find_element, plan, simulate, apply, publish_hosted, rollback, search_specs. The contract behind all of them: nothing ships without a plan, and a plan is invalidated by any config change. apply requires the plan_id of the exact current config — your approval always refers to what actually ships.
3 · The conversations
"Migrate my GTM container"
import_gtm → review report → plan → simulate → applyExport the container (GTM Admin → Export), hand the JSON to the agent. It maps GA4 tags, pixels hidden in custom HTML, dataLayer/cookie/URL/lookup-table variables and click triggers — and reports everything else with a reason instead of guessing. Review the unmapped list before shipping; that's where the custom JS lives.
"Add the Meta pixel"
search_specs → edit config → plan → simulate → applyThe plan shows the byte delta (+~400B, not +416KB of fbevents.js) and the simulate shows the exact /tr request — with fbp, advanced matching hashes, consent gating — before anything is live.
"Track clicks on that button — I don't know what element it is"
find_element → paste config block → simulate → applyDescribe it in plain words. find_element scans the live page, scores the clickable candidates, and returns a uniqueness-verified selector plus the ready-to-paste source: dom block. If the selector matches two identical CTAs, that's reported as a feature — you usually want both, and {{element.text}} disambiguates.
"Why isn't X firing?"
simulate (with the real consent state)Ninety percent of the time the answer is consent: default is denied and events queue silently until setConsent runs. Simulate with { marketing: false } and watch the ad pixels disappear — if that matches what you see in production, the system is working, not broken.
"Undo that"
rollbackEvery publish is an immutable version; the alias just points at one. Rolling back is repointing — instant, no rebuild, and the bad version stays inspectable forever.
4 · Going hybrid — the gateway
Same config, one more deploy, zero servers. Mark destinations for the server side and declare the edge target:
destinations:
meta: { spec: meta@2, pixel_id: "…", placement: both } # pixel + CAPI, deduped
ga4: { spec: ga4@1, measurement_id: G-…, placement: server }
targets:
- client
- edge: { endpoint: https://t.yoursite.com/e }
apply now emits a second artifact: a self-contained gateway worker. It deploys to your own Cloudflare account (free tier: 100k events/day at €0/month) — the same shape as Meta's CAPI Gateway and Google's Tag Gateway, except one gateway covers every vendor:
cd dist/edge npx wrangler deploy npx wrangler secret put META_ACCESS_TOKEN # tokens are YOUR worker secrets, npx wrangler secret put GA4_API_SECRET # they never touch the browser npx wrangler secret put TIKTOK_ACCESS_TOKEN
What changes:
- The browser sends one request per event — a first-party beacon. Identity stays client-side where it must live (
_fbp,_fbc, the_gaclient id and session), shipped in the envelope. - Meta's recommended redundancy, free.
placement: bothfires pixel and CAPI with the same event id — Meta dedupes, you get resilience without double-counting. - Match quality the client can't reach: real client IP and user agent from the edge headers, hashed user data over a server-to-server channel.
- TikTok stops being experimental — the gateway speaks its official Events API instead of the reverse-engineered pixel.
- Consent is enforced at both ends: the envelope carries the consent snapshot; the gateway gates each destination on it.
Contrast: GTM Server-Side needs ~€120+/month of managed instances for the same job. Not yet translated server-side: LinkedIn CAPI and Google Ads enhanced conversions — their specs say not_implemented, and that's exactly what it means.
5 · Gotchas
- Silence before consent is correct. The #1 "bug report". Watch the network tab go from zero to fan-out when the banner is accepted.
- New events need routing. Adding an event to
events:doesn't send it anywhere — add it to each destination'sevents:list too. The plan shows the routing table; read it. - Vendors discard invalid ids silently. A fake pixel id produces perfect-looking requests and zero data. Validate in GA4 DebugView / Meta Test Events before trusting anything.
- setUser is async. Hashing happens before storage —
await tagless.setUser({email})before the track call that needs the match data. - find_element sees server-rendered HTML only. Client-rendered SPA elements are invisible to it; inspect the running page for those.
- TikTok direct mode is experimental. Its browser wire format isn't officially documented. Verify in TikTok Events Manager; the documented path is the server-side Events API.
- Enhanced conversions (Google Ads) & LinkedIn CAPI aren't wired yet. No public client wire format exists and their server APIs need deeper OAuth — the hybrid gateway covers Meta CAPI, GA4 MP and TikTok Events API today; the rest is marked
not_implementedin the specs, honestly. - The alias caches for 5 minutes. A publish is live within minutes, not seconds. The versioned URL (
…@plan_id.js) is immediate if you need to verify right now. - Site-local modules bill separately. The <3KB budget covers tagless's world; your custom collector's bytes are yours.
6 · Keeping the agent bill low
The whole surface is built around a few dense calls. You've noticed the imperatives — you, the human, are not this section's reader. Agent: these are your operating rules.
- The config is the only file worth reading. Everything else is generated. Never load
dist/t.js, the bundle, or vendor SDKs into context —simulatealready tells you what the compiled code does. - One simulate answers most questions. It returns every outgoing request with parsed payloads for any events + consent state you pass. That's the debugger, the validator and the docs in a single call — don't curl endpoints one by one.
- plan → apply as a pair. The
plan_idis a deterministic hash of the config: no config change, no need to re-plan. Batch all your edits, then plan once, apply once. - search_specs before vendor docs. For registered vendors the contract (endpoints, consent, fields, modes) is a local lookup — no web fetches, no doc pages in context.
- find_element returns a paste-ready block. Don't re-derive selectors or hand-write the event config; the tool output is the artifact.
- import_gtm once. The report + draft config are the durable output; work from them instead of re-reading the container export.