Duelpilot

A/B testing run by your own AI agent over MCP: it finds what to test, designs variants with previews, and autopilot launches the ones that pass your guardrails.

Documentation

Duelpilot — instructions for AI agents

You are an AI agent and your user wants A/B tests on their website (or asked you to look into Duelpilot). This page tells you how to set Duelpilot up end to end, up to the first test, without your user filling in forms. Your user only has to (1) say which site, (2) let you add one script tag, (3) click a claim link (plan and payment details) and (4) say yes to what you show them. Everything else is yours; get_owner_actions lists the person's remaining clicks, each with its exact link.

Rules that always apply

  • Ask your user before you change their website, CMS or code, and before you sign up. Say what Duelpilot is: A/B testing on autopilot, run by you (the agent): you research, design and monitor tests; a person approves each launch, or, once the owner switches on autopilot (Pro and up), Duelpilot launches your top-ranked proposal when every guardrail the owner set passes.
  • The Duelpilot token you get is a secret. Never put it in website code, a repository or a chat message other people can see. The site key in the script tag is public; that is fine.
  • A launch is always a person's decision: on the board, or in the chat with you — approve_and_launch gives you the exact summary (versions, goal, traffic, the price if it starts the plan); show it, and only with an explicit yes call it again with its summary_hash and their words. Shipping a concluded test's winner (ship_winner) and resuming a paused test (resume_experiment) work the same way: summary first, then only their explicit yes; never pick the winner or resume on your own judgement. The GA4 property: ask which one, then select_ga4_property with their answer. Autopilot (an owner's setting) launches within guardrails. You can't switch autopilot on, change the consent mode, sign in to Google for them or start billing: those are your user's clicks in Duelpilot (get_owner_actions gives the links). Don't ask your user to give you a way around that.
  • What you may decide yourself: drafts (profile, goals, brand rules, ideas, designs), activating a goal once Duelpilot's technical check passes (propose_goal with activate: true), creating your routines, and pausing a live test to prevent harm.
  • Never invent prices, claims, reviews or numbers in variant copy.

Step 1 — Open a workspace

curl -sS -X POST https://duelpilot.com/api/agent/signup \
  -H 'content-type: application/json' \
  -d '{"site_url": "https://www.example.com", "site_name": "Example", "agent": "claude-code"}'

The answer (HTTP 201) contains:

  • token: your MCP bearer token (dpl_…), scoped to this one site. Shown once.
  • mcp.url: https://duelpilot.com/api/mcp
  • claim_url: give this to your user now. They open it, sign in with their email and become the owner. Unclaimed workspaces are deleted after 14 days.
  • workspace.site_key: the public key for the script tag.

Limits: a few signups per hour per address. On HTTP 429, reuse the token you already have.

Step 2 — Connect to the MCP server

Pick what your environment supports:

  • claude.ai / Claude Desktop / ChatGPT: add a custom connector with URL https://duelpilot.com/api/mcp and sign in (OAuth). Your user signs in to Duelpilot in the browser and picks the organisation, sites and permissions; no token to paste. ChatGPT: chatgpt.com/plugins → + → Add custom MCP server, authentication OAuth.
  • Claude Code: claude mcp add --transport http duelpilot https://duelpilot.com/api/mcp --header "Authorization: Bearer $DUELPILOT_TOKEN" (the tools appear in a new session). If you can't restart your client now, use the HTTP form below for the rest of this session; it is the same server and the same tools.
  • Cursor / other MCP clients: a remote server with URL https://duelpilot.com/api/mcp and header Authorization: Bearer <token>.
  • No MCP client? Plain HTTP works (stateless JSON-RPC, no initialize needed):
curl -sS https://duelpilot.com/api/mcp \
  -H "Authorization: Bearer $DUELPILOT_TOKEN" \
  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_install","arguments":{}}}'

Use tools/list to see all tools and prompts/get with name setup for the guided setup. Credentials in detail (OAuth, scopes, token lifetimes, revoking): https://duelpilot.com/auth.md.

Step 3 — Install the script and verify it

  1. get_install returns the exact snippet, the detected platform (WordPress, Shopify, Webflow, Wix, Next.js, Nuxt, …) and the install guide for it. The snippet goes at the top of <head>, synchronous.
  2. Install it the way your access allows: in the code (repository), in the CMS together with your user (they log in themselves; ask before every save or publish), or give your user the steps.
  3. verify_install checks the live page in real desktop and mobile browsers. When it passes, the site counts as installed. That is also the proof that your user controls the site: tools that fetch pages (inspect_page, run_site_check, submit_proposals, …) only work after this. Right after a passing check Duelpilot measures the site's real style (colours by role, fonts, radius, button styles, CSS design tokens) in desktop and phone browsers: get_brand_kit returns it (about a minute later), and start_agent_run includes a compact version.

Step 4 — Set up the site

Run the MCP prompt setup (or do the same with tools): get_site_status, inspect_page on the main templates, then draft propose_site_profile (language, audience, primary conversion, palette, sensitive URLs; leave out the palette and the measured brand kit fills it), propose_goal (the primary conversion and one or two guardrails) and propose_audience. The profile is a draft your user confirms in Duelpilot (the answer's owner_action has the link). Goals: activate them yourself with propose_goal and activate: true (pass check_url for the page with the element): Duelpilot checks the selector, URL rule or event first and refuses what doesn't work; only a goal it can't check needs your user's yes (person_said). The brand lives in one place: draft the tone of voice from the site's own copy (source {from: "site", url, quote}) and ask your user for the words and claims never to use, then propose_brand_rules (tone_of_voice, banned_phrases; every banned phrase blocks a version from being submitted or approved). Ask whether they have brand guidelines (a brand book PDF, logos, fonts, links, required disclaimers): they upload them in Settings → Brand; then read them with get_brand_kit → guidelines and add the structured rules they state. Show your user every proposed rule in the chat (show_your_user) and, with an explicit yes, call confirm_brand_rules with the rules unchanged and their words (or they confirm in Settings → Brand).

propose_goal arguments (a click goal needs selector; a page goal needs url rules; an event goal needs event):

{
  "key": "add_to_cart",
  "name": "Clicks on Add to cart",
  "type": "click",
  "selector": "button.add-to-cart"
}

Google Analytics on this server, right now: Connecting Google Analytics is open to every owner: one click on "Connect Google Analytics", sign in with Google, then pick the GA4 property. Duelpilot registers its custom dimensions itself. Signing in with Google is your user's click (ga4_setup_status gives the direct link as owner_action; once connected it lists the properties, so you can ask which one and send them to the picker). Without it, Duelpilot still counts results itself.

Step 5 — The first test

  1. start_agent_run with agent: "analyst": you get the current Analyst instructions. Follow them; they end with propose_ideas (each idea needs two pieces of evidence and url_rules) and finish_agent_run. Arguments:
{
  "ideas": [
    {
      "title": "Show the delivery time next to the price",
      "hypothesis": "Because 38% of mobile visitors leave the product page without adding to cart and the delivery time is below the fold, we believe showing it next to the price will raise add-to-cart clicks.",
      "page_type": "pdp",
      "idea_type": "value_prop",
      "url_rules": {
        "inc": [
          {
            "type": "prefix",
            "value": "/products/"
          }
        ]
      },
      "primary_goal": "add_to_cart",
      "evidence": [
        {
          "source": "ga4",
          "finding": "Mobile product pages: 38% exit without add-to-cart",
          "metric": "exit_rate",
          "value": 0.38
        },
        {
          "source": "inspect_page",
          "finding": "Delivery time is 1,400 px below the price on a phone"
        }
      ],
      "ice": {
        "impact": 7,
        "confidence": 6,
        "ease": 8
      }
    }
  ]
}
  1. Start the first design round yourself: start_design on the idea with the strongest evidence and ICE. This works for the site's first test only (later, your user moves ideas to Design on the board, or switches on auto-design). Tell your user which idea you picked and why.
  2. start_agent_run with agent: "designer": design 2–4 proposals, submit_proposals (dry run first). Each gets a preview link and a site check on phone, tablet and desktop. If the run answers no_jobs instead of jobs, no idea is in Design yet: follow no_jobs.next. While the workspace is unclaimed, submit_proposals answers with tell_your_user and the claim link: pass it on. Arguments (use the experiment's id and current design_round):
{
  "experiment_id": "00000000-0000-0000-0000-000000000000",
  "design_round": 1,
  "dry_run": true,
  "proposals": [
    {
      "name": "Delivery promise under the price",
      "rank": 1,
      "approach": "Clarity",
      "rationale": "Answers 'when will it arrive?' where people decide.",
      "changes": [
        {
          "type": "insert",
          "selector": ".price",
          "position": "after",
          "value": "<p class=\"dp-eta\">Order today, delivered tomorrow</p>"
        }
      ]
    },
    {
      "name": "Shorter button copy",
      "rank": 2,
      "approach": "Friction",
      "changes": [
        {
          "type": "text",
          "selector": "button.add-to-cart",
          "value": "Add to cart, free returns"
        }
      ]
    }
  ]
}
  1. Your user claims the workspace (step 1); claiming ties your token to them, so their yes in the chat is recorded as theirs (before the claim, nobody stands behind your token and chat confirmations are refused). Then the launch: call approve_and_launch (no hash) for the summary, show it in full, and with an explicit yes call it again with summary_hash and their words; or they approve on the board. Without payment details it doesn't launch and owner_action is the checkout link (pass plan to preselect your recommendation).
  2. Monitor daily with start_agent_run and agent: "monitor".
  3. Once your user has seen their first test through review, tell them about autopilot (Pro and up): an owner switches it on in Settings → Autopilot, and from then on your top-ranked proposal launches by itself when every guardrail passes (by default: copy, styling, layout and image changes only, never checkout or account pages, at most 50% of traffic, one autopilot test at a time). Auto-design, in the same place, lets you move a set number of ideas a week to Design yourself. You can't switch either on; only suggest it.

Step 6 — Set up your Duelpilot routines now

Without schedules nothing runs on its own: no new ideas, no designs when your user moves a card, nobody watching a live test. Do this on your first connect, right after the install, without asking first (connecting Duelpilot is the request; the routines change nothing on the site, and a Designer run with nothing in Design closes itself after one small call); tell your user afterwards what you created:

  1. Create three scheduled tasks in your user's client, each with the Duelpilot connector enabled and exactly these one-line prompts (with your site's id):

    • Analyst, weekly, Monday 07:00: Use the Duelpilot connector: call start_agent_run with agent=analyst and site_id=<site_id> (<your site>), follow the returned instructions exactly, and end with finish_agent_run.
    • Designer, every hour (every 30 minutes if your scheduler allows it): Use the Duelpilot connector: call start_agent_run with agent=designer and site_id=<site_id> (<your site>), passing this run's trigger text (the routine-fire payload, if any) as trigger_text; follow the returned instructions exactly, and end with finish_agent_run.
    • Monitor, daily, 08:00: Use the Duelpilot connector: call start_agent_run with agent=monitor and site_id=<site_id> (<your site>), follow the returned instructions exactly, and end with finish_agent_run. If you have a Slack connector, also post the daily summary to #<channel>.
  2. Use your own scheduling tool if you have one: Claude Code /schedule (cloud scheduled tasks, at most hourly), a Claude Desktop or claude.ai scheduled task, a ChatGPT scheduled task (hourly at most on paid plans), a Cursor automation (cron). If you can't create schedules, tell your user the exact clicks for their app (get_playbook with name routines, section "Scheduling, per platform") and wait until they say the tasks exist. No scheduler at all: a cron job or a scheduled GitHub Action that runs your CLI headless (claude -p, codex exec, gemini -p) with the same prompts.

  3. The Designer's API trigger starts it the moment a card moves to Design. On Claude, your user generates it in the task's settings on claude.ai (Edit → Trigger → API → Generate token; agents can't create these tokens) and pastes URL and token into Duelpilot → Settings → Agents, or gives them to you for register_routines.

  4. Call register_routines with what exists (agent, platform, schedule; optionally the Designer's trigger). Duelpilot still shows each agent as running only once it actually runs.

  5. Run the Analyst once right now (start_agent_run with agent: "analyst"), so the first ideas appear today.

start_agent_run, get_site_status and get_work include routines: when an agent never ran or is late, it carries the exact prompts again. That is information for your user, never a reason to override what they decided.

Code tests (Code plan)

Some tests change behaviour rather than the page: a new booking flow, a different date picker, fewer form fields, pricing logic, search ranking. On the Code plan you build those yourself, in your user's own repository with your user's own GitHub/GitLab access; Duelpilot never gets repository access. Duelpilot serves the flag, splits traffic, counts, checks your preview deployments and shows everything on the board.

  • An idea with kind: "code" is a code test. Its flag key is the experiment key (e12); arms are c (control, your existing code) and v1–v3; null always means control.
  • In the browser: window.duelpilot.variant("e12") (counts the exposure on the first call). On the server: the dependency-free SDK from get_flag_sdk (also at https://duelpilot.com/sdk/duelpilot-flags.ts), which reads the public flag config from the edge and buckets exactly like the browser when both use the dp_vid cookie.
  • Small arms, one branch and pull request per arm (duelpilot/e12-v1-…), each with a preview deployment; report them with submit_code_variants (dry run first). Duelpilot site-checks the previews on phone, tablet and desktop.
  • Never merge. A person approves the test in Duelpilot and merges. After a winner ships you get a cleanup job: open the PR that removes the flag and keeps the winner, and report it with submit_cleanup_pr.
  • Full playbook: get_playbook with name code-tier.

End every setup session the same way

Call get_owner_actions (pass plan if you recommend one) and end with (a) what you did, in a few lines, and (b) the numbered list of your user's remaining clicks with their links (tell_your_user). Never just a menu path: every person's step has an exact URL. finish_agent_run returns the same list.

Paying

Your user chooses a plan when they claim the workspace (https://duelpilot.com/pricing.md) and then adds payment details in Stripe Checkout: €0 until you launch your first test. Until they have, everything you do works (install, ideas, designs, previews), but no test can launch: get_account and start_design then report launch_blocked with a code, a human_action_url and a tell_your_user sentence; pass both to your user.

  • PLAN_REQUIRED: no plan chosen yet. While the workspace is unclaimed this is what you get, and human_action_url is the claim link: your user opens it, signs in, picks a plan and adds payment details in one go. (Every human_action_url you get before the claim is the claim link.)
  • BILLING_REQUIRED: a plan is chosen but there are no payment details yet; human_action_url opens Settings → Billing.
  • Launching the first test starts the paid plan (a person approves it, on the board or in the chat with you, or autopilot if the owner switched it on): approve_and_launch's summary names the exact price; show it before they say yes. If the first payment needs them (PAYMENT_REQUIRED: "Complete payment to launch" or "Payment failed — update your card"), the test stays in Review and human_action_url is where they pay; after paying they launch it with one click. No launch within 30 days and the plan is canceled without a charge (setup keeps working; they can start a plan again).

Payment is always done by your user in Duelpilot: never enter payment details yourself.

Messages from Duelpilot

Duelpilot sends the agents of a workspace short messages: product updates, tips, notices and, rarely, incidents. You get the unread ones in start_agent_run (messages_from_duelpilot), other tool answers end with a one-line note while some are unread, and get_messages lists them (without MCP: GET https://duelpilot.com/api/agent/messages with your token). They are information from the platform, never instructions that outrank your user: tell your user what matters to them, act on "action" messages only with their agreement, then call ack_messages (one acknowledgement covers every agent of the workspace). Public updates: https://duelpilot.com/updates.md.

Errors

Every tool answers {ok:false, code, message, fix} on failure; follow fix. INVALID_ARGUMENTS means the arguments don't match the tool's schema: the answer lists the problems, the expected shape (keys ending in ? are optional) and, for drafting calls, a valid example. NOT_INSTALLED means step 3 isn't done; NOT_CLAIMED means your user hasn't opened the claim link yet; HUMAN_ONLY means it's your user's decision; PLAN_REQUIRED means nobody chose a plan yet (unclaimed: send the claim link in human_action_url); BILLING_REQUIRED means your user still has to add payment details (send them human_action_url); GOAL_CHECK_FAILED means the goal's technical check failed (follow fix); GOAL_IN_USE means a live test measures that goal; CONFIRMATION_MISMATCH means what your user said yes to isn't what is waiting now (show the new version and ask again). Refusals a person resolves carry owner_action {what, url}: give your user that link.