# 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://xmark-sigma.vercel.app docs: https://xmark-sigma.vercel.app/docs llms.txt: https://xmark-sigma.vercel.app/llms.txt openapi: https://xmark-sigma.vercel.app/openapi.json status: https://xmark-sigma.vercel.app/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/v1, scoped API keys, and webhooks ## 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/v1, use API keys (Authorization: Bearer nt_live_*). create keys at /settings/api-keys. ## pricing $5/month single plan. unlimited bookmark sync, unlimited chat messages, full feature access. no free tier. paid features (chat, bookmark sync) require an active subscription or team-member role. ## 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: up to 800 bookmarks per query, ranked by semantic similarity to your question. that's the X API maximum; if you have more than 800 bookmarks 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://xmark-sigma.vercel.app/api/v1 ``` ## authentication all /api/v1 requests require a bearer token: ``` Authorization: Bearer nt_live_YOUR_API_KEY ``` create API keys at https://xmark-sigma.vercel.app/settings/api-keys. ## scopes API keys carry scoped permissions. grant the minimum needed. | scope | description | |-------|-------------| | read | read-only access to owned resources | | write | create and update owned resources | | delete | delete owned resources | | admin | manage API keys and webhook endpoints | default scopes (if none specified): `read`, `write`. ## endpoints ### projects create and list projects owned by the authenticated user. #### GET /projects list the caller's projects, newest first **scope:** `read` **response (200):** ```json { "data": [ { "id": "uuid", "name": "string", "description": "string | null", "created_at": "ISO 8601", "updated_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded #### POST /projects create a new project **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string (1-100) | yes | project name | | description | string | no | optional description | **response (201):** ```json { "data": { "id": "uuid", "name": "string", "description": "string | null", "created_at": "ISO 8601", "updated_at": "ISO 8601" } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 400 SERVER_002: invalid json body - 400 SERVER_005: validation failed - 500 SERVER_001: internal server error ### API keys manage API keys programmatically. key-management endpoints require the admin scope. #### GET /keys list the caller's API keys **scope:** `admin` **response (200):** ```json { "data": [ { "id": "uuid", "name": "string", "key_prefix": "nt_live_abc123...", "scopes": ["read", "write"], "last_used_at": "ISO 8601 | null", "revoked_at": "ISO 8601 | null", "created_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded #### POST /keys create a new API key. the raw key is returned exactly once **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string (1-100) | yes | human-friendly name | | scopes | ("read" | "write" | "delete" | "admin")[] | no | scopes to grant. defaults to ['read','write'] | **response (201):** ```json { "data": { "key": "nt_live_...raw_key_shown_once...", "api_key": { "id": "uuid", "name": "string", "key_prefix": "nt_live_abc123...", "scopes": ["read", "write"], "created_at": "ISO 8601" } } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 400 SERVER_002: invalid json body - 400 SERVER_005: validation failed - 500 SERVER_001: internal server error - 422 SERVER_002: maximum active api keys reached #### GET /keys/me introspect the key making this request. no scope required **response (200):** ```json { "data": { "id": "uuid", "name": "string", "key_prefix": "nt_live_abc123...", "scopes": ["read", "write"], "last_used_at": "ISO 8601 | null", "created_at": "ISO 8601" } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded #### GET /keys/{id} fetch a specific key owned by the caller **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | api key id | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found #### PATCH /keys/{id} rename an api key **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | api key id | **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string (1-100) | yes | new name | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 400 SERVER_002: invalid json body - 400 SERVER_005: validation failed - 500 SERVER_001: internal server error - 404 SERVER_003: resource not found #### DELETE /keys/{id} revoke an api key immediately **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | api key id | **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found #### POST /keys/{id}/rotate rotate an api key. revokes the old key and returns a new one with the same name + scopes. the usage counter is net-zero so rotation never trips the active-key limit **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | api key id | **response (200):** ```json { "data": { "key": "nt_live_...new_raw_key_shown_once...", "api_key": { "id": "uuid", "name": "string", "scopes": ["read", "write"], "created_at": "ISO 8601" } } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found **notes:** - callers must update stored credentials before the next request - the old key is invalidated immediately ### webhooks register HTTPS endpoints that receive signed HMAC-SHA256 payloads when events happen. all webhook-management endpoints require the admin scope. #### GET /webhooks list the caller's webhook endpoints **scope:** `admin` **response (200):** ```json { "data": [ { "id": "uuid", "url": "https://your-app.example/hook", "events": ["project.created"], "active": true, "created_at": "ISO 8601", "updated_at": "ISO 8601" } ] } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded #### POST /webhooks create a webhook endpoint. returns the signing secret exactly once **scope:** `admin` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | https URL | yes | destination url (must be https) | | events | string[] | yes | event types to subscribe to. use '*' to subscribe to all | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 400 SERVER_002: invalid json body - 400 SERVER_005: validation failed - 500 SERVER_001: internal server error #### GET /webhooks/{id} fetch a specific webhook **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | webhook id | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found #### PATCH /webhooks/{id} update a webhook's url, events, or active flag **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | webhook id | **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | https URL | no | new destination | | events | string[] | no | new event list | | active | boolean | no | enable or disable | **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 400 SERVER_002: invalid json body - 400 SERVER_005: validation failed - 500 SERVER_001: internal server error - 404 SERVER_003: resource not found #### DELETE /webhooks/{id} soft-delete a webhook. no new deliveries are attempted **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | webhook id | **response:** 204 no content **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found #### POST /webhooks/{id}/rotate-secret rotate the signing secret. the new secret is returned exactly once **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | webhook id | **response (200):** ```json { "data": { "id": "uuid", "secret": "hex-encoded-secret" } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found **notes:** - update your signature-verification logic before the next delivery - the old secret is invalidated immediately #### GET /webhooks/{id}/deliveries list recent delivery attempts for a webhook. useful for debugging failed deliveries **scope:** `admin` **query / path parameters:** | name | type | required | description | |------|------|----------|-------------| | id | UUID | yes | webhook id | | page | integer | no | page number (default: 1) | | page_size | integer (1-100) | no | items per page (default: 50) | **response (200):** ```json { "data": [ { "id": "uuid", "event_type": "project.created", "status": "delivered" | "failed" | "pending", "attempts": 1, "response_status": 200, "response_body": "string | null", "delivered_at": "ISO 8601 | null", "created_at": "ISO 8601" } ], "pagination": { "page": 1, "page_size": 50, "total": 0, "total_pages": 1 } } ``` **errors:** - 401 AUTH_001: missing or invalid authorization header - 401 AUTH_004: invalid or revoked API key - 403 AUTH_002: API key missing required scope - 429 RATE_001: rate limit exceeded - 404 SERVER_003: resource not found ## 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 | 429 | plan usage limit reached | | 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 |