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_launchgives 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 itssummary_hashand 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, thenselect_ga4_propertywith 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_actionsgives 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_goalwithactivate: 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/mcpclaim_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/mcpand 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/mcpand headerAuthorization: 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
get_installreturns 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.- 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.
verify_installchecks 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_kitreturns it (about a minute later), andstart_agent_runincludes 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
start_agent_runwithagent: "analyst": you get the current Analyst instructions. Follow them; they end withpropose_ideas(each idea needs two pieces of evidence andurl_rules) andfinish_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
}
}
]
}
- Start the first design round yourself:
start_designon 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. start_agent_runwithagent: "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 answersno_jobsinstead of jobs, no idea is in Design yet: followno_jobs.next. While the workspace is unclaimed,submit_proposalsanswers withtell_your_userand the claim link: pass it on. Arguments (use the experiment's id and currentdesign_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"
}
]
}
]
}
- 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 withsummary_hashand their words; or they approve on the board. Without payment details it doesn't launch andowner_actionis the checkout link (passplanto preselect your recommendation). - Monitor daily with
start_agent_runandagent: "monitor". - 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:
-
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>.
- Analyst, weekly, Monday 07:00:
-
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_playbookwith nameroutines, 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. -
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. -
Call
register_routineswith what exists (agent, platform, schedule; optionally the Designer's trigger). Duelpilot still shows each agent as running only once it actually runs. -
Run the Analyst once right now (
start_agent_runwithagent: "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 arec(control, your existing code) andv1–v3;nullalways means control. - In the browser:
window.duelpilot.variant("e12")(counts the exposure on the first call). On the server: the dependency-free SDK fromget_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 thedp_vidcookie. - Small arms, one branch and pull request per arm (
duelpilot/e12-v1-…), each with a preview deployment; report them withsubmit_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_playbookwith namecode-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, andhuman_action_urlis the claim link: your user opens it, signs in, picks a plan and adds payment details in one go. (Everyhuman_action_urlyou get before the claim is the claim link.)BILLING_REQUIRED: a plan is chosen but there are no payment details yet;human_action_urlopens 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 andhuman_action_urlis 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.