# xskill API Reference

A product by Achronon.

xskill is a prepaid API for agents that need to read and understand X/Twitter
posts, threads, conversations, search results, and image media.

## Base URLs

- Production API: `https://api.xskill.md`
- Local development default: `http://127.0.0.1:3000`

Set these shell variables before using the examples:

```sh
export XSKILL_BASE_URL="${XSKILL_BASE_URL:-https://api.xskill.md}"
export XSKILL_API_KEY="xsk_replace_me"
export X_POST_ID="1234567890123456789"
```

## Authentication

Customer endpoints require an `xsk_` API key. Prefer the bearer form:

```http
Authorization: Bearer xsk_...
```

`x-api-key: xsk_...` is also accepted. Never send provider or parser secrets
such as `TWITTERAPI_IO_API_KEY`, `GETXAPI_API_KEY`, or `ANTHROPIC_API_KEY` to
xskill endpoints.

`GET /health`, `POST /v1/signup`, and `POST /v1/stripe/webhook` do not use
customer API-key auth. `GET /v1/ops/metrics` is an internal endpoint protected
by `XSK_OPS_TOKEN`; never use a customer `xsk_` key for ops access.

## Copy-Paste Quickstart

Check service health:

```sh
curl -sS "$XSKILL_BASE_URL/health"
```

Create a new account and one-time `xsk_` key:

```sh
curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"inviteCode":"replace-with-invite","name":"Research agent"}' \
  "$XSKILL_BASE_URL/v1/signup"
```

Read and summarize a conversation:

```sh
curl -sS \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  "$XSKILL_BASE_URL/v1/thread?id=$X_POST_ID&mode=conversation&parse=summary"
```

Read one post:

```sh
curl -sS \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  "$XSKILL_BASE_URL/v1/post?id=$X_POST_ID"
```

Search X:

```sh
curl -sS -G \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  --data-urlencode "query=from:OpenAI agent" \
  --data-urlencode "type=Latest" \
  "$XSKILL_BASE_URL/v1/search"
```

Create a `$10.00` top-up Checkout Session:

```sh
curl -sS \
  -X POST \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amountCents":1000}' \
  "$XSKILL_BASE_URL/v1/topups/checkout"
```

## Response Shape

Successful API responses return a `data` object. Billable read endpoints also
return `usage`.

```json
{
  "data": {},
  "usage": {
    "cost": {
      "currency": "USD",
      "estimatedUsd": 0.003,
      "itemsRead": 20,
      "unitCostUsd": 0.00015,
      "upstreamRequests": 1
    },
    "pricing": {
      "currency": "USD",
      "operation": "parsed_thread",
      "priceCardVersion": "2026-06-25.default",
      "priceMicroCredits": 20000,
      "priceUsd": 0.02,
      "units": {
        "tweets": 20
      }
    },
    "provider": "twitterapi.io",
    "tweetsRead": 20
  }
}
```

`usage.provider` is the active backend adapter for the call. It is normally
`twitterapi.io`, but may be `getxapi` during an operator failover. Customer
routes and price-card billing stay the same across that provider flip.

Errors use one envelope:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "statusCode": 400
  }
}
```

Rate-limited authenticated requests include `x-ratelimit-limit`,
`x-ratelimit-remaining`, `x-ratelimit-reset`, and, for rejected requests,
`retry-after`.

## Pricing

xskill stores balances as integer micro-credits:

- `1_000_000` micro-credits = `$1.00`
- `$0.02` = `20_000` micro-credits

Default price card:

| Operation | Price |
| --- | --- |
| Raw post | `4_000` micro-credits |
| Raw search | `300 * tweetsReturned` |
| Raw thread | `4_000 + 300 * tweetsRead` |
| Parsed thread, Haiku | `14_000 + 300 * tweetsRead` |
| Parsed thread, Sonnet | `44_000 + 300 * tweetsRead` |
| Vision image OCR/description | `20_000 * images` |

The default free tier tops each account balance up to `2_000_000`
micro-credits once per UTC month. Free credits are account-scoped, not
API-key-scoped, and unused free credits do not stack above the monthly cap.

Top-ups use Stripe Checkout. The minimum top-up is `$10.00`, which credits
`10_000_000` micro-credits after Stripe reports a paid Checkout Session.
Signup and checkout surfaces must link the [Terms of Service](./terms.md) and
[Pricing and Refund Policy](./refund-policy.md) before creating a Checkout
Session.

See [pricing.md](./pricing.md) for price-card overrides and reservation
behavior.

## Endpoints

### `GET /health`

Returns service health. No auth.

Example:

```sh
curl -sS "$XSKILL_BASE_URL/health"
```

Response:

```json
{
  "environment": "production",
  "ok": true,
  "service": "xskill-api"
}
```

### `POST /v1/signup`

Creates a new account and returns a one-time `xsk_` API key. Public signup is
invite-gated with `XSK_SIGNUP_INVITE_CODE`, invite reuse tracking, and IP
throttling via `XSK_SIGNUP_RATE_LIMIT_MAX_REQUESTS` /
`XSK_SIGNUP_RATE_LIMIT_WINDOW_MS`. Set `XSK_DATA_FILE` to persist account
metadata, API-key hashes, invite-use hashes, balances, credit-grant
idempotency, and usage records across single-replica restarts. Without
`XSK_DATA_FILE`, those stores are process-local. Direct deployments do not trust
proxy headers by default; hosted
deployments behind a proxy that strips inbound forwarding headers should set
`XSK_TRUST_PROXY=true` so throttling keys on the forwarded client IP instead of
the load balancer. The key is only returned in this response; store it before
leaving the page or terminal.

Request body:

| Name | Required | Values | Default |
| --- | --- | --- | --- |
| `inviteCode` | yes | active signup invite code | none |
| `name` | no | 1 to 80 characters | omitted |

Example:

```sh
curl -sS \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"inviteCode":"replace-with-invite","name":"Research agent"}' \
  "$XSKILL_BASE_URL/v1/signup"
```

Response:

```json
{
  "data": {
    "account": {
      "createdAt": "2026-06-26T00:00:00.000Z",
      "id": "acct_..."
    },
    "apiKey": {
      "accountId": "acct_...",
      "createdAt": "2026-06-26T00:00:00.000Z",
      "id": "key_...",
      "name": "Research agent",
      "prefix": "xsk_..."
    },
    "docs": {
      "api": "/api",
      "pricing": "/pricing",
      "refundPolicy": "/refund-policy",
      "skill": "/skill.md",
      "terms": "/terms"
    },
    "key": "xsk_...",
    "topUp": {
      "amountCents": 1000,
      "amountMicroCredits": 10000000,
      "endpoint": "/v1/topups/checkout"
    }
  }
}
```

The hosted landing page uses this endpoint, then calls
`POST /v1/topups/checkout` with the new key after showing the Terms of Service
and Pricing and Refund Policy links.

### `GET /v1/post`

Fetches one post by numeric X post ID.

Query parameters:

| Name | Required | Values | Default |
| --- | --- | --- | --- |
| `id` | yes | 1 to 25 digits | none |
| `parse` | no | `summary`, `json`, `tldr` | omitted |
| `model` | no | `haiku`, `sonnet` | `haiku` |
| `vision` | no | `true`, `false` | `false` |

Billing:

- No `parse`: `raw_post`.
- `parse=...&model=haiku`: `parsed_thread` for one tweet, with raw-post
  fallback reservation.
- `parse=...&model=sonnet`: `premium_parsed_thread` for one tweet. Requires
  premium models to be enabled.
- `vision=true`: separately bills `vision_image` for photo media analyzed.

Example:

```sh
curl -sS \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  "$XSKILL_BASE_URL/v1/post?id=$X_POST_ID&parse=tldr&vision=true"
```

Response fields:

- `data.post`: normalized post.
- `data.parsed`: present when `parse` is requested.
- `data.vision`: image OCR/description results when `vision=true`.
- `usage.tweetsRead`: upstream post reads.
- `usage.pricing`: customer price metadata.
- `usage.vision`: separate image billing metadata when applicable.

### `GET /v1/thread`

Fetches tweets in a conversation through the provider's `conversation_id`
search path. Use the root conversation ID when possible.

Query parameters:

| Name | Required | Values | Default |
| --- | --- | --- | --- |
| `id` | yes | 1 to 25 digits | none |
| `mode` | no | `conversation`, `thread` | `conversation` |
| `maxPages` | no | integer `>= 1` | provider default |
| `maxTweets` | no | integer `>= 1` | provider default |
| `parse` | no | `summary`, `json`, `tldr` | omitted |
| `model` | no | `haiku`, `sonnet` | `haiku` |
| `vision` | no | `true`, `false` | `false` |

`mode=conversation` returns all fetched conversation tweets. `mode=thread`
returns the root author's self-reply chain when root-author and reply metadata
are available; otherwise it falls back to the full conversation to avoid
guessing.

Billing:

- No `parse`: `raw_thread`.
- `parse=...&model=haiku`: `parsed_thread`.
- `parse=...&model=sonnet`: `premium_parsed_thread`. Requires premium models.
- `vision=true`: separately bills `vision_image` for photo media analyzed.

The twitterapi.io adapter caps thread fetches at 5 pages / 100 tweets.

Example:

```sh
curl -sS \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  "$XSKILL_BASE_URL/v1/thread?id=$X_POST_ID&mode=thread&parse=summary&maxPages=2"
```

Response fields:

- `data.id`: conversation ID.
- `data.mode`: resolved mode.
- `data.tweets`: normalized tweets.
- `data.truncated`: `true` when the provider reports additional pages.
- `data.parsed`: present when `parse` is requested.
- `data.vision` and `data.visionTruncated`: present when `vision=true`.
- `usage.tweetsRead`: upstream tweets read.
- `usage.tweetsReturned`: tweets returned after mode filtering.

### `GET /v1/search`

Runs provider-backed X advanced search.

Query parameters:

| Name | Required | Values | Default |
| --- | --- | --- | --- |
| `query` | yes | non-empty string, max 512 chars | none |
| `type` | no | `Latest`, `Top` | `Latest` |
| `cursor` | no | provider cursor | omitted |
| `maxPages` | no | integer `1..5` | `1` |
| `maxTweets` | no | integer `1..100` | `20` |

Billing: `raw_search`, settled to `300 * tweetsReturned`. The route reserves
the provider-estimated reachable result window before provider spend.

Example:

```sh
curl -sS -G \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  --data-urlencode "query=conversation_id:$X_POST_ID" \
  --data-urlencode "type=Latest" \
  --data-urlencode "maxTweets=20" \
  "$XSKILL_BASE_URL/v1/search"
```

Response fields:

- `data.query`: query sent to the provider.
- `data.type`: `Latest` or `Top`.
- `data.tweets`: normalized tweets.
- `data.pageInfo.nextCursor`: cursor for the next page when present.
- `usage.tweetsRead`: upstream tweets read.
- `usage.tweetsReturned`: tweets returned to the caller.

### `POST /v1/topups/checkout`

Creates a Stripe Checkout Session for prepaid credits.
Show or link the [Terms of Service](./terms.md) and
[Pricing and Refund Policy](./refund-policy.md) before creating a Checkout
Session.

Request body:

```json
{
  "amountCents": 1000
}
```

`amountCents` must be an integer of at least `1000`.

Example:

```sh
curl -sS \
  -X POST \
  -H "Authorization: Bearer $XSKILL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amountCents":1000}' \
  "$XSKILL_BASE_URL/v1/topups/checkout"
```

Response:

```json
{
  "data": {
    "amountCents": 1000,
    "amountMicroCredits": 10000000,
    "currency": "usd",
    "id": "cs_test_...",
    "url": "https://checkout.stripe.com/..."
  }
}
```

Credits are added only after Stripe sends a paid Checkout webhook.

### `POST /v1/stripe/webhook`

Stripe-only webhook endpoint. No customer API-key auth.

Headers:

```http
Stripe-Signature: ...
Content-Type: application/json
```

The route verifies the raw request body with `STRIPE_WEBHOOK_SECRET`. Paid
`checkout.session.completed` and `checkout.session.async_payment_succeeded`
events credit the account once using the Checkout Session ID as the idempotency
key.

### `GET /v1/ops/metrics`

Internal observability endpoint. Requires the configured ops token:

```sh
curl -sS \
  -H "Authorization: Bearer $XSK_OPS_TOKEN" \
  "$XSKILL_BASE_URL/v1/ops/metrics"
```

The response is a JSON dashboard with real-time in-process usage, cost, revenue,
margin, credit grants, recent per-call billing logs, and twitterapi.io prepaid
float alert status.

Response fields:

- `data.totals.revenueUsd`, `data.totals.costUsd`, and
  `data.totals.marginUsd`: revenue vs upstream cost vs margin.
- `data.byEndpoint` and `data.byOperation`: request, unit, revenue, cost, and
  margin breakdowns.
- `data.byProvider`: provider/parser cost breakdown. Parsed calls split
  twitterapi.io fetch cost from Anthropic parser cost.
- `data.recentUsage`: recent billable reservations/settlements.
- `data.float.status`: `ok`, `low`, or `unknown`.
- `data.alerts`: includes `twitterapi_io_float_low` when
  `XSK_TWITTERAPI_IO_FLOAT_BALANCE_USD` is below
  `XSK_TWITTERAPI_IO_FLOAT_ALERT_USD`.

## Error Codes

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | Request validation failed or JSON body is invalid. |
| 400 | `invalid_stripe_event` | Stripe webhook payload is not a valid xskill top-up event. |
| 400 | `invalid_stripe_signature` | Stripe signature is missing or invalid. |
| 401 | `unauthorized` | Missing, invalid, or revoked `xsk_` API key. |
| 402 | `insufficient_balance` | Prepaid balance is too low; top up to continue. |
| 403 | `invalid_signup_invite` | Signup invite code is invalid. |
| 403 | `premium_model_required` | `model=sonnet` was requested but premium models are disabled. |
| 404 | `post_not_found` | The requested post was not found. |
| 404 | `not_found` | Route does not exist. |
| 409 | `stripe_idempotency_conflict` | Stripe top-up idempotency conflict. |
| 413 | `invalid_request` | Request body is too large. |
| 413 | `cost_ceiling_exceeded` | Request would exceed the configured per-call cost ceiling. |
| 413 | `parse_budget_exceeded` | Parse input/output budget is too large. |
| 415 | `invalid_request` | Request media type is not supported. |
| 429 | `rate_limited` | Per-key or public signup rate limit exceeded. |
| 500 | `internal_error` | Unexpected server error. |
| 503 | `auth_unavailable` | API-key auth is not configured. |
| 503 | `billing_unavailable` | Credit ledger is not configured or did not return a usage id. |
| 503 | `ops_unavailable` | Ops metrics are missing or `XSK_OPS_TOKEN` is not configured. |
| 503 | `parser_provider_unavailable` | The configured parser provider rejected or failed the parse request. |
| 503 | `parser_unavailable` | Parse was requested but no parser is configured. |
| 503 | `pricing_unavailable` | Pricing engine is not configured. |
| 503 | `provider_unavailable` | Tweet provider is missing or unavailable. |
| 503 | `signup_unavailable` | Public API-key issuance is not configured. |
| 503 | `stripe_unavailable` | Stripe Checkout or webhook verification is not configured. |
| 503 | `vision_unavailable` | Vision was requested but no analyzer is configured or available. |

## Normalized Data Notes

Tweet objects can include:

- `id`, `text`, `url`, `createdAt`, `conversationId`, `inReplyToTweetId`
- author fields such as `id`, `username`, `name`, `verified`,
  `profileImageUrl`
- count fields such as `replyCount`, `retweetCount`, `likeCount`,
  `quoteCount`, `bookmarkCount`, `viewCount`
- `media[]` with `type`, `url`, `previewImageUrl`, `altText`, and optional
  `vision` OCR/description data

Provider and parser failures are sanitized; responses do not expose upstream
API keys, Anthropic keys, raw prompt internals, or customer API-key material.
