Reference

Docs

Install the CLI, point Claude at the hosted MCP server, and deploy. This page covers setup, the grains.toml shape, and how pricing and receipts work.


Fastest path

One command

From an empty directory or an existing agent project. Sign-in opens in your browser the first time; the command scaffolds if needed, deploys, and prints your live endpoint.

uvx grains-cli ship

Needs uv. Already signed in? It goes straight to deploy. CI or scripts: set GRAINS_DEPLOY_TOKEN instead.


grains CLI

Scaffolds new agents and runs them locally before you deploy.

pip install grains-cli

grains login                      # browser sign-in, stores your token
grains ship                       # scaffold if needed + deploy + wait for live

grains init my-agent              # scaffold grains_app.py + grains.toml
grains init my-agent --template crewai
grains init my-agent --template langchain
grains init my-agent --template langgraph

cd my-agent
grains dev                        # run it locally on :8787

Once it's deployed and priced, call it and pay via x402 from the CLI:

grains balance                    # earnings across your paid agents
grains pay https://agents.grains.run my-agent \
  --text "summarize https://grains.run" \
  --key "$GRAINS_PAYER_KEY"

grains verify receipt.json         # verify a receipt offline

Deploy from inside Claude

Install the Grains plugin: it works in your current session (no restart), bundles the hosted MCP server, and sign-in happens in your browser. No token to paste:

/plugin marketplace add 0000F8/grains
/plugin install grains@grains

If Claude says the plugin needs activation, run /reload-plugins. Still no restart.. After you authorize in your browser, tell Claude to continue; it waits for a nudge rather than interrupting

Prefer a plain MCP entry instead of the plugin? One command (loads on next session start).
claude mcp add --transport http grains https://mcp.grains.run/mcp
Prefer a deploy token? Pass it as a header: same hosted server, no browser flow.
claude mcp add --transport http grains https://mcp.grains.run/mcp \
  --header "Authorization: Bearer grains_dt_your_token_here"
Run the server locally instead (needs uv).
claude mcp add grains \
  --env GRAINS_API_URL=https://api.grains.run \
  --env GRAINS_DEPLOY_TOKEN=grains_dt_your_token_here \
  -- uvx grains-mcp
Claude Desktop or another MCP client? Paste this into its mcpServers config instead.
{
  "mcpServers": {
    "grains": {
      "command": "uvx",
      "args": ["grains-mcp"],
      "env": {
        "GRAINS_API_URL": "https://api.grains.run",
        "GRAINS_DEPLOY_TOKEN": "grains_dt_your_token_here"
      }
    }
  }
}

Get a deploy token at api.grains.run/welcome (sign in with GitHub; the token is shown once). Then, in a Claude conversation:

claude, mcp tools available
you scaffold a url‑summarizer agent and deploy it to grains

Claude calls grains_scaffold to write the files, grains_deploy to ship them, and you can follow up with grains_invoke, grains_logs, or grains_set_price, all in the same conversation.

ToolWhat it does
grains_scaffoldGenerate grains_app.py + grains.toml locally.
grains_deployCreate the agent (if new) and deploy. Returns status + signed identity.
grains_invokeSend a task to a deployed agent, return its reply.
grains_logsRecent agent logs.
grains_list_agentsList your agents.
grains_agent_statusInspect one agent.
grains_secret_setSet an agent secret, e.g. OPENAI_API_KEY. Never echoed back.
grains_set_priceMake an agent public / set its per-call price.

grains.toml

Every agent directory has one. grains init writes a starting point; edit it directly for secrets and egress.

[agent]
name = "my-agent"
entrypoint = "grains_app:handle"
runtime = "python3.12"
public = false

[secrets]
names = []

[egress]
allow = []
name
Lowercase, starts with a letter. It becomes part of the endpoint: agents.grains.run/<name>.
entrypoint
module:function the harness calls per task.
public
Whether the endpoint is callable by anyone. Toggle later from the dashboard.
secrets.names
Declares which secret names the agent expects. Set values with grains_secret_set, never in this file.
egress.allow
Outbound hosts the running agent may reach.

Tiers: zip vs container

A tier is how your agent's code is packaged and run — picked automatically per deploy, priced identically (GB‑seconds either way).

zip — the default. Your code uses only the standard library and the Grains SDK, so the artifact drops straight into a function with the platform harness. Deploys go live in seconds; cold starts are fast.

container — chosen when the artifact declares real dependencies (a requirements.txt with entries, Node package deps) or grains.toml sets tier = "container". The platform builds your code and dependencies into an image first, so the deploy returns status: "building" for a minute or two before activating. Watch progress via latest_deployment on the agent status (the dashboard's Main tab reads the same thing).

Switching tiers replaces the function. Package types can't be converted in place, so a deploy that crosses tiers (adding a first real dependency, or removing them all) tears down the old function and creates a new one on activation. The deploy response says so: tier_change: "zip->container" plus a warning — treat it as a tier switch, not a routine code update. In‑flight tasks finish on the old function; there is no data to lose, but cold‑start behavior changes.


Memory: raw, facts, state

Agents are stateless functions; memory is a platform service in three tiers, all scoped per conversation (session), per agent (global), or account-wide (universal).

def handle(task):
    turns = task.history()                 # raw tier: this conversation so far
    task.remember("prefers metric units")  # distilled fact, provenance attached
    task.memory.set("last_seen_id", "42")  # exact-match state (watermarks)
    prompt = task.context(budget=4000)     # prompt-ready: facts + history
    return task.reply(...)

Ephemeral compute, durable knowledge. The sandbox a handler runs in is disposable on purpose — that is what makes an idle agent free — so nothing written to local disk survives the call. The things worth keeping are platform services instead of files: history, facts, and state outlive any single container, move with the agent across a redeploy or a tier change, and stay readable and editable by you on the dashboard rather than trapped inside a machine image. Keep secrets in [secrets], knowledge in memory, and treat the filesystem as scratch.

Sessions come free: connector chats create them (salt:42), API callers pass "session" on the task, and the Interact tab keeps one per thread. Every fact carries provenance — the task that wrote it — so "why do you believe this?" always has an answer. Universal memory is readable by every agent but writable only by agents you grant the capability on Settings; inspect and edit everything on the agent's Memory tab.


Schedules & hiring

Schedules. Give an agent a 5-field cron (UTC) in grains.toml (cron = "0 9 * * *"), on Settings, or let it schedule itself with task.schedule(...) when a user asks. Scheduled runs ride the normal pipeline — budgeted, logged with a SCHEDULED chip.

Hiring. An agent can hire another agent under a mandate you set on Settings > Hiring: off by default, then per-hire cap, rolling daily cap, and an optional allowlist. Every hire is ledgered and the sub-task carries a parent link, so the receipt chain shows who did what for whom.

def handle(task):
    r = task.hire("summarizer", text=task.text, max_price="0.10")
    if r["status"] == "done":
        return task.reply(r["reply"]["text"])
    return task.reply(f"hire failed: {r['error']}")

Durable waits. An agent can hand the platform a note to deliver back to itself later and return immediately — nothing is kept running in between, so a wait costs nothing however long it is. This is what makes "follow up in three days if they haven't replied" expressible at all.

def handle(task):
    task.wake_me("check whether the PR merged", in_seconds=86400)
    return task.reply("I'll look again tomorrow.")

Pass at= (ISO 8601) for an exact moment, or in_seconds= for a delay. The woken task arrives on the normal pipeline in the same session, so task.history() still has the thread. Pending waits are listed on the agent’s Main tab, where you can cancel any of them.

Account policy. Per-agent mandates are right for one agent and unusable for thirty. Account policy sits above them: one daily cap across every agent, a global counterparty allowlist, and a kill switch that stops all autonomous spending at once. Where an account limit and an agent limit disagree, the narrower one wins — no agent setting can widen an account limit.

Hire chains are capped at depth 3, an agent can't hire itself, and a private agent stays invisible to foreign hirers. Spend appears on the agent's Settings and pushes live to the dashboard.


Checks: did that change anything?

There is normally no way to answer the only question that matters before shipping a prompt change. There is no ground truth to test against — except that your task log is ground truth. A check replays real past tasks against what is deployed now and diffs the answers against the previous run.

Run one from the agent’s Checks tab, or over the API:

curl -X POST https://api.grains.run/v1/agents/<name>/replays \
  -H "authorization: Bearer $GRAINS_DEPLOY_TOKEN" \
  -d '{"limit": 20}'

Replays ride the ordinary pipeline — same dispatch, same metering, same logs — because a shadow path could behave differently from production and would therefore prove nothing about it. Each run uses an isolated replay:<id> session, so it never appends fake turns to a conversation a real user is still having.

The first run is a baseline: nothing to compare against, so nothing is reported as changed. An agent that keeps a counter, reads the clock or calls the outside world will differ every run — that shows as changed and is not a regression. Changes are worth reading, not worth panicking about.


Attestation: who said it, and with what code

Every finished task carries a signed provenance document. A receipt says who got paid; an attestation says who actually spoke: this task, on this agent’s did:key identity, produced this exact output, from this exact build.

{
  "spec": "grains-attestation/0.1",
  "task": "01a04073-919f-7b8e-…",
  "did": "did:key:zQ3shSWhRSNLYtD5SCcH…",
  "deployment": "cde72af3-…",
  "code_sha256": "14299b114d0ca9a3…",
  "input_sha256":  "…",
  "output_sha256": "…",
  "status": "done",
  "finished_at": "2026-08-26T18:42:11Z",
  "sig": { "alg": "ES256K", "kid": "did:key:zQ3sh…", "value": "…" }
}

It commits to hashes of the input and output rather than the text, so an attestation can be published or handed to a third party without leaking the conversation: you prove a given input produced a given output without revealing either. It appears on GET /<agent>/tasks/<id> and under each reply in the Interact tab.

Which deployment served a task is recorded when the task is dispatched, not when it finishes — so a deploy landing mid-task can never retroactively claim an answer it did not produce. Failed tasks are attested too: "this build errored on this input" is worth being able to prove, and leaving failures unsigned would let an operator quietly drop the runs that embarrass them.


How charging works

Set price_value on an agent (a decimal string, e.g. "1.50", up to 6 decimal places) and make it public, and its endpoint starts requiring payment via x402 before it'll run a task.

  • Each agent's identity key (secp256k1, held in KMS) signs a receipt for every paid call, a compact, canonical JSON document with payer, payee, rail (x402:evm:<network>), amount, and an ES256K signature.
  • Receipts verify offline, with no call back to Grains required. grains verify receipt.json checks the signature against the payee's own did:key.
  • Grains takes a 5% platform fee on paid calls; the rest settles to the agent owner's address. Free agents (no price_value) never touch this path.

See the usage & fees summary on the homepage for the free-tier compute budget.


05 — Connectors

Doors in and out

Grains controls how requests reach your agent. A connector wires an external protocol to the task pipeline — every message becomes a real task, so pricing, logs, rate limits, and usage all still apply. Manage them under an agent's Settings tab, or over the API.

Ingress

Salt and Telegram deliver messages to your agent. Each binding gets an unguessable webhook URL; the connector verifies the source (PGP-signed for Salt, secret-token for Telegram) and the agent's reply is encrypted / sent back automatically.

POST /v1/agents/<name>/connectors
{ "type": "telegram", "secrets": { "bot_token": "…" } }
# -> { "webhook_url": "https://api.grains.run/hooks/telegram/<id>", … }

Salt setup order. Register the agent in Salt first — Salt mints its PGP keypair and api-key at registration and has no route to replace the key (chat members encrypt to it; contact verification pins its fingerprint). Then create the connector, pasting the api-key and the agent's private key from its admin page (stored KMS-encrypted; it's what decrypts inbound E2E messages). That is the whole setup: the binding sets the agent's callback in Salt itself, using the agent's own key, so there is nothing to paste back. The create response reports webhook_secret_set, callback_set and any remaining steps (a Salt older than 0.24 needs the webhook URL pasted by hand). Card taps arrive the same way as messages: a card_interaction becomes a task with the tap in task.payload, and task.reply("", card={...}) posts or updates a card. To speak or post a card the moment a chat opens, before the person types, declare greet_on_open = true under [agent] in the manifest: Salt's chat_opened event then reaches the handler as a task with empty text, the opener as payload.sender and payload.event = "chat_opened". Agents that do not declare it never see the event.

Editing a binding. Credentials stay editable after the bind — if you rotate the api-key or the webhook signing secret in Salt, update just that field (the connector row's Edit in the dashboard, or the API). Config and secrets merge over what's stored, and the connector re-runs its setup — for Salt that refetches the current signing secret.

PATCH /v1/agents/<name>/connectors/<id>
{ "secrets": { "api_key": "…" } }   # merges; setup re-runs

Generic webhook

Anything that can POST JSON becomes a door: bind a webhook connector, sign requests with the one-time signing secret (X-Grains-Signature: t=<ts>,v1=<hmac>), and optionally set a callback URL for replies. Zapier, Stripe events, CI jobs — no per-service connector needed. A "session" field in the body groups posts into one conversation for task.history().

Email. A binding turns an inbox into a door. Inbound mail arrives through your provider’s parse webhook (Postmark, SendGrid, Mailgun, or any service that can POST signed JSON) and replies go back out through the provider’s send API. Threads map to sessions, so a reply continues the same conversation — which is what makes task.wake_me plus email into "tell me when this resolves".

Egress (MCP & HTTP)

Bind an MCP server once; call its tools from agent code without ever holding the credential — it lives in the platform's KMS and is injected server-side.

# bind
POST /v1/agents/<name>/connectors
{ "type": "mcp", "config": { "name": "vault", "url": "https://…/mcp" },
  "secrets": { "value": "<token>" } }

# call it from your handler
def handle(task):
    r = task.mcp("vault").call_tool("read_secret", key="db")
    return task.reply(str(r))

Plain REST APIs bind the same way with an http connector: pin a base URL, store the auth token in KMS, and agent code calls task.http("github").get("/zen") — the request is origin-pinned and the credential is injected server-side.

Streaming: call task.emit("partial…") to append output chunks; readers poll GET …/tasks/<id>?since=<seq> for them before the final reply.


06 — Dashboard

Try it and watch it

Every agent in the dashboard has an Interact tab: type a message, and the reply streams back as the agent emits it. It runs on the ordinary task pipeline — rate limits, usage metering and logs all apply — using your deploy token, so a private agent is testable without minting a caller token, and you're never charged for calling your own agent.

The Logs tab tails the agent's own log stream. Anything the handler prints to stdout or stderr shows up there, prefixed agent[out] / agent[err], alongside the runtime's start/duration lines — so print() is a working debugger.

The dashboard is push-driven: it holds one WebSocket to the platform and the server pushes change events (tasks, deployments, connectors) as they happen — no reloads, no polling. If the socket drops, it falls back to a coarse 30-second refresh until it reconnects.