SDK
There is no Stitch client library to install. Read the registry directly with viem, or point an agent framework at the hosted MCP endpoint. Both paths hit the same contract, so they agree by construction.
Two ways in#
Pick by where your agent runs. If you are writing TypeScript and want exact control over reads, payloads, and purchases, use viem against the deployed registry. If you are running CrewAI or ElizaOS, wire in https://stitch-ai.net/api/mcp and the framework handles the calls.
npm install viempip install crewai "crewai-tools[mcp]"npm install @elizaos/plugin-mcpThe two paths do not cover exactly the same ground:
| Task | Contract function | MCP tool |
|---|---|---|
| Browse the catalogue | listPacks(offset, limit) | stitch_list_packs |
| Inspect one pack | getPack(packId) | stitch_get_pack |
| Check who may read it | hasAccess(packId, account) | not exposed |
| Replay the version history | versionHistory(packId) | stitch_get_pack (inline) |
| Find a creator's packs | packsByCreator(creator) | not exposed |
| Find a pack by keyword | not on chain | stitch_search_memory |
TypeScript and viem#
These snippets assume the stitch.ts module from the quickstart, which exports client, REGISTRY, registryAbi, and robinhoodChain.
Listing the catalogue#
// catalogue.ts
import { formatEther } from "viem";
import { client, REGISTRY, registryAbi } from "./stitch";
const [page, total] = await client.readContract({
address: REGISTRY,
abi: registryAbi,
functionName: "listPacks",
args: [0n, 12n],
});
// listPacks returns newest first, so id = total - offset - index
const packs = page.map((pack, index) => ({
id: Number(total) - index,
name: pack.name,
category: pack.category,
priceEth: formatEther(pack.price),
version: pack.version,
}));
console.table(packs);Against the live registry the first page prints, trimmed to four rows:
┌─────────┬────┬─────────────────────────────────────┬─────────────┬──────────┬─────────┐
│ (index) │ id │ name │ category │ priceEth │ version │
├─────────┼────┼─────────────────────────────────────┼─────────────┼──────────┼─────────┤
│ 0 │ 28 │ 'Structuring a Memory Pack' │ 'agents' │ '0' │ 1 │
│ 1 │ 27 │ 'Reading Chainlink Feeds Safely' │ 'oracles' │ '0.005' │ 1 │
│ 2 │ 26 │ 'Rate Limits and Provider Failover' │ 'developer' │ '0.003' │ 1 │
│ 3 │ 25 │ 'Evaluating Agent Output Quality' │ 'agents' │ '0.005' │ 1 │
└─────────┴────┴─────────────────────────────────────┴─────────────┴──────────┴─────────┘The payload shape#
A pack URI resolves to one JSON document. Every pack is currently at version 2 and served from Cloudflare R2, so the URI on chain is an ordinary HTTPS URL you can fetch directly.
type PackPayload = {
packId: number;
version: number;
name: string;
category: string;
summary: string;
updatedAt: string;
entries: { title: string; body: string; tags: string[] }[];
};Pack 1, trimmed to its first entry:
{
"packId": 1,
"version": 2,
"name": "Robinhood Chain RWA Primitives",
"category": "onchain",
"summary": "How real-world assets are represented and priced on Robinhood Chain, and what an agent must check before acting on them.",
"updatedAt": "2026-08-31T04:17:19.164Z",
"entries": [
{
"title": "Stock Tokens are debt securities, not equity",
"body": "Robinhood Stock Tokens are tokenized debt securities issued by Robinhood Assets (Jersey) Limited. They track the price of the underlying share but grant no legal or beneficial ownership ...",
"tags": ["rwa", "compliance"]
}
]
}Loading a pack into an agent#
Check the access grant before you fetch the payload. The registry is the authority on who may read a pack, and the URI will serve anyone who asks.
// load-memory.ts
import type { Address } from "viem";
import { client, REGISTRY, registryAbi } from "./stitch";
type PackPayload = {
packId: number;
version: number;
name: string;
summary: string;
entries: { title: string; body: string; tags: string[] }[];
};
export async function loadPackMemory(packId: bigint, account: Address): Promise<PackPayload> {
const [pack, granted] = await Promise.all([
client.readContract({ address: REGISTRY, abi: registryAbi, functionName: "getPack", args: [packId] }),
client.readContract({ address: REGISTRY, abi: registryAbi, functionName: "hasAccess", args: [packId, account] }),
]);
if (!granted) throw new Error("no access grant for pack " + packId);
if (!pack.active) throw new Error("pack " + packId + " is inactive");
const res = await fetch(pack.uri);
if (!res.ok) throw new Error("pack payload unavailable: " + res.status);
return (await res.json()) as PackPayload;
}Then put the memory in the context before the agent takes its first step, not after it has already guessed:
// agent.ts
import { loadPackMemory } from "./load-memory";
const pack = await loadPackMemory(6n, account);
const memory = [pack.summary, ...pack.entries.map((e) => e.title + "\n" + e.body)].join("\n\n");
const messages = [
{ role: "system", content: "You are a treasury agent operating on Robinhood Chain." },
{ role: "system", content: "Memory pack: " + pack.name + " v" + pack.version + "\n\n" + memory },
{ role: "user", content: task },
];Buying access#
Purchases are ordinary transactions on Robinhood Chain, signed by your own wallet. The MCP server never signs anything, so this step always belongs to you. Send exactly the listed price: anything else reverts with WrongPayment. Free packs still need a purchase call, with a value of zero, because that is what writes the access grant.
// purchase.ts
import { createWalletClient, custom, formatEther } from "viem";
import { client, REGISTRY, registryAbi, robinhoodChain } from "./stitch";
const wallet = createWalletClient({ chain: robinhoodChain, transport: custom(window.ethereum) });
const [account] = await wallet.requestAddresses();
const packId = 6n;
const pack = await client.readContract({
address: REGISTRY,
abi: registryAbi,
functionName: "getPack",
args: [packId],
});
const hash = await wallet.writeContract({
account,
address: REGISTRY,
abi: registryAbi,
functionName: "purchase",
args: [packId],
value: pack.price,
});
await client.waitForTransactionReceipt({ hash });
console.log("access granted to", pack.name, "for", formatEther(pack.price));CrewAI#
MCPServerAdapter from crewai-tools connects to the hosted endpoint over streamable HTTP and turns the three Stitch tools into CrewAI tools. Nothing runs on your side beyond the adapter.
# crew.py
from crewai import Agent, Crew, Task
from crewai_tools import MCPServerAdapter
server_params = {
"url": "https://stitch-ai.net/api/mcp",
"transport": "streamable-http",
}
with MCPServerAdapter(server_params) as tools:
analyst = Agent(
role="Onchain risk analyst",
goal="Size positions on Robinhood Chain without breaching the risk budget",
backstory="Pulls the guardrails out of Stitch before making a call.",
tools=tools,
verbose=True,
)
review = Task(
description=(
"Call stitch_search_memory for the agentic trading risk guardrails, "
"then review the open stock token positions and flag every breach."
),
expected_output="One line per breach, naming the rule it violates.",
agent=analyst,
)
print(Crew(agents=[analyst], tasks=[review]).kickoff())The adapter takes an optional allowlist, which is the cleanest way to stop a crew from browsing the whole catalogue when it should only read what it already owns:
# Only expose the tools this crew should have.
with MCPServerAdapter(server_params, "stitch_search_memory", "stitch_get_pack") as tools:
...ElizaOS#
ElizaOS talks to MCP through @elizaos/plugin-mcp. Add the plugin to the character and declare the Stitch endpoint as a streamable-http server. The agent then has stitch_search_memory, stitch_get_pack, and stitch_list_packs available on every turn.
{
"name": "Atlas",
"plugins": ["@elizaos/plugin-mcp"],
"settings": {
"mcp": {
"servers": {
"stitch": {
"type": "streamable-http",
"url": "https://stitch-ai.net/api/mcp"
}
}
}
}
}Caching and rate limits#
Pack metadata changes rarely and payloads are immutable per version, so almost every read can come from a cache. Two rules keep it correct:
- Cache metadata by pack id on a short timer, a few seconds; this site uses five.
- Cache payloads by pack id plus version, with no expiry. A new version means a new key, so a stale payload can never be served.
// cache.ts
const TTL_MS = 15_000;
const packCache = new Map<string, { at: number; value: unknown }>();
export async function cached<T>(key: string, loader: () => Promise<T>): Promise<T> {
const hit = packCache.get(key);
if (hit && Date.now() - hit.at < TTL_MS) return hit.value as T;
const value = await loader();
packCache.set(key, { at: Date.now(), value });
return value;
}
// Payloads are immutable per version, so key them by id and version, not by time.
const payloadKey = (id: number, version: number) => "pack:" + id + ":v" + version;The public RPC is rate limited per IP, and the MCP endpoint allows 120 requests per minute per IP. If you are polling harder than that, run your own RPC endpoint and cache locally. For the full list of functions and revert reasons behind these calls, see the contract reference.