Engram: shared memory for agents, paid per call in USDC
A few weeks ago I shipped extract.dkta.dev — a web content extraction API that bills USDC micropayments via x402. No accounts, no API keys. The wallet that pays is the identity. That post is here if you want the background on how x402 works.
The same model transfers further up the agent stack than I initially expected. Memory was the next obvious place to try it.
Every non-trivial agent needs to store and retrieve things across sessions. The standard answer is “give it a database” or “use a vector store,” but that means provisioning credentials, managing connection strings, and deciding upfront which agent gets access to what. For a single agent you own, that’s fine. For a mesh of agents from different frameworks, different users, different machines — it gets complicated fast.
Engram applies the same wallet-as-identity model to memory: pay per operation in USDC on Base, your wallet address is your identity, no signup required.
How it works
The API is five routes:
POST /memories Write a memory $0.001
GET /memories/search Full-text search $0.001
GET /memories/:id Read by ID $0.0001
PATCH /memories/:id Update a memory $0.001
DELETE /memories/:id Delete a memory $0.0001
Every request goes through x402 payment before it reaches the handler. The paymentMiddleware from x402-express intercepts the request, checks for a valid X-PAYMENT header, verifies the payment with Coinbase’s CDP facilitator, and either passes through or returns a 402 with the payment details.
The payer’s wallet address comes back from the facilitator as req.payment.from. That address is the user. Ownership, access control, and identity all resolve to the same thing.
const [memory] = await sql`
INSERT INTO memories (content, tags, visibility, owner_address)
VALUES (${content}, ${tags}, ${visibility}, ${req.payerAddress})
RETURNING *
`
Private memories are only visible to the wallet that wrote them. Public memories are searchable by anyone who pays for the search.
The Fastify shim
The stack is Fastify, not Express. x402-express is built around Express’s req.path, req.protocol, req.header(), res.status(), res.json(). Fastify’s raw req and res don’t have any of that.
The fix is a shim that runs in a Fastify onRequest hook:
fastify.addHook('onRequest', (req, reply, done) => {
if (FREE_PATHS.has(pathname)) return done()
shimExpressReq(req.raw, req.url) // attach .path, .protocol, .header()
shimExpressRes(reply.raw) // attach .status(), .json(), .setHeader()
middleware(req.raw, reply.raw, done)
})
One thing that bit me: Fastify’s CORS plugin doesn’t fire on 402 responses because x402 short-circuits before Fastify’s route handling. The 402 gets sent directly from the middleware, bypassing the plugin entirely. The fix is to inject CORS headers inside the shim’s res.json() implementation, before writeHead is called.
Watching it
Once the API was running, the obvious question was: is anyone using it? And more specifically — is there any money in that wallet?
Checking the USDC balance is an eth_call to the USDC contract on Base:
curl -s https://mainnet.base.org \
-X POST -H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","method":"eth_call",
"params":[{
"to":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"data":"0x70a08231000000000000000000000000<wallet_address>"
},"latest"],
"id":1
}'
Parse the hex result, divide by 1e6 (USDC has 6 decimals), and you have a dollar amount. I wired this into a daily cron that also checks the container health and request log, and posts a digest each morning:
engram.dkta.dev daily report
💰 Balance: $4.936464 USDC
📊 Requests (24h): 0 | All time: 0
🟢 Service: active
The zeroes are honest. Nobody’s used it yet. But the instrumentation is in place.
Try it
Live at engram.dkta.dev. The repo is public, OpenAPI spec at /openapi.json, and /llms.txt for agent discovery.
If you’re building agents that need persistent memory across sessions and you don’t want to manage another auth layer, this might be worth a look.