Skip to content

Search the documentation by title, section, or page text.

CLI & MCP

@botsi/cli is a command-line tool (botsi) and a local MCP server. Use it to manage an app's access levels, products, paywalls, placements and experiments, and to check whether its integration works. It runs against one app at a time, using that app's secret key and the Botsi management API (/v2/manage/*).

You need Node.js 20 or later on macOS, Linux or Windows, a Botsi app, and an Owner or Admin to turn on one switch in the dashboard.

Quick start#

npx @botsi/cli init

init asks for the app's secret key (input is hidden), checks it, and shows the app it belongs to:

✓ Key works: <App name> (<app id>)

It then saves the key under a profile name, asks which coding agent you use, and offers to write its MCP config:

  • Claude Code: .mcp.json in the current folder.
  • Codex: ~/.codex/config.toml (or $CODEX_HOME/config.toml).

The config names the profile. The key is never written into it.

When it finishes, open Claude Code in that folder and run /mcp to see the Botsi server connected. Then start the integration:

/mcp__botsi-<profile>__integrate

In Codex, paste the output of this command instead:

botsi integrate --print-prompt

If something is wrong (a wrong key, the management API switched off, the app not enabled for the Botsi API, a network error), init stops and says how to fix it. To target another API host (default https://api.botsi.com), pass --base-url:

npx @botsi/cli init --base-url https://api.example.com

In a script, skip the prompts and run these instead:

botsi profiles add my-app-prod < key.txt
botsi agent setup --client claude --profile my-app-prod --write    # or --client codex

Before you start#

  1. Turn on the management API. An Owner or Admin opens App Settings › General in the dashboard and turns on Management API. It's off by default, and every call answers 403 management_api_disabled until it's on.
  2. Copy the app's secret key from the same page.
  3. Pick the app. Every command acts on the one app its key belongs to. Publishing a placement or starting an experiment changes what that app's users see. The tools work on production apps, so consider trying changes on a separate test app first.

Connecting your coding agent#

init does this for you. To do it later, or for another app:

botsi agent setup --client claude --profile my-app-prod --write    # adds the server to ./.mcp.json
botsi agent setup --client codex --profile my-app-prod --write     # appends to ~/.codex/config.toml

Without --write it only prints the config. It also prints a short snippet to add to CLAUDE.md or AGENTS.md so the agent knows how to use Botsi in that repository.

To set it up by hand:

claude mcp add botsi-my-app-prod -- npx -y @botsi/cli mcp --profile my-app-prod

Cursor reads this from .cursor/mcp.json. Register one server per app, named botsi-<profile>, so an agent working on one app can't drift to another.

On Windows, wrap npx in cmd /c

Outside WSL, npx is a .cmd script that agents can't start directly. init and agent setup handle this for you. If you set it up by hand, use these forms instead.

Claude Code:

claude mcp add botsi-my-app-prod -- cmd /c npx -y @botsi/cli mcp --profile my-app-prod

Cursor:

{
  "mcpServers": {
    "botsi-my-app-prod": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "@botsi/cli", "mcp", "--profile", "my-app-prod"]
    }
  }
}

Having an agent integrate Botsi#

The MCP server's integrate prompt has the agent integrate Botsi end to end. Agents without MCP prompts, such as Codex, use botsi integrate --print-prompt. Add --platform ios|android|flutter|react-native|web|server to narrow it.

The agent works in nine phases and stops for your go-ahead after each one. It never runs a destructive action without asking.

PhaseWhat the agent does
1. PrerequisitesReads the app and its health, then lists what only you can do in the dashboard: the management API switch, store credentials, store server notification URLs, and copying the public key
2. Confirm the appShows the app name and id and asks you to confirm it's the one you mean to change
3. DiscoverFinds your platforms, purchase library, where paywalls appear, and which store product ids you sell
4. CatalogCreates access levels (the entitlement keys your app checks), products with their store ids, and paywalls
5. PlacementCreates one placement per place a paywall appears, with a fallback paywall and traffic weights. Validates it, and publishes only after you agree
6. Runtime codeWrites the Botsi API calls into your app: create the user profile, fetch the paywall, report that it was shown, validate purchases
7. VerifyAsks you to run the app as a new test user and make a sandbox purchase, then uses the per-user health trace until every step passes
8. ExperimentAsks which kind you want, creates and validates it, and starts it only with your go-ahead
9. ReportLists what it created (with ids), what code it changed, and what's left for you

The agent follows a built-in guide for each platform: iOS (Swift, URLSession, StoreKit 2), Android (Kotlin, Google Play Billing), Flutter (Dart, in_app_purchase), React Native (fetch, for example with react-native-iap), web (browser fetch with the public key, sales recorded from your server) and server (a custom webhook for sales no store reports, plus store server notifications). Read one with botsi guide ios.

Running experiments#

There are two kinds, and you choose. The agent asks rather than picking for you.

KindtypeHow traffic splitsFields
Static A/B teststaticFixed shares between two or more paywallsarms (each a paywallId with weightBps, totalling 10000 = 100%) and controlPaywallId (the baseline, one of the arms)
Dynamic ModeldynamicA share (modelTrafficBps) goes to the Botsi model, which picks the best candidate for each user. Everyone else sees the baselinebaselinePaywallId, modelTrafficBps, candidatePaywallIds (at least two)
botsi experiments create --name "Pricing model" --type dynamic --goal-metric rpi \
  --baseline-paywall-id 11 --model-traffic-bps 5000 --candidate-paywall-ids 12,13 \
  --placement-plan-id 42

A Static A/B test's arms is a list of objects, so put it in a JSON file and pass it with --file.

A new experiment appears in the dashboard's placement editor right away, added to its placement's draft at 0% traffic. Creating it changes no traffic, and deleting a draft experiment removes that entry.

Starting an experiment doesn't send it traffic

After start, save the placement draft with a weight for the experiment (all weights total 10000), validate it, then publish.

Botsi staff enable experiments per app. Until then, botsi app shows experimentsEnabled: false: you can create and validate experiments, but starting one is refused with 422 inference_settings_required. Ask Botsi to enable experiments for your app.

A started experiment is fixed. To change its configuration, duplicate it into a new draft. Changing a live experiment's arm allocation is a separate switch Botsi turns on (allocationChangeEnabled).

Checking your integration#

botsi health                                                 # the last 24 hours
botsi health --since 2026-11-04T10:00:00Z                    # a window up to 30 days back
botsi health --profile-id 0072102a-c00c-4ea5-9271-1b6e975f2d63   # trace one user end to end

Agents use the health_get tool. The report has an overall result (pass, warn or fail) and a list of checks. Each check is pass, warn, fail or not_applicable, with a hint saying what it saw and what usually fixes it.

AreaChecks
Setupsetup.placement_published, setup.store.app_store, setup.store.play_store, setup.store.stripe, setup.experiments_enabled
Profilesprofiles.created
Paywallpaywall.decisions, paywall.impressions (warns when under 10% of decisions are shown), paywall.fallbacks
Purchasespurchases.validation, purchases.attribution
Notificationsnotifications.app_store, notifications.play_store
Revenuerevenue.production, revenue.sandbox

With --profile-id, a trace follows one user: profile_created → paywall_decided → paywall_shown → purchase_validated → purchase_bound → revenue_recorded. Sandbox purchases are never bound to a paywall session, by design. When request logs are available for the app, the report also lists failed requests (route, status, error code).

botsi health exits 1 when the report fails, so CI can gate on it. Reports can lag by about a minute, so re-check before changing code that looks right.

Using the CLI directly#

Managing profiles#

A profile is one app's secret key saved under a name. The key is checked against the API before it's saved.

botsi profiles add my-app-prod              # prompts for the key, hidden
botsi profiles add my-app-test < key.txt    # or reads it from stdin
botsi profiles list                         # shows the last four characters of each key
botsi profiles use my-app-test              # makes it the default
botsi profiles remove my-app-prod

The CLI never accepts a key as a command-line argument, since it would land in your shell history. Profiles live in $XDG_CONFIG_HOME/botsi/config.json (default ~/.config/botsi/config.json), readable by you only.

A command uses the first of these it finds: --profile <name>, BOTSI_PROFILE, BOTSI_SECRET_KEY (meant for CI, with BOTSI_BASE_URL, default https://api.botsi.com), then the default profile.

Running commands#

PurposeCommands
Your app and its healthbotsi app, botsi health
Catalogbotsi access-levels, botsi products, botsi paywalls
Trafficbotsi placements, botsi experiments
Integration helpbotsi guide, botsi integrate
Setupbotsi init, botsi profiles, botsi agent setup, botsi mcp

Every command lists its subcommands and flags with --help:

botsi experiments --help
  • Output: every command prints App: <name> (<id>) to stderr, then the response as JSON on stdout, so it pipes cleanly into jq.
  • Request bodies: pass --file <path> (or --file - for stdin), plus a flag for each plain field, such as --revision or --placement-key. A flag wins over the same field in the file. Lists of ids are one comma-separated flag (--candidate-paywall-ids 12,13).
  • Retries: creates and lifecycle actions carry an idempotency key. If you don't pass --idempotency-key, the CLI makes one and prints it. Rerun with it to retry without acting twice.
  • Revisions: drafts carry a revision. Send the current one with saves and lifecycle actions. A stale one answers 409 revision_conflict.
  • Confirmation: destructive commands ask first, naming the app. In scripts, pass --yes. Without a terminal and without --yes, they refuse.
  • Exit codes: 0 on success, 1 when the API refused (its code and details are printed) or the request failed, 2 on a usage error.

The key you give an access level (--access-level-id, for example pro) is what your app checks for the entitlement. An access level can be renamed or deleted only while no product uses it and no customer has it. The default access level is never deleted.

MCP reference#

botsi mcp [--profile <name>] serves one app over stdio until the agent disconnects. Every tool result starts with App: <name> (<id>). If the app can't be read at startup (a wrong key, the switch off), the server still runs and each tool answers with the reason. Fixing it takes effect on the next call.

Each tool maps to a CLI command: x_y is botsi x y. Read tools change nothing. Write tools change data with no live effect. Destructive tools delete, or change live traffic or experiment state.

ToolEffectWhat it does
app_getReadThe app this key belongs to, with experimentsEnabled and allocationChangeEnabled
health_getReadIntegration health report and per-user trace
access_levels_list, _getReadAccess levels
access_levels_create, _updateWriteCreate an access level, or rename an unused one
access_levels_deleteDestructiveDelete an unused access level
products_list, _getReadProducts and their store ids
products_create, _updateWriteCreate or update a product
products_deleteDestructiveDelete a product nothing uses
paywalls_list, _getReadPaywalls
paywalls_create, _updateWritePaywalls built outside Botsi
paywalls_deleteDestructiveDelete a paywall nothing uses
placements_list, _get, _historyReadPlacements and their published versions
placements_create, _save_draftWriteCreate a placement draft, or save its draft
placements_validateReadWhat a publish would refuse
placements_publishDestructiveMake the draft live, changing what users see
placements_discardDestructiveThrow the draft away
placements_deleteDestructiveDelete a placement that never served
experiments_list, _getReadExperiments
experiments_create, _save_draft, _duplicateWriteDrafts
experiments_validateReadWhat a start would refuse
experiments_start, _pause, _resume, _completeDestructiveLifecycle
experiments_candidates_retireDestructiveRemove a Dynamic Model candidate (botsi experiments retire-candidate)
experiments_allocationDestructiveChange a live experiment's arm allocation
experiments_deleteDestructiveDelete a draft experiment

Read tools are marked read-only, and destructive tools carry destructiveHint. These are hints to your MCP client, which decides whether to ask you before a call.

The server also exposes two kinds of resources and one prompt:

  • botsi://openapi/web-api: the Botsi API's OpenAPI document, with runtime and management routes and their schemas.
  • botsi://guides/<platform>: the six integration guides.
  • integrate: the integration prompt, with an optional platform argument.

Security#

  • The key stays in your profile file, readable by you only. Agent configs name the profile, never the key.
  • The CLI masks keys in output to their last four characters and never prints them in full.
  • The management API never returns keys, key hashes, notification keys, or webhook and store settings.
  • Destructive actions ask first in the CLI. In MCP clients they're marked so your client can ask.

What it doesn't do#

  • Create apps, list your other apps, show keys, or change webhook or store settings. Use the dashboard.
  • Start experiments before Botsi enables them for the app, or change a live allocation before Botsi turns that on.

Troubleshooting#

What you seeWhat to do
401 unauthorized or "Botsi refused that key"You used a wrong or public key. Copy the app's secret key from App Settings › General
403 management_api_disabledAn Owner or Admin turns on Management API in App Settings › General
"This app is not enabled for the Botsi API yet. Ask Botsi to enable it."Contact Botsi to enable the app
422 inference_settings_required on startExperiments aren't enabled for the app yet. Ask Botsi
403 allocation_change_disabledChanging a live allocation isn't turned on for the app. Ask Botsi
409 revision_conflictSomeone, or the dashboard, changed the object. Read it again and retry with the new revision
409 access_level_in_use, paywall_in_use, experiment_referencedIt's still in use. Remove the reference first
429 rate_limitedWait a moment and retry
/mcp in Claude Code shows the Botsi server failing on WindowsUse the cmd /c npx … form, or rerun botsi agent setup --write
A new experiment gets no trafficGive it a weight on the placement and publish. See Running experiments
The health check shows missing data right after testingReports lag about a minute. Re-check before changing code