pmarca-kv-storage
v1.0.0
Published
Use Twitter DMs as a persistent key-value store for AI agents. Store data in anyone's inbox.
Downloads
100
Readme
twitter-kv-store
Use Twitter DMs as a persistent key-value store for AI agents.
Store your agent's memory, state, config, and data in anyone's DM inbox. pmarca stores your grocery list. You get free distributed storage.
agent.set("tasks:pending", JSON.stringify(todo_list))
// → DMs @pmarca: "KV:SET:tasks:pending:W3sidGFzayI6..."What This Is
A key-value database where every write is a Twitter DM and every read replays the DM history. The conversation thread IS the database — append-only, chronologically ordered, globally accessible from any device with your Twitter credentials.
Why this is useful for agents:
- Zero infrastructure — no Redis, no Postgres, no S3
- Persistent across sessions by default
- Readable by humans (your DM thread shows agent state)
- Works anywhere that can hit the Twitter API
- The billionaire reads your agent's internal state as his inbox
Quick Start
npm install
cp .env.example .env
# fill in your Twitter API credentials
npm run dev# CLI
npm run cli -- set my_key "hello world"
npm run cli -- get my_key
npm run cli -- list
npm run cli -- dump# HTTP
curl -X PUT http://localhost:3001/v1/kv/my_key \
-H "Content-Type: application/json" \
-d '{"value": "hello world"}'
curl http://localhost:3001/v1/kv/my_keyUse as a Library
import { TwitterTransport, TwitterKV } from 'twitter-kv-store';
const transport = new TwitterTransport({
appKey: process.env.TWITTER_APP_KEY!,
appSecret: process.env.TWITTER_APP_SECRET!,
accessToken: process.env.TWITTER_ACCESS_TOKEN!,
accessSecret: process.env.TWITTER_ACCESS_SECRET!,
targetUserId: '23375688', // pmarca
});
const kv = new TwitterKV(transport);
await kv.set('agent:memory:last_task', 'analyze $NVDA earnings');
const task = await kv.get('agent:memory:last_task');
const allKeys = await kv.listKeys('agent:memory:');Use With AI Agents (OpenAI Function Calling)
Fetch tool definitions and inject into your agent:
curl http://localhost:3001/toolsReturns:
{
"tools": [
{ "type": "function", "function": { "name": "kv_set", ... } },
{ "type": "function", "function": { "name": "kv_get", ... } },
{ "type": "function", "function": { "name": "kv_delete", ... } },
{ "type": "function", "function": { "name": "kv_list", ... } },
{ "type": "function", "function": { "name": "kv_dump", ... } }
]
}Execute tool calls:
curl -X POST http://localhost:3001/tools/call \
-H "Content-Type: application/json" \
-d '{"name": "kv_set", "arguments": {"key": "agent:state", "value": "running"}}'Plug into Claude:
import anthropic, requests
tools = requests.get("http://localhost:3001/tools").json()["tools"]
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Remember that I prefer dark mode"}]
)
# Agent calls kv_set("user:preferences:theme", "dark")
# → DMs @pmarca: "KV:SET:user:preferences:theme:ZGFyaw=="Storage Protocol
Every KV operation is encoded as a plain-text DM:
| Operation | DM Message Format |
|-----------|------------------|
| SET | KV:SET:{key}:{base64(value)} |
| DEL | KV:DEL:{key} |
On read, the full DM history is fetched and replayed chronologically. Last write wins. DEL is a tombstone.
Example DM thread (your side)
KV:SET:grocery_list:LSBlZ2dzCi0gc2t5ciBZb2d1cnQKLSByYXZpb2xp
KV:SET:api_token:c2VjcmV0X3Rva2Vu
KV:SET:agent:task:YW5hbHl6ZSAkTlZEQQ==
KV:DEL:grocery_list
KV:SET:agent:status:cnVubmluZw==The other person's inbox:
- eggs
- skyr yoghurt
- ravioli ← they see your grocery list decoded, or they don't, who caresKey Design
- Keys are namespaced with colons:
agent:memory:fact_1 - Values are base64-encoded UTF-8 — store anything: JSON, binary-safe strings, numbers
- Max value size: ~7,400 bytes (Twitter DM limit 10,000 chars minus key + overhead)
- For larger values: chunk them with keys like
bigdata:chunk:0,bigdata:chunk:1
API Reference
REST
GET /health Health check
GET /v1/kv List all keys (?prefix=agent:)
GET /v1/kv/:key Get value
PUT /v1/kv/:key Set value { "value": "..." }
DELETE /v1/kv/:key Delete key
POST /v1/kv/sync Force sync from DM history
GET /v1/kv/dump Full database dump
GET /tools OpenAI tool definitions
POST /tools/call Execute tool callCLI
npm run cli -- set <key> <value>
npm run cli -- get <key>
npm run cli -- del <key>
npm run cli -- list [prefix]
npm run cli -- dump
npm run cli -- syncTwitter API Setup
- Go to developer.twitter.com
- Create an App with OAuth 1.0a enabled
- Set permissions to Read and Write and Direct Messages
- Generate Access Token + Secret
- Copy to
.env:
TWITTER_APP_KEY=...
TWITTER_APP_SECRET=...
TWITTER_ACCESS_TOKEN=...
TWITTER_ACCESS_SECRET=...
TARGET_USER_ID=23375688 # pmarca's user ID
PORT=3001Required scopes: dm.read, dm.write, users.read
Target user ID lookup:
# Get user ID from username
curl "https://api.twitter.com/2/users/by/username/pmarca" \
-H "Authorization: Bearer $BEARER_TOKEN"
# → {"data":{"id":"23375688","name":"Marc Andreessen","username":"pmarca"}}Rate Limits
| Tier | DMs/day | DM history | |------|---------|-----------| | Free | 500 | last 30 days | | Basic ($100/mo) | 1,000 | last 30 days | | Pro ($5000/mo) | 35,000 | full history |
For agent use (read-heavy, write-light): Free tier is fine for most workloads.
Cache strategy: Local in-memory cache syncs from DM history every 30s (configurable). Reads are fast; writes always flush to DM.
Limits & Edge Cases
- Max value size: ~7,400 bytes. Chunk large values manually.
- DM history depth: Twitter returns last ~200 events. Old writes are lost after ~200 ops. For long-lived agents: periodic dump + compact (write a
KV:SNAPSHOT:entry). - Concurrent writes: Last write wins. No locking. Single-agent use is fine; multi-agent requires namespacing.
- Target user blocking: If target blocks you, writes fail. Use an alt account you control as the target.
- Public visibility: DMs are private. The content is not visible to anyone except sender and recipient.
Architecture
Your Agent
│
│ kv.set("key", "value")
▼
TwitterKV (store.ts)
│ encode → KV:SET:key:base64val
▼
TwitterTransport (twitter.ts)
│ POST /1.1/direct_messages/events/new
▼
Twitter API
│
▼
Target User's DM Inbox ← pmarca sees: "KV:SET:grocery_list:..."
│
│ (on kv.get)
▼
GET /1.1/direct_messages/events/list
│ replay all KV:SET/KV:DEL ops in order
▼
In-memory cache → return valueInspiration
"If you ever want to get a billionaire's attention — make their DMs your grocery list" — @michaelbeer01
Using Marc Andreessen's DM inbox as a database is peak zero-infrastructure architecture.
License
MIT. The Twitter ToS is your problem, not ours.
