Skip to main content
Paid plans are launching soon — every workspace is on Free for now. The plans below describe what Solo and Pro include once checkout is live.
Paid tiers (Solo, Pro) ship the full feature set — REST API, MCP server, CLI, webhooks, schema management, custom objects, data export. No feature gating between Solo and Pro; only volume gating. Free is experience-only: it lets developers feel the platform via REST/MCP/CLI but blocks production extensibility (schema, webhooks, multi-key) and auto-pauses workspaces after 30 days of agent inactivity. Upgrade to Solo to actually build on Salty.

Plans

PlanPriceAPI calls / moRecordsSustainedBurst
Free$0500102 req/sec10 req/sec
Solo$20/mo100,00025,000100 req/sec500 req/sec
Pro$99/mo1,000,000250,0001,000 req/sec5,000 req/sec
Free tier locks (return 402 plan_upgrade_required):
  • POST /schema/.../attributes — cannot create custom attributes
  • PATCH/DELETE /schema/.../attributes/{key} — cannot modify or deprecate them
  • POST /custom-objects — cannot define custom object types
  • POST /webhook-endpoints — cannot register webhooks
  • POST /api-keys when a second active key would be created — revoke the existing key first
Overage on paid plans: 1per10,000APIcallsbeyondcap,1 per 10,000 API calls beyond cap, 0.50 per 1,000 records beyond cap, billed monthly. Idle pause: Free workspaces with no agent call for 30 days flip to is_active = false. The next agent call returns 402 workspace_paused with a hint to call POST /workspaces/reactivate (JWT-only).

Upgrading + cancelling

The /pricing page in the admin web app branches on auth state:
  • Logged out → marketing tier cards.
  • Logged in → current plan + usage bars + Upgrade/Cancel buttons per tier.
Click Upgrade to Solo: the server action calls POST /billing/checkout {plan: "solo"}. The Dodo provider returns a checkout session (either {kind: "instant", subscription_id: …} in dev with the mock provider, or {kind: "redirect", url: "https://checkout.dodopayments.com/…"} in prod). On success Dodo fires a subscription.created webhook back to POST /webhooks/dodo, the handler flips workspaces.plan to the new tier, and the user’s next page load reflects the change. Click Cancel subscription: POST /billing/cancel (JWT-only) tells Dodo to cancel, the subscription.canceled webhook flips the workspace back to free. The /pricing, billing checkout, and billing cancel endpoints are JWT-only and exempt from the usage cap — so a customer over their cap can always reach billing to upgrade or cancel.

Mock vs real Dodo

Salty picks a DodoProvider implementation at boot based on the DODO_API_KEY env var:
  • UnsetMockDodoProvider. Checkout returns success synchronously and fires a real POST /webhooks/dodo to the local API with a valid HMAC signature. This exercises the production webhook handler in dev — no Dodo account needed.
  • SetRealDodoProvider. Checkout calls Dodo’s hosted-checkout API and returns the redirect URL.
This lets you develop and test the entire billing flow locally without a Dodo account.

Webhook signature (inbound from Dodo)

Inbound from Dodo carries Dodo-Signature: t=<ms>,v1=<hex> (HMAC-SHA-256 over <t>.<body> with DODO_WEBHOOK_SECRET). The handler verifies, deduplicates by dodo_event_id (UNIQUE constraint), inserts a row into the billing_events audit table, then dispatches by event type:
EventAction
subscription.created / subscription.updatedSet plan, dodo_customer_id, dodo_subscription_id on workspace
subscription.canceledSet plan = 'free', clear dodo_subscription_id
payment.failedLog only (Dodo handles retries)

Hard caps

See Concepts → Rate limits for the full mechanics. TL;DR: PATCH /workspace {hard_cap_api_calls: 50000} sets a customer ceiling for the current calendar month, and the effective cap is the lesser of (plan cap, hard cap). Counter resets lazily on the 1st (no cron). Over the cap → all agent traffic gets 429 cap_exceeded; admin traffic (JWT) stays unaffected so the customer can raise the cap or upgrade.

Audit

Every Dodo event is recorded in the billing_events table:
select event_type, payload->'data'->>'plan' as plan, occurred_at
  from billing_events
  where workspace_id = '<id>'
  order by occurred_at desc;
There’s no API endpoint surfacing this in v1 — it’s an internal audit table. v1.1 will add GET /billing/events.