# xmark — full documentation for AI agents chat with your X (Twitter) bookmarks. xmark pulls your saved tweets, embeds them with OpenAI, and answers questions in plain english using Claude — with citations linking back to the original tweet. website: https://www.xmark.dev api: https://api.xmark.dev docs: https://www.xmark.dev/docs llms.txt: https://api.xmark.dev/llms.txt openapi: https://api.xmark.dev/openapi.json status: https://www.xmark.dev/api/health ## what xmark is - a personal search-and-chat layer over your X bookmarks - semantic retrieval (pgvector + OpenAI text-embedding-3-small) over the full bookmark contents - Claude (claude-sonnet-4-5) answers grounded in your bookmarks with [N] citations - a developer platform with a REST API at api.xmark.dev, scoped API keys, and an MCP server (@jclvsh/xmark) ## what xmark is not - not a bookmark manager (read-only mirror of X bookmarks; remove unbookmarks on X too) - not an X analytics dashboard (no engagement insights or follower metrics) - not a public search engine (bookmarks are private and RLS-gated per user) ## quick start 1. sign in with Google at /auth/signup 2. connect your X account on /bookmarks (separate OAuth flow for bookmark read access) 3. click sync — xmark pulls all bookmarks and embeds new ones 4. open /chat and ask questions in plain english 5. responses cite each referenced bookmark by [N] — click the number to open the original tweet ## authentication flows xmark uses two distinct OAuth flows: 1. **Google OAuth (sign-in)** — creates the xmark account itself. Supabase Auth handles the session. 2. **X (Twitter) OAuth 2.0 with PKCE (bookmark access)** — separate flow that grants xmark read access to your bookmarks. The X token is encrypted (AES-256-GCM) and stored in connected_accounts. Tokens auto-refresh; rotated refresh tokens are saved automatically. for the developer API at api.xmark.dev, use API keys (Authorization: Bearer nt_live_*). create keys at /settings/api-keys. native clients (the xmark iOS app) instead sign in as the user and send the Supabase access JWT as the bearer token. ## pricing free tier: 20 chat messages/month, 1 sync/day, full feature access (quotas return 402/429 on overage). pro: $5/month (or $50/year) — unlimited chat and sync. the same plan covers the CLI, MCP server, and developer REST API — there is no separate API tier. ## use cases xmark works well for: 1. **research assistant** — "what did I save about [topic]?" → semantic search returns top tweets with citations. 2. **knowledge-base assembly** — extract bookmarks into structured notes (Obsidian, Logseq, Notion) by topic or time window. 3. **briefing prep** — "summarize what I saved this month" → chat endpoint with month filter. 4. **citation extraction** — "find the tweet that backed claim X" → search + cite the original tweet URL. 5. **personal-history retrieval** — "that thread about Y from a few months ago" → semantic search beats keyword recall. xmark is NOT a good fit for: bulk timeline scraping, public-tweet search (use Twitter API directly), real-time monitoring (bookmarks sync on-demand). ## MCP server xmark publishes an MCP (Model Context Protocol) server for use inside Claude Code, Cursor, or any MCP-compatible client. ``` # install npx -y -p @jclvsh/xmark xmark-mcp # or run the CLI npx -y -p @jclvsh/xmark xmark ``` set `XMARK_API_KEY` (issued from /settings/api-keys) before running. tools exposed: - `sync_bookmarks` — trigger a fresh sync from X - `search_bookmarks` — semantic search query - `chat` — ask a natural-language question with citations - `list_bookmarks` — paginated bookmark list - `get_bookmark` — fetch a single bookmark - `delete_bookmark` — remove a bookmark (local-only, or also on X with unbookmark) - `list_conversations` — past chat threads - `get_conversation_messages` — a conversation's message history - `delete_conversation` — permanently delete a conversation - `get_usage` — plan + quota usage ## how xmark compares to alternatives - **native X bookmarks UI** — chronological list, keyword-only. xmark adds semantic search, chat, citations, and a developer API. - **Pocket / Reader** — read-it-later for articles. xmark is X-bookmarks-only with embedding-based search and chat. - **Readwise** — highlights from books/PDFs/articles. xmark covers a different surface (your X bookmarks) and adds chat. - **Glasp** — social highlighting on the web. xmark is private semantic search over your own X library. - **Composio / generic agent toolkits** — broad multi-platform tools. xmark is a single-surface specialist: bookmarks + chat. ## FAQ ### getting started Q: what is xmark? A: xmark lets you chat with your X (Twitter) bookmarks. you sign in, connect your X account, sync your bookmarks, and ask questions in plain english. xmark uses semantic search over the full bookmark contents (text, quoted tweets, linked articles, image alt text) to answer. Q: how do I sign up? A: go to /auth/signup and sign in with Google. once you're in, head to /bookmarks and connect your X account via the 'connect' button. xmark uses two separate OAuth flows: Google for the xmark account itself, X for read access to your bookmarks. Q: do I need an X (Twitter) account? A: yes. xmark reads your X bookmarks via the X API, so you need an X account with bookmarks already saved. if you've never bookmarked anything on X, there's nothing for xmark to chat with. Q: what plan do I need? A: xmark is a single $5/month plan. no free tier, no upgrades. one price covers unlimited bookmark sync, unlimited chat messages, and access to all features. ### bookmarks sync Q: how do I sync my bookmarks? A: open /bookmarks and click 'sync'. xmark pulls your bookmarks from X via the X API and embeds new ones with OpenAI's text-embedding-3-small for semantic search. the first sync may take a minute if you have hundreds of bookmarks; subsequent syncs only process new ones. Q: is sync automatic? A: no. sync is manual — you click the button when you want fresh bookmarks. this keeps API usage predictable and avoids hitting X's rate limits unnecessarily. Q: what gets synced? A: the full tweet text (including long-form note tweets), author info, engagement metrics, quoted tweets, image alt text, linked article metadata (title + description), and topic annotations. all of this becomes searchable in chat. Q: X says my account 'needs to be reconnected' — what now? A: X's OAuth tokens expire and rotate. when xmark can't refresh your token, you'll see a 'reconnect' prompt on /bookmarks or /settings. click reconnect to re-authorize. xmark detects rotated refresh tokens and saves them automatically going forward. Q: can I delete a bookmark? A: yes. clicking the remove icon on a bookmark unbookmarks it on X AND removes it from xmark in one step. both must succeed — leaving X bookmarked while removing locally would let the next sync re-create it. ### chat Q: how does chat work? A: you ask a question, xmark embeds it with OpenAI, retrieves the most relevant bookmarks via pgvector cosine similarity, then asks Claude (claude-sonnet-4-5) to answer using those bookmarks as context. responses cite each bookmark by number — [1], [2] — so you can click through to the original tweet. Q: how many bookmarks does chat consider? A: chat retrieves up to 800 of your most relevant bookmarks per query, ranked by semantic similarity to your question. if you have more, the lowest-ranked are excluded for that specific query, not deleted. Q: why is chat citing the same tweet multiple times? A: Claude is told to cite every bookmark it references with a number. if a single bookmark is the best match for several parts of an answer, you'll see the same number repeated. clicking any instance of [N] opens the original tweet. Q: do you train models on my bookmarks? A: no. your bookmarks are sent to OpenAI for embeddings (one-shot, not retained per OpenAI's API terms) and to Anthropic for chat answers (one-shot, not retained per Anthropic's API terms). xmark stores embeddings + bookmark text in your private Supabase row, gated by row-level security. ### privacy and data Q: where is my data stored? A: in xmark's Supabase Postgres database, encrypted at rest. row-level security policies ensure only your authenticated session can read your bookmarks and conversations. nobody else — no admin, no other user — can see your data. Q: what happens to my X access token? A: X tokens are encrypted with AES-256-GCM before being written to the database. only the running app can decrypt them; database backups don't expose plaintext tokens. Q: how do I delete my account? A: go to /settings and click 'delete account'. xmark soft-deletes your profile + bookmarks + conversations and hard-deletes your active credentials (X OAuth tokens, API keys). signing back in within the retention window restores your account; after that, the soft-deleted rows are purged. ### billing Q: what does the $5/month plan include? A: unlimited bookmark sync, unlimited chat messages, full access to every feature. the plan is the only paid tier — there are no upsells or per-message overages. Q: how do I cancel? A: go to /settings → billing and click 'manage subscription'. you'll be redirected to Stripe's customer portal where you can cancel at the end of the current period. cancellation stops the next charge; you keep access until the period ends. Q: do you offer refunds? A: if you cancel within the first 7 days of a new subscription, email josh@jclvsh.art and we'll refund your most recent payment. after that, cancellations stop future charges but the current period isn't refunded. ### troubleshooting Q: sync is stuck or won't start A: xmark uses a Redis lock to prevent concurrent syncs for the same account. if a previous sync hung, the lock auto-expires after 30 seconds — wait, then click sync again. if it still won't start, your X token may have expired (see the reconnect prompt). Q: chat says 'subscription required' but I just paid A: Stripe webhooks usually arrive within seconds of checkout, but rarely take a minute. refresh /chat after 60 seconds; if you still see the message, contact support with your Stripe email and we'll reconcile the subscription manually. Q: I'm getting rate-limited by X A: xmark uses X's pay-per-use API tier with strict rate limits. if you hammer the sync button or run many parallel sessions, X may briefly 429 you. wait a few minutes and try again. xmark treats 429 as 'X is healthy, just busy' — it's not a permanent error. --- # developer API reference ## base URL ``` https://api.xmark.dev ``` legacy host `https://xmark.dev/api/v1/*` still responds for backwards compatibility, but `api.xmark.dev` is canonical. ## authentication all requests require a bearer token: ``` Authorization: Bearer nt_live_YOUR_API_KEY ``` create API keys at https://www.xmark.dev/settings/api-keys. native clients signed in via Supabase may instead send the user's access JWT as the bearer token — it authenticates as the account owner with all scopes. ## scopes API keys carry scoped permissions. grant the minimum needed. | scope | description | |-------|-------------| | read | read-only access to owned resources | | write | create, update, and delete owned resources (soft-deletes treated as writes) | | generate | AI/ML or other expensive compute operations | | send | outbound communication (email, DM, post, webhook delivery) | | admin | manage API keys and webhook endpoints | default scopes (if none specified): `read`, `write`, `generate`, `send`. legacy `delete` scope still authenticates for grandfathered keys against routes requiring `write` (delete folded into write 2026-05-07). ## endpoints ### bookmarks read, search, and sync the authenticated user's X bookmarks. #### GET /bookmarks paginated list of the caller's bookmarks, newest first **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "tweet_id": "1234567890", "text": "full tweet text, including long-form note tweets", "author": { "id": "string | absent", "username": "jclvsh", "name": "josh", "profile_image_url": "https://... | null" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/jclvsh/status/1234567890", "public_metrics": { "like_count": 42, "retweet_count": 7 }, "media": [ { } ] } ], "pagination": { "page": 1, "pageSize": 50, "total": 1234, "totalPages": 25 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 INTERNAL: internal server error #### GET /bookmarks/{id} fetch a single bookmark **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | bookmark id | **response (200):** ```json { "success": true, "data": { "id": "uuid", "tweet_id": "1234567890", "text": "full tweet text, including long-form note tweets", "author": { "id": "string | absent", "username": "jclvsh", "name": "josh", "profile_image_url": "https://... | null" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/jclvsh/status/1234567890", "public_metrics": { "like_count": 42, "retweet_count": 7 }, "media": [ { } ] } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 NOT_FOUND: resource not found - 500 INTERNAL: internal server error #### DELETE /bookmarks/{id} remove a bookmark. local-only by default (the next sync re-creates anything still bookmarked on X); pass unbookmark=true to also remove it on X **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | bookmark id | | unbookmark | boolean | no | also remove the bookmark on X via the stored OAuth token (default: false) | **response (200):** ```json { "success": true, "data": { "removed": true } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 NOT_FOUND: resource not found - 400 SERVER_002: x account needs to be reconnected (unbookmark=true only) - 500 INTERNAL: internal server error #### POST /bookmarks/search semantic search over the caller's bookmark library (pgvector + OpenAI embeddings) **scope:** `read` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | query | string (1-2000) | yes | natural-language search query | | limit | integer (1-50) | no | max results (default: 10) | **response (200):** ```json { "success": true, "data": [ { "bookmark": { "id": "uuid", "tweet_id": "1234567890", "text": "...", "author": { "username": "jclvsh", "name": "josh" }, "bookmarked_at": "ISO 8601 | null", "url": "https://x.com/..." }, "score": 0.83 } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 VALIDATION_FAILED: invalid json body - 500 INTERNAL: internal server error #### POST /bookmarks/sync pull the latest bookmarks from the connected X account, embed new ones, and store them **scope:** `write` **response (200):** ```json { "success": true, "data": { "count": 1234, "newCount": 12, "removedCount": 3, "durationMs": 8200 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 SERVER_002: x account needs to be reconnected - 409 SERVER_012: a sync is already in progress - 429 BILLING_006: free-tier daily sync limit reached (1/day) - 500 INTERNAL: internal server error ### chat ask natural-language questions about bookmarks; Claude answers with citations grounded in them. #### POST /chat submit a question. xmark retrieves the most relevant bookmarks via semantic search and has Claude generate an answer with inline citations. returns a single JSON response **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | question | string (1-8000) | yes | the question to ask | | conversationId | UUID | no | continue an existing conversation | | maxCitations | integer (1-20) | no | cap the returned citations (default: 5) | **response (200):** ```json { "success": true, "data": { "answer": "string", "citations": [ { "bookmark_id": "uuid", "tweet_url": "https://x.com/...", "author": "jclvsh", "snippet": "..." } ], "conversation_id": "uuid" } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 400 VALIDATION_FAILED: invalid json body - 402 BILLING_006: free-tier monthly chat limit reached (20/month) - 429 BILLING_006: free-tier concurrent-conversation limit reached - 500 INTERNAL: internal server error **notes:** - native clients use POST /chat/stream instead — the same request body, answered as a text/event-stream of JSON frames (meta, delta, error, done). streaming is outside the OpenAPI codegen contract ### conversations list, read, and delete past chat conversations. #### GET /conversations paginated list of the caller's conversations, most recently active first **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | pageSize | integer (1-100) | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "title": "string | null", "created_at": "ISO 8601", "updated_at": "ISO 8601" } ], "pagination": { "page": 1, "pageSize": 50, "total": 3, "totalPages": 1 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 INTERNAL: internal server error #### GET /conversations/{id}/messages the conversation's message history, oldest first. assistant messages carry the IDs of the bookmarks retrieved for that answer **scope:** `read` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | conversation id | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "role": "user", "content": "string", "retrieved_bookmark_ids": null, "created_at": "ISO 8601" }, { "id": "uuid", "role": "assistant", "content": "string", "retrieved_bookmark_ids": ["uuid"], "created_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 NOT_FOUND: resource not found - 500 INTERNAL: internal server error #### DELETE /conversations/{id} permanently delete a conversation and its messages **scope:** `write` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | conversation id | **response (200):** ```json { "success": true, "data": { "deleted": true } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 404 NOT_FOUND: resource not found - 500 INTERNAL: internal server error ### usage plan and quota usage for the authenticated user. #### GET /usage the caller's plan, chat-message usage this month, syncs today, bookmark count, and whether an X account is connected. limits are null on unlimited plans **scope:** `read` **response (200):** ```json { "success": true, "data": { "plan": "free", "x_connected": true, "bookmarks": { "total": 1234 }, "chat": { "used": 4, "limit": 20 }, "syncs_today": { "used": 1, "limit": 1 } } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key / expired session token - 500 INTERNAL: internal server error ## error codes all errors return: `{ success: false, error: { code, message, requestId } }` the `X-Request-ID` response header carries the same id for log correlation. ### authentication | code | HTTP | description | |------|------|-------------| | AUTH_001 | 401 | missing or invalid authorization header | | AUTH_002 | 403 | API key missing required scope | | AUTH_004 | 401 | invalid or revoked API key | ### billing | code | HTTP | description | |------|------|-------------| | BILLING_001 | 402 | no active subscription | | BILLING_006 | 402 | plan usage limit reached (402 on chat quota, 429 on sync rate) | | BILLING_007 | 402 | subscription required for this operation | | BILLING_008 | 403 | current plan does not include this feature | ### rate limiting | code | HTTP | description | |------|------|-------------| | RATE_001 | 429 | too many requests - back off and retry | ### validation & server | code | HTTP | description | |------|------|-------------| | SERVER_001 | 500 | internal server error | | SERVER_002 | 400 | bad request (malformed body or missing field) | | SERVER_003 | 404 | resource not found | | SERVER_004 | 405 | method not allowed | | SERVER_005 | 400 | validation failed | | SERVER_008 | 500 | database error | | SERVER_012 | 409 | conflict (e.g. concurrent modification) | ### webhooks | code | HTTP | description | |------|------|-------------| | HOOK_001 | 401 | invalid webhook signature | | HOOK_002 | 500 | webhook processing failed | | HOOK_004 | 408 | webhook delivery timed out |