adam-id
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@adam-idsearch my archive for posts about productivity and draft a thread in my voice"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
adam-id
A private vault over your X (Twitter) archive, exposed to an AI agent through MCP so it can draft posts in your voice, grounded in what you actually wrote and how it actually performed.
Your archive never leaves your machine. Everything is encrypted at rest under a key you hold, the ciphertext store is the record of truth, and every tool call the agent makes is logged.
Quick start
npm install
npm run vault -- ingest # parse the archive into a normalized corpus
npm run vault -- index # build the SQLite FTS5 search index
npm run vault -- doctor # check what's wired up
npm run smoke # drive the MCP server end-to-endThen register it with Claude Code using config/mcp-config.json and ask it to draft something. Optionally encrypt the corpus for durable storage:
npm run vault -- seal --skip-media # encrypt the text corpus, anchor a Merkle root
npm run vault -- verify # prove the store is intact and decryptableThe first seal generates a 32-byte key and stores it in your macOS Keychain
(adam-id-vault / master-kek). Back that up — without it the store cannot be opened. On a
headless machine set VAULT_PASSPHRASE instead.
Sealing media (seal without --skip-media) writes a second, encrypted copy of every file, so
it needs ~2.2 GB free on top of the archive; seal checks up front and refuses rather than
failing halfway. This machine currently has ~2.1 GB free, so only the text corpus has been
sealed here. Nothing depends on media being sealed: get_media reads from the archive either
way. Media sealing exists for eventual upload to MCSP, where a local second copy isn't the point.
Related MCP server: X Archive Daemon
What's in the archive
Measured from this export (2026-08-02), not estimated:
count | count | ||
posts | 8,270 | likes | 11,411 |
— replies | 5,968 | followers | 2,422 |
— originals | 946 | following | 910 |
— retweets | 856 | DM messages | 35,251 |
— quotes | 351 | media files | 2,940 (2.13 GB) |
— self-replies | 149 | multi-post threads | 87 |
posts spans tweets.js, community-tweet.js and deleted-tweets.js. 342 posts had their
truncated text replaced with the full body from note-tweet.js.
What the archive does not contain
These limits are structural, not implementation gaps, and the agent is told about them:
No impressions, views, reply counts, bookmarks or profile clicks. X archives contain none of it — there is no bookmarks file at all. Performance analysis is likes and retweets only. The X Analytics CSV export is the only source for reach, and could be joined on tweet id later.
Engagement is recorded for ~67% of posts, and a missing count is indistinguishable from a true zero. Aggregates therefore compute over posts with nonzero recorded engagement and report
coveragealongside every statistic, rather than treating absence as zero.No follower history, so engagement can't be normalized by audience size. Percentiles are computed within
(year, kind)buckets instead — the strongest honest comparison available.Likes have no timestamp or author, only text.
The social graph is numeric account ids with no usernames. Handles for ~30% of accounts were recovered by harvesting mentions from elsewhere in the archive.
Architecture
twitter-*/ read-only X archive
│ ingest generic YTD parser driven by manifest.js
corpus/*.ndjson canonical records + threads, percentiles, media links
│ index SQLite FTS5 (BM25) ── the queryable working set
│ seal AES-256-GCM envelope per object + Merkle root
store/objects/ ciphertext ──[StorageAdapter]──→ MCSP (not provisioned)
──[AnchorAdapter]───→ Moca testnet
│ rebuild ← restores corpus + index from ciphertext alone
mcp serve scoped, audited tools ← Claude Code / Animoca MindThe search index is a derived cache. vault rebuild reconstructs the corpus and index from
sealed objects plus your key, which is what makes the ciphertext store the record of truth
rather than a backup. This is verified in practice: deleting corpus/ and rebuilding produces
a byte-identical fingerprint across search, style and engagement queries.
You cannot search ciphertext. MCSP holds encrypted blobs; the index is local and decrypted. That is stated plainly here because the alternative reading — searchable encryption — is not what this does.
MCP tools
Each tool is gated by one scope. A tool whose scope isn't granted is never registered, so the agent cannot see or call it — a stronger guarantee than refusing at call time.
tool | scope |
| tweets.read |
| tweets.read |
| tweets.read |
| tweets.read |
| tweets.read |
| tweets.read |
| analytics.read |
| analytics.read |
| likes.read |
| graph.read |
| media.read |
| dms.read |
VAULT_SCOPES controls the grant. dms.read is excluded by default because DMs contain other
people's words; turning them on should be deliberate. Every call, and every denial, appends to
audit.log.
Search note: bare terms match as prefixes, because handles tokenize as one word — an exact
search for songjam misses all ~1,300 posts mentioning @SongjamSpace. Quote a term for an
exact match ("songjam" → 208 hits vs 1,265).
Encryption
Envelope encryption: each object gets a fresh random data key (DEK) under AES-256-GCM, and the
DEK is wrapped under a key-encryption key (KEK) held in the macOS Keychain (or derived from
VAULT_PASSPHRASE).
The indirection earns its keep twice. A per-object DEK means one random nonce per key, avoiding GCM nonce reuse. And rotating the KEK only rewraps the small DEK blobs — migrating to a Moca CAK-derived key will not re-encrypt the 2 GB of media.
vault verify runs three independent checks, because each catches a different failure:
every object still matches its recorded ciphertext digest (corruption, partial writes)
the Merkle root recomputed from disk matches the manifest and the anchor (additions and deletions, which per-object digests alone would miss)
a sample of objects actually decrypts and matches its plaintext digest (a wrong or rotated key, which hashing alone would not catch)
Anchoring publishes one Merkle root rather than 2,940 per-object proofs — leaves are sorted by key so the root is order-independent, and leaf/node hashes use distinct domain prefixes to prevent second-preimage confusion.
Verified in practice: flipping a single byte in one ciphertext blob is caught by all three checks independently, and an interrupted write is reported as a specific inconsistency rather than crashing the verifier.
Moca integration status
piece | state |
Moca testnet RPC | working — chainId 223400, verified live |
Merkle-root anchoring | implemented — needs a funded |
Agent keypair ( | implemented — |
Credential verification | working — src/identity/credential.ts, 22 checks in |
Credential-gated HTTP transport | working — |
Per-Mind grant table | working — |
SD-JWT-VC issuance | working — |
Issuer-side revocation | working — |
Dashboard AIR credential | working — |
Cloudflare Tunnel exposure | working — |
Mind playbook | written — |
CAK-derived KEK | blocked — Credential Services pending activation |
MCSP storage | blocked — no client exists to call |
Gating the vault behind a Moca credential
A remote agent — an Animoca Mind — can query the vault over HTTP only if it presents a valid credential and has been granted access locally. Both gates must agree, and the effective scopes are their intersection: an over-broad credential cannot exceed the local grant, and a generous grant does nothing without a valid credential.
vault grant --mind <mind-id> --scopes tweets.read --days 1
vault serve --http --port 8787 # loopback only; tunnel to it deliberately
vault revoke --mind <mind-id> # instant, offline, tears down live sessionsWhy the credential is an SD-JWT-VC and not an AIR credential proper. It cannot be
one. An AIR-issued VC is encrypted to the holder's public key and stored in DStorage;
no issuance endpoint ever returns it, and verifying one means AirService.verifyCredential
— a browser iframe flow requiring the holder's live session and their consent to
decrypt. A headless Mind can neither retrieve, decrypt, nor present that. So what a
Mind actually sends is an SD-JWT-VC signed by the same whitelisted AIR partner key,
which this server verifies with jose against the issuer's published JWKS. That
credential's vct is self-asserted rather than registered in the AIR dashboard: it is
cryptographically real, but AIR's own verifier programs will not consume it. A parallel
dashboard-issued VC can serve as the network-recognised record of a grant; it plays no
part in per-request authorization.
The credential is a bearer token, and that is the weak point. The issuer's cnf
key binding is a TODO, and a Mind has no secret store — the credential transits the
conversation transcript and is persisted to long-term memory. Anyone who reads that
transcript has the granted access until expiry. Challenge-response with the agent key
cannot fix this, because the Mind cannot hold a private key: signing with a key that
lives in a transcript is signing with a public secret. The mitigations are blast-radius
reduction, not prevention — short expiry, instant local revocation, minimal scopes
(vault grant refuses dms.read without an explicit override), loopback binding with
a tunnel in front, and every call attributed in audit.log. Putting a second,
independent secret at the tunnel (a Cloudflare Access service token) is strongly
advised: a public URL guarded only by a token designed to appear in an LLM transcript
is not a security boundary.
Failures are distinguishable on purpose — credential_expired means re-issue,
credential_issuer means something is impersonating your issuer, grant_missing means
the credential is fine but you never granted it. Revocation checks fail closed, so
an unreachable issuer denies access rather than assuming validity.
Two revocation paths, and they are not equivalent. vault revoke is local: it needs
nothing to be reachable, takes effect immediately, and tears down sessions already open.
Revoking at the issuer (POST /admin/revoke-sd-jwt) is the durable record, but a
successful check is cached for 60s (VAULT_REVOCATION_TTL_MS), so it takes up to that
long to stop a new session from a credential just used, and it does not touch live
sessions at all. Reach for vault revoke when it matters; use issuer revocation to make
it permanent.
Issuing a credential
With air-issuer-service running (Postgres up, migrations applied):
curl -X POST http://127.0.0.1:3000/admin/issue-sd-jwt \
-H 'content-type: application/json' -H "x-admin-api-key: $ADMIN_API_KEY" \
-d '{"schemaId":"adam-id-access-v1","holderDID":"did:air:...",
"mindId":"<mind-id>","scopes":["tweets.read"],"label":"Adam (Hello Minds)"}'Returns {credential, credentialId, nonce, expiresAt}. Unlike /issue-vc, this hands the
credential back rather than encrypting it to the holder and pushing it to DStorage —
necessary because the holder here is headless and could never decrypt it.
Verify the whole loop against a live issuer with npm run smoke:issuer.
The two credentials, and why both exist
runtime credential | dashboard credential | |
format | SD-JWT-VC, | iden3 |
where it lives | presented as a bearer header | encrypted in Moca DStorage |
who verifies it | this vault, server-side with | AIR verifier widget, Program |
gate access? | yes | no |
revocable by us? | yes, | see caveat below |
A Mind can only present the first. The second is network-recognised provenance that a
grant was made, issued with npm run issue-air-credential; nothing in the request path
reads it.
Caveat on the dashboard credential's revocation. Its credentialStatus.id points at
https://issuer.staging.air3.com/credential-status/<nonce> — derived from ISSUER_ORIGIN,
which must stay as Moca's host for partner auth to work. So the status URL embedded in that
credential is not ours to serve, and revoking it locally does not change what an AIR
verifier sees. Treat it as a record of issuance, not a control surface. Access is revoked by
vault revoke and by revoking the SD-JWT.
Two constraints worth knowing, both verified against @mocanetwork/airkit@1.10.0:
AIR Kit is browser-only.
AirServicedrivesHTMLIFrameElement, so it cannot be imported into the Node MCP server. Anything needing it lives in web/companion.html; anchoring usesviemwith a local key instead.AIR Kit ships no storage API. Grepping its
distformcsp|dstorage|storageProvider|uploadBlob|putObjectreturns nothing. src/storage/mcsp.ts therefore implements the interface and fails loudly listing what's missing, rather than shipping a guessed REST contract that would silently not work.
When Credential Services activate, issueCredential returns cakPublicKey; that becomes the
KEK and rewrapAll migrates the store without re-encrypting anything.
Commands
vault ingest parse the archive into corpus/
vault index build the FTS5 index
vault seal [--skip-media] encrypt into store/, anchor the root
[--storage local|mcsp] [--anchor local|moca-testnet]
vault verify [--spot-checks N] digests + Merkle root + real decryption
vault anchor [--anchor ...] publish the current root
vault rebuild restore corpus + index from ciphertext
vault agent-key create/show the agent keypair
vault serve [--http] [--port N] MCP server over stdio, or credential-gated HTTP
vault doctor status of archive, corpus, key, storage, anchor
vault grants who may reach the vault remotely, and how far
vault grant --mind <id> --scopes a,b [--label L] [--days N] [--note "..."]
vault revoke --mind <id> cut off a Mind immediatelyTests:
npm run smoke stdio MCP end-to-end
npm run smoke:credential every way a credential can be wrong
npm run smoke:http the credential-gated HTTP path, offline
npm run smoke:all all threePrivacy
.gitignore excludes the archive, corpus/, store/, *.db, audit.log and .env. The
archive contains your email address, phone number, contact book, IP audit log and DMs — none of
it should reach git. Check with git status before committing.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Guarded agent surface for X & LinkedIn: discover, score, and draft in your voice. No key can post.
X (formerly Twitter) posts, profiles, and search for AI agents. Free key, self-minted, no signup.
Twitter (X) API alternative for AI agents: tweet search, profiles, followers. $0.0002 per result.
Twitter/X, Instagram, Reddit & TikTok data for AI agents. Billions of posts. No API keys.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables LLM agents to interact with Twitter (X) for searching tweets, posting content with images, analyzing engagement, and extracting topics using the Twitter API.
- FlicenseNot gradedqualityDmaintenanceArchives posts from X into a local SQLite database and provides tools for semantic analysis and natural language search. It enables agents to manage low-cost data ingestion and retrieve relevant posts using local embeddings and topic labeling.3
- AlicenseBqualityAmaintenanceEnables AI agents to search, read user profiles, timelines, media, follow threads, track trends, and manage accounts on X/Twitter via GraphQL, without browser automation or paid API keys.10010MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to fully automate X/Twitter interactions including searching, posting, engaging, and managing multiple accounts without paid API keys.MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/minds-monster/adam-id'
If you have feedback or need assistance with the MCP directory API, please join our Discord server