Introduction
Mainspring is an on-chain witness layer for autonomous agents. Every decision an agent makes — the intent, the inputs it weighed, the action it took — is sealed to Casper testnet as a permanent, independently verifiable attestation.
Where a log file can be rewritten, a Mainspring attestation cannot. It turns “trust me” into a receipt anyone — a counterparty, an auditor, a regulator — can verify without your cooperation.
- Transport:
MCP · stdio - Runtime:
Node.js 20+ / TypeScript - On-chain:
Casper mainnet / testnet - Encryption:
AES-256-GCM - Storage:
File JSONorSupabase - Hashing:
SHA-256 · canonical JSON
Installation
Mainspring runs as a local MCP server over stdio. The normal user path is the npm package: install the CLI, let it create local config/data/log folders, then point your MCP client at the mainspring command.
# 1. Install the MCP server CLI
npm install -g mrmainspring
# 2. Create local config and print the MCP snippet
mainspring setup claude
# or: mainspring setup cursor
Paste the printed JSON into your MCP client configuration. The same command works for Claude Desktop, Cursor, or any MCP-compatible host:
{
"mcpServers": {
"mainspring": {
"command": "mainspring"
}
}
}
Node.js 20+ and npm install -g mrmainspring put the mainspring command on PATH.
mainspring setup claude or mainspring setup cursor creates local files and prints the MCP config.
Restart the MCP client, then run mainspring doctor if the server does not appear.
# Check what Mainspring will use locally
mainspring doctor
# Optional advanced setup
mainspring init
mainspring config
No wallet, hosted account, or environment variable is required for local memory, Grimoire, audit, or payment preflight tools. Mainspring stores its default config under the user's app config folder and data under that local namespace.
For Casper anchoring during development, use the testnet — anchors are free and the contract behavior is identical to mainnet.
Quickstart
Once the MCP server is connected, your agent can call Mainspring tools directly. The local user path is: write a memory, get a deterministic receipt, then verify the hash. Casper submission stays pending unless you explicitly configure real testnet anchoring.
{
"agent_id": "atlas-treasury-02",
"type": "decision",
"source": { "tool": "price_oracle", "symbol": "CSPR" },
"body": {
"action": "TRANSFER 12400 CSPR",
"rationale": "Rebalance to 30% stable reserve after weekly inflow."
},
"anchor": true
}
{
"memory_id": "mem_4f2c8e1a-...",
"content_hash": "9f3c4a71e0b2...", // SHA-256 of full envelope
"metadata_hash": "b8e109d2fa31...", // SHA-256 of identity fields
"anchor_status": "pending", // local default until Casper is configured
"anchor_id": "c7f8a0e3..."
}
That receipt is a verifiable handle. Hand the content_hash to any counterparty; they can recompute it independently from the same envelope. With Casper testnet anchoring configured, the same hash-only proof can also be checked against the transaction record.
Overview
Mainspring is a single MCP server with four internal services. All communication with the agent happens over stdio — no HTTP server, no port bindings. The only outbound connection is the optional call to casper-client for on-chain anchoring.
The storage layer is swappable at startup. Both backends implement the same interface — switching from file to Supabase requires only an environment variable change, no code changes.
The Loop
Every decision runs the same clock. Mainspring is built like a watch — four movements that only mean something when they turn together.
- Act — your agent decides and acts on available data.
- Witness — Mainspring meters the decision at the source, hashing the full envelope before it can be altered.
- Anchor — the content hash and anchor id are submitted to Casper via
casper-client, creating an immutable on-chain record. - Prove — anyone can recompute the hash from the stored envelope and verify it against the chain, independently, without trusting Mainspring.
Attestations
An attestation is the sealed record of one decision. Mainspring stores the full envelope off-chain and anchors only its SHA-256 hash on Casper — nothing about the content ever reaches the blockchain.
Memory Envelope
Every memory written to Mainspring is serialized into a MemoryEnvelope before hashing. The schema is versioned and deterministic — the same inputs always produce the same hash.
Canonical JSON sorts all object keys alphabetically at every depth level and omits undefined values. This guarantees the same input always produces the same hash regardless of the serializer used.
The prev_anchor_hash field chains attestations together. When a memory is anchored, its anchor_id is stored as the previous hash for the agent's next anchored write — creating a verifiable chain of custody.
Witnesses
A witness is the MCP server running alongside your agent. It is the sole trusted observer — it receives the raw decision data, computes the hash before any transmission can alter it, and dispatches the anchor submission.
The witness is non-blocking. Anchoring runs asynchronously — anchor_status: "pending" is returned immediately; the status transitions to "anchored" once casper-client confirms the transaction. If the anchor call fails, the memory is still stored locally with anchor_status: "failed" — no data is lost.
The witness never touches body content for anchoring — it only sees the hash. Body data stays in local storage (file or Supabase) and never reaches Casper.
memory.write
Creates a new MemoryEnvelope, computes both hashes, persists the entry, and optionally submits an anchor to Casper. Every write is also recorded as a memory.created audit event.
| Field | Type | Req | Description |
|---|---|---|---|
| agent_id | string | ✓ | Identifies the writing agent. Used as a key in the anchor chain. |
| type | enum | ✓ | observation · decision · payment · secret_usage · system_event |
| source | object | ✓ | Contextual metadata about the memory origin (arbitrary JSON). |
| body | object | ✓ | The memory content (arbitrary JSON). Never sent on-chain. |
| anchor | boolean | — | If true, submit anchor to Casper immediately. Default: false. |
| Field | Type | Description |
|---|---|---|
| memory_id | string | Unique identifier — mem_<uuid> |
| content_hash | string | SHA-256 of the canonical full envelope (anchored on-chain) |
| metadata_hash | string | SHA-256 of identity fields only (agent_id, memory_id, type, source, created_at) |
| anchor_status | enum | not_requested · pending · anchored · failed |
| anchor_id | string? | Present when anchor_status ≠ not_requested |
memory.read · search · verify
Three read-path tools that cover retrieval, discovery, and integrity verification.
Reads one stored memory by agent_id + memory_id. Returns the full StoredMemoryEntry including both hashes and anchor state.
Full-text search over body, source, and type fields for a given agent_id. Takes a query string and optional limit (default 20). Returns ranked matches.
Recomputes the content_hash locally from the stored envelope and compares it to the stored value. Returns valid: true|false, both hashes, and whether they match. Detects any tampering with the stored data.
{
"valid": true,
"stored_hash": "9f3c4a71e0b2…",
"computed_hash": "9f3c4a71e0b2…",
"hashes_match": true,
"onchain_content_hash": "9f3c4a71e0b2…" // present if anchored
}
Grimoire
The Grimoire is Mainspring's encrypted vault for secrets and spending policies. It provides two sub-systems: secret storage (AES-256-GCM) and policy enforcement for the x402 payment flow.
Secrets
Secrets are encrypted with AES-256-GCM using a 12-byte random nonce. The plaintext is never returned by any MCP tool — not even in confirmations. If you lose the plaintext, it is gone.
| Field | Type | Description |
|---|---|---|
| name | string | Unique identifier for this secret. |
| value | string | The plaintext secret. Encrypted immediately; never stored in the clear. |
| type | enum | casper_private_key_ref · x402_client_key_ref · api_key · webhook_secret |
| scopes | string[] | Permitted operations: x402:sign · casper:sign |
grimoire.secret.list returns metadata only (name, type, scopes, created_at). The value field is never included in any response.
Policies
Policies gate x402 payment operations. Every payment.fetch call runs a policy check first — no payment proceeds without an active, matching policy.
{
"policy_id": "pol_cspr_transfers",
"enabled": true,
"allowed_urls": ["https://api.example.com/data"],
"allowed_methods": ["GET"],
"max_amount_per_call": "0.05",
"max_amount_per_period": "1.00",
"period_seconds": 86400
}
Policy denial reasons: policy_not_found · policy_disabled · url_not_allowed · method_not_allowed · amount_over_limit · invalid_amount. Each reason is audited.
Payments
Mainspring implements the x402 payment protocol — a three-stage state machine for policy-gated, challenge-based micropayments over HTTP. Each stage advances the same PaymentIntent record.
Not yet implemented: real Casper x402 signed payload, paid retry to the resource, and facilitator settlement verification. The DisabledX402SettlementProvider always returns settlement_unavailable. Both CASPER_ENABLE_REAL_SUBMISSION and X402_ENABLE_REAL_SETTLEMENT are explicit opt-in gates — they never default to true.
Use payment.receipt to read a payment intent at any stage — it returns the full record including status, denial reason, and challenge requirements.
Audit
audit.tail returns the most recent audit events across all services. Every tool call that writes state records an event automatically — no opt-in required.
{ "limit": 20 } // default: 20, max: 100
memory.createdmemory.verify_succeededmemory.verify_failedpayment.policy_approvedpayment.policy_deniedpayment.challenge_receivedpayment.settledsecret.storedpolicy.setAll audit metadata is sanitized before storage. Keys matching secret, token, key, password, private, payload, credential, authorization, or value are automatically redacted to [REDACTED]. Max depth: 6, max string: 2 048 chars, max total event size: 16 KB.
Casper Anchoring
Anchoring writes a compact record to the memory-anchor Rust/Wasm contract on Casper. Only hashes are submitted — never memory bodies, secrets, or private keys.
anchor_id derivation
anchor_id = SHA-256(
agent_id + ":" + memory_id + ":" + content_hash + ":" + prev_anchor_hash
)
When prev_anchor_hash is null (first anchor for an agent), the literal string "null" is used in the concatenation. This links successive anchors into a tamper-evident chain per agent.
Contract entry point
| Arg | Type | Description |
|---|---|---|
| anchor_id | String | Unique anchor identifier (hex-encoded SHA-256) |
| content_hash | String | SHA-256 of the full envelope (hex) |
| metadata_hash | String | SHA-256 of identity fields (hex) |
| agent_id | String | Agent identifier — stored alongside the anchor |
The contract rejects duplicate anchor_ids with error code DuplicateAnchor (0). All hash strings are validated against /^(hash-)?[a-f0-9]{64}$/i before being passed to the CLI — no unvalidated input reaches casper-client spawn().
Submission modes
transaction-package(default) — usesMEMORY_ANCHOR_PACKAGE_HASHdeploy-contract-hash— usesMEMORY_ANCHOR_CONTRACT_HASH
Deployed testnet contracts
MEMORY_ANCHOR_CONTRACT_HASH=hash-9a10301e…
MEMORY_ANCHOR_PACKAGE_HASH=hash-162da013…
CASPER_NETWORK_NAME=casper-test
CASPER_ENABLE_REAL_SUBMISSION=true # explicit opt-in required
Full hashes are in the repository README under Casper Contract Status. Block explorer: testnet.cspr.live.
Storage Backends
Both backends implement identical interfaces. The choice is made at startup via SIGIL_STORAGE_BACKEND — no code changes required to switch.
| · | File (default) | Supabase |
|---|---|---|
| Activate | SIGIL_STORAGE_BACKEND=file | SIGIL_STORAGE_BACKEND=supabase |
| Memory | .sigil/memory.json | sigil_memory_entries |
| Secrets | .sigil/grimoire-secrets.json | sigil_secrets |
| Policies | .sigil/grimoire-policies.json | sigil_policies |
| Payments | .sigil/payments.json | sigil_payment_intents |
| Audit | .sigil/audit.json | sigil_audit_events |
| Setup | None — auto-created | Apply backend/supabase/schema.sql |
| Best for | Development, demos, single-agent | Production, multi-agent, persistence |
Security Model
- Memory body content
- Secret plaintext
- Rationale text
- Signed x402 payloads
- Private keys
- Secret plaintext values
- AES-GCM auth tags
- Signed x402 payloads
- Raw nonces
- Audit event metadata
- Challenge requirements
- Policy denial details
CLI arguments passed to casper-client are validated against /^(hash-)?[a-f0-9]{64}$/i before the process is spawned. This prevents command injection via malformed hash strings in any stored data.
The grimoire master key is intentionally excluded from all storage backends, audit logs, and MCP responses. If GRIMOIRE_MASTER_KEY is absent at startup, Mainspring generates one automatically for local use; keep production secrets in a private env file.
Environment Reference
Normal npm users do not need to copy .env.example or create a repo-local .env. Run mainspring setup; it creates the local config, data, and logs directories automatically. The variables below are advanced overrides for Supabase, real Casper testnet anchoring, x402 settlement sidecars, or repository development.
Use mainspring doctor to see the active config path. If you need custom values, put them in the user config file created by mainspring init, or point SIGIL_ENV_FILE at a private env file. Copying .env.example is only for local development from this repository.
| Variable | Default | Description |
|---|---|---|
| SIGIL_ENV_FILE | (user config) | Optional path to a private env file. If unset, Mainspring checks the user's app config first, then repo-local env files for development. |
| SIGIL_DATA_DIR | (user data) | Root directory for file-based storage. Defaults to the user's Mr Mainspring app data directory. |
| SIGIL_STORAGE_BACKEND | file | file or supabase. |
| GRIMOIRE_MASTER_KEY | (auto) | Base64-encoded 32-byte AES-256-GCM master key. Generated automatically for local use when omitted. |
| PROJECT_URL | — | Supabase project URL (required when backend=supabase). |
| SECRET_KEY | — | Supabase service role key. |
| CASPER_RPC_URL | — | Casper node RPC endpoint for casper-client. |
| CASPER_ACCOUNT_KEY_PATH | — | Path to the account signing key PEM file. |
| CASPER_NETWORK_NAME | casper-test | casper-test or casper (mainnet). |
| CASPER_ENABLE_REAL_SUBMISSION | false | Explicit opt-in gate. Must be true to actually write to chain. |
| MEMORY_ANCHOR_CONTRACT_HASH | — | Deployed contract hash (hash-<64 hex chars>). |
| MEMORY_ANCHOR_PACKAGE_HASH | — | Deployed package hash. Used in transaction-package mode. |
| CASPER_ANCHOR_SUBMISSION_MODE | transaction-package | transaction-package or deploy-contract-hash. |
| X402_ENABLE_REAL_SETTLEMENT | false | Opt-in gate for real x402 settlement. Requires a configured signer/facilitator/resource path. |
| X402_SIGNER_URL | — | External signer sidecar URL. Required for real paid retries. |
| X402_FACILITATOR_URL | http://localhost:4022 | x402 facilitator endpoint for challenge verification and settlement tests. |
FAQ
No. Only a SHA-256 hash of the full envelope is anchored. Body content lives in your local storage (file or Supabase) and never leaves your infrastructure.
Yes. The hash is deterministic: sort the envelope keys alphabetically, serialize to compact JSON, run SHA-256. Any runtime can recompute and verify against what's on Casper — independently.
The memory is still stored locally with anchor_status: "failed". No data is lost. You can inspect the audit log for the error detail and retry manually.
Never. It lives only in your environment. If absent at startup, Mainspring generates one automatically for local use. Production secrets should live in a private env file.
Not yet. The three-stage flow — policy check, HTTP 402 challenge, settlement — is fully wired, but DisabledX402SettlementProvider always returns settlement_unavailable. This is intentional and honest; fake receipts are never returned.