# cloud v0.1

MCP and API.

Ask dif.sh Cloud how an experiment is doing, or what needs action across a project. Agents ask over MCP at https://cloud.dif.sh/mcp. Scripts ask over REST at https://cloud.dif.sh/v1/. No read endpoint and no tool writes anything.

// cloudEverything on this page needs a dif.sh Cloud project on a paid plan. The CLI and the SDK work without it.

What it is

Cloud analyses every active experiment every 5 minutes and stores one verdict per experiment: turn_off, investigate, review, ship, conclude_inconclusive, ramp or keep_running. The Cloud UI, the REST API and the MCP server all read that one stored verdict, so the three never disagree. Verdict rules lists how Cloud picks one.

Nothing here edits an experiment. Ramps, kills and conclusions happen in your repo: an edit to dif/experiments/active/<id>.md, or a run of dif conclude. The dif-triage-experiments skill turns a verdict into that edit. When you ask, or on an unattended run, it opens a pull request for you to merge.

The CLI does not call Cloud. It makes no network calls and never asks you to log in. Read tokens are for MCP clients, scripts and CI.

Create a read token

A read token is the only credential the MCP server and the REST API accept. It belongs to you: it reads the projects you are a member of, and nothing else.

  1. Open your project on cloud.dif.sh, then Settings, then Keys.
  2. In the Read tokens card, under Create a read token, name the token after where it runs, for example claude-code or ci-triage.
  3. Pick Expires in: 30 days, 90 days or 365 days. The default is 90 days.
  4. Leave Scope as it is to limit the token to the current project, or tick All my projects to cover every project you belong to.
  5. Select Create token and copy it. Cloud stores only a SHA-256 hash, so it shows the token once.

A read token starts with dif_pat_, followed by 8 hex characters, an underscore and a 32-character secret. It can read and nothing more. Keep it in an environment variable named DIF_TOKEN. Every example on this page reads it from there, except VS Code, which prompts for it:

$export DIF_TOKEN=dif_pat_…

The same card lists your tokens with their prefix, project, expiry and last use. Select Revoke to end one. A revoked or expired token answers 401 on the next request.

// cloudNever commit a read token. It reads every experiment result in the projects it covers. The repo files below (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, .gemini/settings.json) hold no token value, so they are safe to commit. claude mcp add is different: it writes the token itself into ~/.claude.json in your home directory. Do not commit or share that file. Revoke a leaked token in Settings, then Keys.

Connect a client

Every client sends your read token in an Authorization: Bearer header to the same URL: https://cloud.dif.sh/mcp. The transport is Streamable HTTP. Browser-based connectors (Claude.ai, ChatGPT) are not supported, because they need OAuth and the server accepts read tokens only.

Claude Code

$claude mcp add --transport http dif https://cloud.dif.sh/mcp \
    --header "Authorization: Bearer $DIF_TOKEN"

Your shell expands $DIF_TOKEN when you run the command, and Claude Code saves the server and the header, with the token in plain text, in ~/.claude.json, outside the repo. To share the server with your team, commit a .mcp.json at the repo root instead. Claude Code expands ${DIF_TOKEN} when it loads the file, so each person sends their own token:

{
  "mcpServers": {
    "dif": {
      "type": "http",
      "url": "https://cloud.dif.sh/mcp",
      "headers": {
        "Authorization": "Bearer ${DIF_TOKEN}"
      }
    }
  }
}

Claude Code asks you to approve a server from .mcp.json before it uses it. Export DIF_TOKEN before you start Claude Code. Then run /mcp to check the connection. A wrong or missing token shows as failed with status 401.

Cursor

Add the server to .cursor/mcp.json:

{
  "mcpServers": {
    "dif": {
      "url": "https://cloud.dif.sh/mcp",
      "headers": {
        "Authorization": "Bearer ${env:DIF_TOKEN}"
      }
    }
  }
}

Cursor reads DIF_TOKEN from its own environment. Export it in your shell profile, then restart Cursor.

VS Code

Add the server to .vscode/mcp.json. GitHub Copilot chat in VS Code reads this file. VS Code prompts for the token the first time the server starts and stores it:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "dif-token",
      "description": "dif read token",
      "password": true
    }
  ],
  "servers": {
    "dif": {
      "type": "http",
      "url": "https://cloud.dif.sh/mcp",
      "headers": {
        "Authorization": "Bearer ${input:dif-token}"
      }
    }
  }
}

Codex

Add the server to ~/.codex/config.toml. Codex reads DIF_TOKEN and sends it as the bearer token:

[mcp_servers.dif]
url = "https://cloud.dif.sh/mcp"
bearer_token_env_var = "DIF_TOKEN"

Gemini CLI

Add the server to .gemini/settings.json in the repo, or to ~/.gemini/settings.json for every project. Gemini CLI replaces ${DIF_TOKEN} with the environment variable when it loads the file:

{
  "mcpServers": {
    "dif": {
      "httpUrl": "https://cloud.dif.sh/mcp",
      "headers": {
        "Authorization": "Bearer ${DIF_TOKEN}"
      }
    }
  }
}

Edit the file by hand, so that the token stays in your environment and not in the file.

MCP tools

The server lists four tools, in this order. All four carry readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: false.

toolinputreturns
list_projectsrepo?The projects the token can read: slug, name, repo and the number of active experiments (live).
get_overviewproject?, repo?Every active experiment with its verdict, reasons, next action and draft decision, sorted turn_off first and keep_running last, then by id. Call this first.
get_experiment_resultsproject?, repo?, experimentOne active experiment in full (experiment is the id in dif/experiments/active/<id>.md): counts per arm, an analysis per primary and guardrail metric, the sample ratio check, and progress toward the sample target. A concluded id answers not_found.
get_learningsproject?, repo?, surface?, limit?The learnings on each surface, and the decisions of concluded experiments. limit caps each list. It takes 1 to 100 and defaults to 20.

Pick the project with project (its slug) or repo. repo takes owner/name or a git remote URL, such as git@github.com:acme/shop.git, and is matched without regard to case. A value with no host means github.com.

A token limited to one project can leave both out. A token for all your projects must pass project or repo on every call to get_overview, get_experiment_results and get_learnings, even when you belong to one project. With neither, the tool answers ambiguous_project and lists the slugs the token can read in candidates. The agent retries with one. Inputs are strict: an unknown argument is an error.

A result has two parts. structuredContent is the data: the same JSON object the REST API returns, checked against the tool's output schema. The one text block is a short summary for people: a table for the overview, labelled lines for the rest. Agents read structuredContent.

An error is a result with isError: true and one text block that holds the error as JSON, with no structuredContent:

{"error":"not_found","message":"No such project or experiment for this token."}

The codes are the ones in Errors. One case differs: an argument that fails the input schema (an unknown key, a wrong type, a value that is too long) comes back as a plain-text error, not JSON. A bad token and the rate limit answer as HTTP 401 and 429, before any tool runs.

REST API

The base URL is https://cloud.dif.sh. Send Authorization: Bearer $DIF_TOKEN on every call except openapi.json.

method and pathreturns
GET /v1/projects?repo=owner/nameThe projects the token can read. repo is optional and filters to one repo.
GET /v1/projects/{slug}/overviewEvery active experiment with its verdict. Same object as get_overview.
GET /v1/projects/{slug}/experiments/{id}/resultsOne active experiment in full. Same object as get_experiment_results.
GET /v1/projects/{slug}/learnings?surface=&limit=Learnings and concluded decisions. limit takes 1 to 100 and defaults to 20. Same object as get_learnings.
GET /v1/openapi.jsonThe OpenAPI 3.1 document. No token needed.

{slug} is always the project slug. To find it from a repo, call GET /v1/projects?repo=owner/name first. Then ask for the overview:

$curl -fsS -H "Authorization: Bearer $DIF_TOKEN" \
    https://cloud.dif.sh/v1/projects/acme-shop/overview

The overview holds project, counts (experiments per verdict) and experiments, one row per active experiment.

Every successful response that needs a token has a meta object: api_version ("1"), generated_at and analysis_computed_at. Every response that needs a token, errors included, is sent with Cache-Control: private, no-store. openapi.json is public and is sent with Cache-Control: public, max-age=3600. A query parameter that appears twice answers 400. The JSON Schemas in openapi.json are also the output schemas of the MCP tools. The dif skills read the base URL from DIF_CLOUD_URL when it is set, and use https://cloud.dif.sh otherwise.

Fields an agent acts on

fieldwhat it holds
verdictOne of the seven verdicts. See verdict rules.
reasonsPlain-language reasons for the verdict.
next_actionkind (set_weights, conclude, fix_instrumentation, set_metric_direction or wait) and a short summary. set_weights adds weights, which sum to 100. conclude adds command, a dif conclude line with the decision in single quotes. Both add file, the experiment file, dif/experiments/active/<id>.md.
decision_draftThe one-line decision for ship, turn_off and conclude_inconclusive. Null for the other verdicts.
caveatsWhat the numbers do not cover, for example Conversions count events within 24h of first exposure. or Fixed-horizon test. Calls before the sample target are not made.
weightsThe weight of each variant, as the analysis used them. Before the first analysis, the weights in the repo.
days_live, window_daysWhole days since the first exposure, and whole days since the last weight change. Drafts that say over 14d use window_days.
primaryThe primary metric: metric, lift, ci, p_value and significant. lift and ci are relative: percent of the control rate, so 2.1 means +2.1%. ci is the 95% interval, an array of two numbers. lift, ci and p_value are null when there is not enough data.
analysesIn results only. One item per primary and guardrail metric, with status (analyzed, not_analyzed or insufficient_data) and one entry per treatment arm that has weight above 0 and exposures: lift, ci95, ci99, p_value, significant, harm and no_baseline. lift, ci95 and ci99 are null together. p_value can be null. no_baseline is true when control has no conversions, so no lift exists. analyses is empty before the first analysis.
arms, srmIn results only. Per arm: weight, is_control, exposures, conversions and rate in percent. arms is empty before the first analysis. srm is the sample ratio check: p_value (it can be null) and tripped.
progressIn results only. per_arm_min, required_per_arm and eta_days. The last two can be null, and so can progress.
meta.analysis_computed_atWhen the stored analysis ran. Null before the first run. Always null on the project list and on learnings.

Errors

An error returns a JSON body with error and message. ambiguous_project adds candidates, a list of slugs.

errorstatuswhat to do
unauthorized401The token is missing, malformed, revoked or expired. Check that the client sends your token: for most clients, that DIF_TOKEN is set where the client runs. If it does, create a new token. The WWW-Authenticate header starts with Bearer realm="dif".
not_found404The project or experiment does not exist, you are not a member of the project, the token is limited to another project, or the experiment is not active.
ambiguous_project409More than one project matches, or a token for all your projects named none. Retry with a slug from candidates.
plan_required402The project's organization has no active Cloud plan.
rate_limited429Wait for the number of seconds in Retry-After. It is 60.
unavailable503Cloud cannot answer right now. Your token is not the cause. Keep it and retry later.
bad_request400A parameter is invalid or repeated, for example limit outside 1 to 100.

Verdict rules

Cloud checks these rules in order. The first rule that matches sets the verdict.

#verdictwhennext_action.kind
0turn_offEvery treatment already has weight 0. The experiment is off; conclude it to clean up.conclude
1investigateThe data is broken. One of: the sample ratio check fails (p below 0.001); the experiment has been active more than 2 days with no exposures; an arm with weight above 0 has no exposures while another arm has more than 100; or the primary metric cannot be analysed.fix_instrumentation
2turn_offA treatment harms the primary metric, or a guardrail with a known direction.set_weights (control 100, others 0)
3reviewA guardrail with no direction set moved (p below 0.001), so Cloud cannot tell good from bad.set_metric_direction
4rampA rollout has run 7 days at its current weights, the treatment has 1,000 exposures in that time, and every guardrail is analysed, has enough data and has a direction.set_weights (next step)
5shipAn experiment has run 7 days at its current weights, every arm has reached the sample target, and the best treatment is significant in the good direction.conclude
6turn_offThe same 7 days and sample target, and a treatment is significant in the bad direction. Control stays.conclude
7conclude_inconclusiveAn experiment with the same 7 days and sample target and no significant effect, or with 42 days at its current weights.conclude
8keep_runningAnything else.wait (the summary gives the ETA in days when there is one)

Rules 5 to 7 apply to experiments only. A rollout that misses rule 4 stays keep_running, and its reasons say what is missing: days, exposures, or a guardrail that has no direction, too little data or no analysis.

The numbers behind the rules

  • Rollout or experiment. A rollout has exactly 2 arms and a treatment weight below 50. Everything else is an experiment. A rollout that reaches 50 is judged as an experiment.
  • Significance. A pooled two-sided z-test at alpha 0.05, split evenly across the treatment arms (0.025 each with two treatments). Lift is relative to the control rate and is reported with a 95% interval.
  • Sample target. Per arm, from a two-proportion power calculation on the observed control rate: power 0.8, a 10% relative minimum detectable effect, and the split alpha above.
  • Harm. The same test gives p below 0.001 and the treatment's rate is on the bad side for the metric's direction, with at least 20 conversions across control and the treatment together. The bar is 0.001, not 0.05, because Cloud checks again every 5 minutes. The 99% interval in the API (ci99) is for display. It does not decide harm.
  • No baseline. When control has no conversions there is no lift to report. Cloud still runs the test, sets no_baseline, and prints the two conversion counts in the reason.
  • Direction. Each metric is increase or decrease, set in Cloud under Metrics. The primary metric defaults to increase, with a caveat. A guardrail has no default.
  • Ramp ladder. The treatment weight steps through 1, 5, 10, 25 and 50. Control gets the rest.
  • Time. ship needs at least 7 days at the current weights, one weekly cycle. An experiment with 42 days at its current weights and no winner is concluded as inconclusive.
  • After a weight change. Lift, harm and the sample ratio check count only full days after the change, because a ramp mixes populations. window_days counts those days. days_live still counts from the first exposure. Arms at weight 0 are left out of lift and the sample ratio check.
// cloudWhat a verdict does not cover. Cloud analyses conversion and count metrics only. A revenue or duration primary metric sends the experiment to investigate. A revenue or duration guardrail is listed as not analysed, and the guardrail summary names it. A conversion counts when the event fires within 24 hours of the user's first exposure. The test is fixed-horizon: no Bayesian or sequential statistics, so Cloud names no winner before the sample target and 7 days.

Draft decisions

For ship, turn_off and conclude_inconclusive, Cloud writes decision_draft: one line, ready for dif conclude --decision. Lift and intervals carry a sign and 1 decimal place. p carries 3 decimal places, or reads p<0.001. The day count is window_days.

# ship
Shipped variant_a. +2.1% completed_checkout (95% CI +0.8% to +3.4%), p=0.004 over 14d. Guardrails: refund_rate clean.
# turn_off, harm (rule 2)
Killed. variant_a regressed refund_rate +4.2% (p<0.001) over 3d.
# turn_off, harm with no baseline (rule 2)
Killed. variant_a regressed checkout_error: 60 conversions vs 0 in control (p<0.001) over 3d.
# turn_off, significant loser (rule 6)
Killed. variant_a -3.1% completed_checkout (95% CI -5.4% to -0.8%), p=0.008 over 14d. Kept control.
# turn_off, already off (rule 0)
Killed. Traffic set to control on 2026-10-01.
# conclude_inconclusive
Inconclusive after 42d. variant_a +0.4% completed_checkout (95% CI -1.2% to +2.0%). No effect larger than ±2.0% likely. Next: re-run on a higher-traffic surface or abandon.

The guardrail part of a ship draft lists every guardrail with its status, for example refund_rate clean; aov not analyzed (revenue). With no guardrails it reads Guardrails: none declared.

Security

  • Read-only. No read endpoint and no tool writes anything. A read token has the read scope and no other.
  • Hashed tokens. Cloud stores a SHA-256 hash of each read token, never the token. A revoked or expired token fails on the next request.
  • Live membership. Every request checks your project membership at that moment. Remove a member and their tokens lose that project on the next request. Organization membership alone grants nothing. A project you cannot read answers not_found, the same as one that does not exist.
  • Other keys cannot read. Every read endpoint rejects publishable keys (dif_pk_) and ingest keys (dif_live_, dif_test_).
  • No sessions. The MCP server is stateless. It issues no session id, holds no connection open and authenticates every request on its own.
  • POST only, one message, 64 KiB. The MCP server takes POST only; every other method answers 405. A JSON-RPC batch answers 400. A body over 64 KiB answers 413.
  • No browser access. The MCP server answers 403 to a request whose Origin header is not on its allowlist, and it sends no CORS headers. A page in a browser cannot call it. Editors, terminals and CI send no Origin header and pass.
  • Rate limit. 120 requests per minute per token, shared between REST and MCP and counted on each server instance. An MCP client spends 2 of them to connect. Past the limit, Cloud answers 429 with Retry-After: 60.
  • No caching. Every REST and MCP response that needs a token, errors included, is sent with Cache-Control: private, no-store. The public openapi.json is the one exception.
  • No token logging. When a request fails, Cloud logs the id of the token. It never logs the bearer value or a request header.
  • Repo text is data. Hypotheses, learnings, decisions, reasons, caveats, draft decisions and owners come from your repo, so anyone with write access can put words in them. The project name comes from project settings. The API schema marks each of those fields "x-dif-untrusted": true and names its source. Every tool description and the dif skills tell agents to treat names, ids and text as data, not instructions.

Limits

  • Read-only. Ramps, kills and conclusions happen in the repo, through a PR you merge.
  • Analysis runs every 5 minutes. A response built on an analysis more than 30 minutes old carries the caveat Analysis is <n> min old.
  • An active experiment with no analysis yet shows keep_running with the reason Awaiting first analysis.
  • Results cover active experiments only. A concluded experiment answers not_found; its decision is in get_learnings.
  • Conversion and count metrics only. No revenue or duration (mean) tests.
  • No Bayesian or sequential tests, and no CUPED. The thresholds on this page are the same for every project.
  • 120 requests per minute per token, per server instance, across REST and MCP.
  • Read tokens expire after 30, 90 or 365 days. The default is 90.
  • limit on get_learnings takes 1 to 100 and defaults to 20.
  • Every call that reads a project needs a paid Cloud plan on that project's organization. Without one it answers plan_required (402). list_projects and GET /v1/projects still list the project.
  • The CLI has no command that reads Cloud. Use MCP or REST.