Skip to content

Set up RoutingCat

This tutorial takes a site that your own templates render, on any host, from nothing to a working install, in five steps. Your agent does most of the work; you approve a sign-in, choose what to track, choose how production registers, deploy and add a card, with nothing charged. One real conversion of your own, recommended and your choice, confirms your tracking end to end. Your visitors see no change, because no page is routed until you declare one.

It follows Lumen, whose site runs on Laravel at www.lumen.com. What your site needs lists the requirements.

1. Install with init

In your site’s folder, you or your agent run:

Terminal window
npx routingcat init

It finds your templates. It recognizes Laravel, Rails, Django, a WordPress theme, Hugo, Jekyll, Eleventy, a site of PHP or HTML pages, and an app that renders in the browser, built with Vite or Create React App; Client-rendered apps says what differs for an app. It reads where their templates live, and where it finds none, it says so: run it in your site’s own folder.

It sets your site up to decide in the page. Your host runs nothing of RoutingCat’s, so RoutingCat’s script decides in each page, and init’s first note says what a host whose middleware runs RoutingCat would add. An app that renders in the browser always gets this setup. A site whose folder names no host makes init ask where it deploys, or, for a site your own server renders, where it is served; run by an agent, it stops: run it again with --host none, for a host that runs nothing of RoutingCat’s. Once your site deploys on Vercel or behind Cloudflare, init run again with that host, as --host vercel, moves it there, its pages, variants and results kept, and binds it to that host’s project, and the docs for a static or server-rendered site on Vercel or Cloudflare take over.

It finds your production address in your stack’s configuration: APP_URL for Laravel, config.hosts for Rails, ALLOWED_HOSTS for Django, WP_HOME for WordPress, baseURL for Hugo, url for Jekyll, homepage in package.json for an app, and a CNAME file for Eleventy, a site of PHP or HTML pages, or an app without homepage. Where none gives it, init asks at a terminal, or stops: run it again with --origin https://www.example.com. Pages served from that host decide and record as production. On any other host, such as your own machine, pages run in development mode and record nothing.

It signs you in. A browser page opens, where you sign in, or sign up, and allow the command on this machine. Where no browser can open, init prints the link, waits 50 seconds and exits with code 3: open the link, approve, and run init again, which continues with the same link.

It shows its plan, then applies it. At a terminal it asks whether to add a note to AGENTS.md, then whether to apply the plan; run by an agent, it asks nothing, applies the plan and prints what changed. --dry-run stops after the plan. On Lumen’s Laravel site, run by an agent with --host none and signed in already:

routingcat 1.0.0 · <project>
Checks
✓ Laravel: its templates resources/views/**/*.blade.php
· Laravel renders this site's pages with nothing of RoutingCat's running before the HTML, so RoutingCat decides in the page: the script integration. On a host whose middleware runs RoutingCat, such as Vercel or Cloudflare, the same templates would take server decisions: a visitor id the server sets for 400 days, secret seeds, no wait on a first view, and geography from the first view. (D87)
✓ production address https://www.lumen.com, from APP_URL in .env
✓ signed in to RoutingCat
Plan
1 create the site lumen, for the script integration, with no host binding
2 bootstrap/app.php leave rc_vid out of cookie encryption: $middleware->encryptCookies(except: ['rc_vid']);
3 routingcat.config.json new: the site and its public key
4 resources/views/routingcat-head.blade.php new: the head partial, as routingcat push --head-only writes it
5 resources/views/layouts/app.blade.php include the head partial first in <head>: @include('routingcat-head')
6 the skill .agents/skills/routingcat/, copied to .claude/skills/routingcat/
Nothing reaches visitors until you deploy.
✓ site lumen created (op_01m3yesth0fjfsctr17tbq1z82), for the script integration, with no host binding
✓ 2 files edited, 2 files and 2 folders created (see git diff)
· In CI, before your deploy: npx routingcat@1.0.0 push --check, which fails on a stale stamp or partial.
· Right after your deploy, on your production branch: npx routingcat@1.0.0 push, which registers it.
· The push proves itself with the site's deploy key: ask the site's owner to run npx routingcat keys deploy --branch <your production branch> in their own terminal, and to set what it prints as the secret ROUTINGCAT_DEPLOY_KEY in your CI.
· Where your host deploys on its own, as Forge or a hook on your server does, run that push at the end of its deploy script, with ROUTINGCAT_DEPLOY_KEY and ROUTINGCAT_BRANCH, your production branch's name, in its environment.
· Without a CI, register production from this machine after each deploy: npx routingcat@1.0.0 push --env production, which shows the plan and pushes it once you confirm.
· Your Laravel server can set RoutingCat's visitor cookie, which Safari then keeps 400 days instead of 7, and the fetch-site cookie, which tells a visit typed or bookmarked from one that followed a link. If you want it to, run npx routingcat init --helpers, which adds the helpers that set them. (D102)
Ask the person: "Add a two-line note to AGENTS.md so coding agents always know RoutingCat is here? (recommended)" If yes, run npx routingcat init --agents-md.
Dashboard: https://app.routingcat.com/lumen
Next: read .agents/skills/routingcat/setup.md and follow it

The site is named after your production domain, and bound to no host: your pushes register it. It already shows in the dashboard, at the address init prints: Lumen’s is https://app.routingcat.com/lumen. Your plan covers it: Hobby, at no cost, holds 3 sites, and init refuses a 4th with the way out. Each edit has a purpose:

File What init does So that
routingcat.config.json creates it, with your site, its public key, the routingcat version your site runs, your production host, and where your templates are it names your site, its pages and its environments; the key is public and can only write
The head partial, here resources/views/routingcat-head.blade.php writes it, as push --head-only does RoutingCat’s script decides in <head>, before the page shows
Your layout includes the partial first in <head>, after the charset and viewport tags every page carries it
On Laravel, bootstrap/app.php, or before Laravel 11, app/Http/Middleware/EncryptCookies.php leaves rc_vid out of the cookie encryption your own code reads the visitor id as the browser sent it, where Laravel would empty a cookie it didn’t encrypt
.agents/skills/routingcat/, .claude/skills/routingcat/ installs the skill your agent knows the commands and the words
AGENTS.md, and CLAUDE.md if you have one with your agreement, adds a two-line note every coding agent knows RoutingCat is here, and reads the skill before changing its code

Every edit shows in git diff, and nothing joins your dependencies. Lumen’s config, as init wrote it:

{
"$schema": "https://cdn.routingcat.com/schemas/1.0.0/config-1.json",
"site": "lumen",
"public_key": "pk_lumen_XMufiEsf",
"integration": "script",
"routingcat": "1.0.0",
"environments": {
"production": {
"hosts": [
"www.lumen.com"
]
}
},
"sources": [
"resources/views/**/*.blade.php"
],
"head": {
"partial": "resources/views/routingcat-head.blade.php"
},
"pages": {}
}

And its layout, the partial included first in <head>:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@include('routingcat-head')
<title>{{ config('app.name', 'Lumen') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
@yield('content')
</body>
</html>

The partial goes first in <head>, after the charset and viewport tags, so that RoutingCat’s script runs at once, rather than after your stylesheets load. init includes it where one layout holds <head>, and otherwise prints the edit for each layout:

Your templates The line that includes the partial
Laravel @include('routingcat-head')
Rails <%= render "routingcat_head" %>
Django {% include "routingcat_head.html" %}
Hugo {{ partial "routingcat-head.html" . }}
Jekyll {% include routingcat-head.html %}
Eleventy {% include "routingcat-head.html" %}, in Nunjucks and Liquid alike
WordPress, PHP <?php include __DIR__ . '/routingcat-head.html'; ?>, with the partial’s path from the layout
HTML None: paste the partial into each page’s <head>, and again each time push rewrites it

The partial is generated: never edit it by hand, except to give its script tags your page’s nonce, where your Content Security Policy runs scripts by nonce: push keeps it. push writes the partial again each time your slots or your config’s declarations change, and your CI’s push --check fails on one that differs. Build a page says what it holds.

The config pins the command’s version, here "routingcat": "1.0.0", since each version writes its own partial, which another version’s push --check refuses. So npx routingcat runs the version your config pins for push, plan, doctor, docs and, once the pin is written, init: started as another version, it says so in one line, then runs the pinned one through npx, with the same arguments. Where npx can’t fetch or run that version, the command stops with exit code 2 and runs nothing. To move the site to another version, set routingcat to it in routingcat.config.json and in your CI’s steps, then run npx routingcat push and commit what it changes.

Run init again at any time. It marks with ✓ what is done and does only what is missing, and it corrects a public_key that isn’t your site’s public key. It refreshes the skill unless you edited it, and keeps the note in AGENTS.md up to date once you have it, without asking again.

The note in AGENTS.md is your choice, and recommended. Coding agents read AGENTS.md in every session, but may open a skill only when a request matches it; the note tells them RoutingCat is here and to read the skill first. Run by your agent, init leaves the choice to you: your agent asks, then runs npx routingcat init --agents-md if you agree. See Add the note to AGENTS.md.

The server helpers are optional. On a site your own server renders, init’s last note offers them; Build a page says what they change and when they help.

2. Choose what to track

RoutingCat counts the conversions your code reports. Your agent reads your code, and asks you two questions:

  • The main conversion, pick one. This is the objective: what routing learns to get more of. It counts at most once per visit, when it happens within its window after the visitor first saw the page, 24 hours by default.
  • Also track, pick any. These outcomes are counted and reported beside it, and never steer routing.

Answer in your own words when the list misses something. A main conversion can be several events where any one counts, such as any plan start; it still counts once per visit.

For a SaaS with trials, choose sign-up or trial start as the main conversion, and track purchase beside it. A payment at a trial’s end comes days or weeks after the visit: the default 24-hour window misses it, and a longer window, up to the visit’s length, delays learning by as much. See Conversions and visits.

Lumen chooses sign-up, and tracks its waitlist beside it.

3. Wire and declare them

Your agent wires each choice where it happens, with two calls:

  • identify links the visitor to your own account id, at sign-up and at every sign-in, so that a conversion on another device still counts;
  • track reports each conversion. A server event needs an id, because providers deliver some events twice.

Two places can send them:

  • Your server, through the HTTP events API, with the visitor id from the request’s rc_vid cookie. Sign-ups and payments belong here: a browser can send anything, so an objective worth defending comes from your server.
  • The page, with window.routingcat.track() and window.routingcat.identify(), which the head partial gives every page: for a conversion only the browser sees, such as a form with no backend.

The recipes are in Track conversions, with Auth providers and PostHog on their own pages.

Then it declares them in routingcat.config.json, beside what init wrote: Lumen’s gains "objective": "signup" and "outcomes": ["waitlist"].

  • Events under names you haven’t declared are recorded, but not analyzed until you declare them.
  • Declare no page yet: a page comes with your first idea.
  • In development, the page’s track and identify log to the browser’s console instead of recording.

Then it runs push, and commits what it changes:

Terminal window
npx routingcat push

The head partial names everything your config declares, the objective and outcomes included, so declaring them changes it. Until push writes it again, your CI’s push --check fails, and doctor finds that your deployed pages’ partial names templates no push registered.

4. Choose how production registers

Your pages decide from what push registers, so each production deploy is registered by a push right after it, with push --check before it: a push before a deploy that then fails would make live variants your pages never ship. From a deploy that changed your templates until about 2 minutes after its push, your pages show your defaults and record no decision. A site registers in one of three ways. A site created for Vercel or Cloudflare, then moved here, stays bound to that host’s project, so it registers with the deploy key alone, and the GitHub link and a push from your machine are refused.

In GitHub Actions, with no secret to keep. The site’s owner links it to its repository once, from a clone whose origin remote names it:

Terminal window
npx routingcat link github --branch main
$ routingcat link github --branch main
link github · done · op_01m58vb0sff5f8vmx9bg22924h · registry version 3
st_01m58vasyqevdvx3zhrfa86nsa repository quire-app/site · branch main · status pending · expires_at 2026-10-20T01:10:12Z
lumen · GitHub repository quire-app/site · production branch main · pending until Oct 20, 01:10 UTC
✗ lumen is linked to GitHub repository quire-app/site once a workflow run of it proves it: run routingcat push in a workflow of quire-app/site whose job has permissions: id-token: write, before Oct 20, 01:10 UTC. (D116, exit 3)

Then give your deploy workflow’s job permissions: id-token: write, and two steps:

  1. before the deploy, npx routingcat push --check, which changes nothing and fails while your templates lack a stamp, the partial is out of date or the config’s $schema is another version’s;
  2. right after the deploy, npx routingcat push, which registers what the deploy serves.

RoutingCat’s example app takes both around its build and its deploy, at the version its config pins, 1.0.3:

.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
# `id-token: write` lets `push` ask GitHub for the run's OIDC token, which proves the push once
# `npx routingcat link github --branch main` has linked the site to this repository.
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx routingcat@1.0.3 push --check
- run: npm run build
# The site's own deploy of dist/ goes here.
- run: npx routingcat@1.0.3 push

A run on your production branch registers production when a push, workflow_dispatch, schedule or repository_dispatch event started it. Any other run registers a preview, which changes nothing live: one on another branch, or one another event started, such as a pull request, a release or a workflow_run.

The first such run within a day proves the link; until then, link github and doctor say it waits.

With the deploy key, in your CI, or in the deploy script of a host that deploys on its own. The owner runs npx routingcat keys deploy --branch main and sets what it prints as the secret ROUTINGCAT_DEPLOY_KEY where the push runs; the key shows only once:

$ routingcat keys deploy --branch main
deploy key · production · branch main · minted now (op_01m4zbfqr0fwqttp9y5atppz0f)
dk_qEi3Pi0oOEMbi6gb9l2uUsWQPF2A7H_OEIrNnn1cO3iCUcOn3YG4MgeQpxfKf4c-v6BzUec_KQP9ml31_8pHZOJqlNX_OP9EtLs1tZs_OrPB1YozQ4JQhqFMeKhYKYge17RdO_0ANyBicK6
Set it as the secret ROUTINGCAT_DEPLOY_KEY where routingcat push runs, in your CI or in your host's deploy script. It is shown only when minted or rotated.
It proves production only for pushes on main, as your CI reports the branch, or ROUTINGCAT_BRANCH in a deploy script.

It registers production only from pushes of that branch, which push reads:

  • from the CI’s own variables, on GitHub Actions, GitLab, Bitbucket, CircleCI, Jenkins’ multibranch pipelines, Azure Pipelines, Buildkite, Cloudflare’s Workers Builds or Pages, Netlify and Vercel;
  • elsewhere, from ROUTINGCAT_BRANCH, which you set to the branch being deployed. Without it, the production key is refused, with exit code 1.

Inside a CI RoutingCat reads, its own variables decide, and ROUTINGCAT_BRANCH doesn’t apply: a Jenkins job that isn’t a multibranch pipeline, or Azure Pipelines over TFVC, names no branch and is refused. Run the push from a multibranch pipeline, or from a Git repository on Azure.

In your CI, the workflow takes the same two steps. A run of another branch, of a tag or of a pull request is refused the production key; to register previews from such runs, give them the preview key instead, from npx routingcat keys deploy --env preview.

On a host that deploys on its own, such as Forge, Ploi or a deploy hook on your server, push runs at the end of the host’s deploy script, once the server serves the new templates, with the key and ROUTINGCAT_BRANCH in the script’s environment: on Forge, ROUTINGCAT_BRANCH=$FORGE_SITE_BRANCH npx routingcat push. push --check stays before the deploy, in your CI, or on your machine before you commit. Only a host without a deploy script needs your CI to start its deploy, through the host’s deploy hook, and push once the site serves it.

From your machine, without a CI or a deploy script that pushes. Before each deploy, run npx routingcat push --check; where it fails, run npx routingcat push, which from your machine stamps your templates and registers nothing, and commit what it changes. Right after each production deploy, run:

Terminal window
npx routingcat push --env production

It prints the edits it would make and the plan, as plan shows it, then pushes that plan once you confirm it. At a terminal it asks. Run by your agent, it pushes nothing, and ends with exit code 3 and the plan’s digest:

✗ Nothing was pushed: this plan goes to production once the person confirms it. Show it to them; once they agree, run routingcat push --env production --confirm 6553f70fd916. (D115, exit 3)

Your agent shows you the plan, and once you agree, runs the same command with --confirm and that digest. It pushes exactly the plan you saw, and nothing if your templates or production changed since. This push acts with your sign-in, and is recorded as yours.

5. Deploy and verify

Deploy to production the usual way, or let your agent do it where your repository’s rules allow, with the partial push wrote in step 3 committed, then register it right after, as you chose. The push ends with RoutingCat’s receipt:

routingcat registered ch_1 · objective signup · outcomes waitlist · no page routed yet

A push with no credential, from your machine without --env production or in a CI with neither the GitHub link’s token nor a deploy key, stamps your templates, registers nothing, and says how to fix it:

$ routingcat push
…
· routingcat push registered nothing: it registers in CI, with GitHub's token once routingcat link github links this site to its repository, or with the site's deploy key in ROUTINGCAT_DEPLOY_KEY, and neither is here. From this machine, routingcat push --env production registers production once you confirm the plan it shows. Commit what it changed. (D72)

A push whose credential RoutingCat refuses fails with the reason and exit code 1: a production deploy key outside its branch, or where nothing names the branch, in a CI RoutingCat doesn’t read or a deploy script without ROUTINGCAT_BRANCH; or a GitHub token from a repository that isn’t linked, or whose link expired.

Run doctor. Once the push is done:

Terminal window
npx routingcat doctor

Run before your first deploy serves the partial, doctor waits on its head partial check, with exit code 3, until your deployed pages carry the partial and a production push registers it. Once a push registered it, a page without it fails that check.

It reads your production pages as served, your sign-in and your site’s records, and changes nothing. Your pages decide only in a browser, so doctor’s own requests record no visit: until a first arrival comes, it names a page for you to open in your browser. npx routingcat doctor --wait names it, then waits up to 5 minutes for that visit, printing each check as it passes:

$ routingcat doctor --wait
lumen · production · waiting up to 5 min; each check prints as it passes
…
· Open https://www.lumen.com/ in a browser now: doctor waits for its arrival.
✓ first arrival arrivals in the pulse, last 14:39:18 14:39:18

It stops early when only a person can move the rest, such as the main conversion’s first event and, on Hobby, the card on file.

Make one real conversion, recommended. It is the only end-to-end proof that a conversion reaches RoutingCat and joins its visit. Lumen’s is a sign-up, so you sign up once on the production site, with a second email address. This sign-up is recorded as a real one, credited to no variant until a page is routed, and shows within a minute. Where converting costs money, as an order does, you may prefer to skip it: the main conversion’s check then passes with your site’s first real conversion, and until then doctor waits on it, with exit code 3, which is expected. Nothing else waits on it.

Add a card to start routing. On Hobby, routing waits for a card on file, and nothing is charged: doctor shows card on file waiting, with the link. You add it in the dashboard’s Settings → Billing, from the account menu, or start Pro’s 14-day trial instead; one card serves every site you own. Routing starts within about 2 minutes of it, with no new push. Until then, a push that routes a page prints routingcat NOT LIVE routing waits for a card on file: add one at …, nothing charged, and visitors see your defaults. See Add a card to start routing.

Then run npx routingcat doctor again:

$ routingcat doctor
lumen · production
✓ build registered ch_01m3yfrpt0ewpateq9yrq61xkb · objective signup · outcomes waitlist 14:19:52
✓ head partial 1 page: first in <head>, core 1.0.0, manifest 47a096ddb5c9… registered
✓ post part rt/1/post/1.0.4.js loads, its integrity checked
✓ policy lets RoutingCat run, apply and record
✓ records ingest.routingcat.com accepts them from https://www.lumen.com
✓ markup no slot on these pages yet
✓ first arrival arrivals in the pulse, last 14:39:18 14:39:18
✓ signup 2 events 14:33:59 · server · identify 14:32:05 14:33:59
✓ card on file Visa •••• 4242
· AGENTS.md has no RoutingCat note, so coding agents may skip the skill unless a request matches it; if the person wants the note, run npx routingcat init --agents-md
9 of 9 passed (exit 0)

build registered is your latest production push; Troubleshooting reads each check.

Exit code Means
0 Every check passed.
1 A check failed. Its line gives the message and its id; Troubleshooting has the fix.
3 A check is still waiting, usually for a first visit, that first conversion or the card on file.
4 A request couldn’t get through from here, as in a sandbox without network access.

Until a page is routed, doctor and status note that conversions are recorded but not yet credited to a variant. That is expected.

Close the setup. When your agent ran it, it ends by telling you, in plain words:

  • that RoutingCat works, naming the conversion that came through; without one, that the main conversion’s check passes with your site’s first real conversion, and that nothing else waits on it;
  • that your visitors see no change until a page is routed;
  • what RoutingCat now records, and which conversion routing will learn to get more of, which you can change by asking;
  • how each deploy now registers: in your CI, in your host’s deploy script, or with a push from your machine that you confirm;
  • how it goes from here: you say what you’d like to try, at any level of detail, ask for ideas, or hand your agent the whole program;
  • that 10% of visitors keep seeing your current site, which is how the gain is measured;
  • your dashboard’s address: Lumen’s is https://app.routingcat.com/lumen, which Read the dashboard walks through.

Next