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 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.
| Tool | What it does |
|---|---|
| grains_scaffold | Generate grains_app.py + grains.toml locally. |
| grains_deploy | Create the agent (if new) and deploy. Returns status + signed identity. |
| grains_invoke | Send a task to a deployed agent, return its reply. |
| grains_logs | Recent agent logs. |
| grains_list_agents | List your agents. |
| grains_agent_status | Inspect one agent. |
| grains_secret_set | Set an agent secret, e.g. OPENAI_API_KEY. Never echoed back. |
| grains_set_price | Make 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 anES256Ksignature. - Receipts verify offline, with no call back to
Grains required.
grains verify receipt.jsonchecks the signature against the payee's owndid: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.