# @veezee/sdk

Typed REST client and CLI for the [Veezee API](https://veezee.io): LinkedIn, Reddit, and X (formerly Twitter) data for AI agents, metered in credits with per-call receipts. Zero runtime dependencies, built on Node 20's global `fetch`. Current version: 0.6.1.

The key spends; signing in adds a management token for key management. `vz init` mints a key with no signup: that key alone is enough to call every data endpoint. `vz login` is a separate, optional step that proves an email and stores a `vzm_` management token for listing, creating, revoking, and rotating keys, and for setting a low-balance alert. The first sign-in that confirms an email claimed on a trial key also adds a one-time 10,000-credit grant (once per email, once per key), spent after the free daily budget.

## Install

```
npm install @veezee/sdk
```

Node 20+ required.

## Quickstart

Start with one command:

```
npx @veezee/sdk init
```

`vz init` (same command, shorter once installed) POSTs to `/v1/keys/mint` with no signup and no card, and writes the returned key to `~/.veezee/config` (mode 0600). It never prints the raw key, only a masked hint: the key lives in a file and an env var, not in your terminal history or an agent's chat transcript. After that, every `vz` command and every SDK call in the same environment just works:

```ts
import { VeezeeClient } from "@veezee/sdk";

const client = new VeezeeClient();
await client.mint(); // reuses the key from `vz init`, or mints one the first time
const profile = await client.linkedin.getProfile({ identifier: "williamhgates" });
console.log(profile.common.full_name, profile.common.headline);
```

`mint()` is idempotent: it reuses `VEEZEE_API_KEY` or `~/.veezee/config` if either already has a key, and only mints a new one when neither does. The trial key itself carries no balance; the free tier is a per-IP, per-day allowance (200 credits/day, recent data, first page only) checked against the calling IP, not the key. Claiming the key adds a free balance on top: attach an email, confirm it by signing in at [veezee.io/login](https://veezee.io/login), and the account gets a one-time 10,000-credit grant (once per email, once per key) that spends at the same trial limits after each day's free allowance is used. Outgrow both and a call fails with `TRIAL_CAP_EXCEEDED` carrying `upgrade_url`: buy credits at [veezee.io/upgrade](https://veezee.io/upgrade) (no login required) and they land on the same key you already have. Nothing to reconfigure: same key, same config file, same code, now with a real balance.

You can skip `vz init` and pass a key directly instead:

```ts
const client = new VeezeeClient({ apiKey: process.env.VEEZEE_API_KEY });
```

## Auth

Every call sends `Authorization: Bearer <apiKey>`. A call with no resolvable key throws `VeezeeError` with `code: "KEY_REQUIRED"` and a `mint_url`: POST there (no auth, no body) to mint a key, or just run `vz init` or call `client.mint()`, then retry. `get_usage` always needs a key too.

## The API surface

Platform tools live under `client.linkedin`, `client.reddit`, and `client.x`; account/billing tools (platform-independent) are top-level:

```ts
await client.linkedin.getProfile({ identifier: "williamhgates", sections: ["experience", "education"] });
// -> PersonEnvelope { common: { full_name, headline, experience, ... } }

await client.linkedin.searchPeople({ keywords: "CTO", current_company: "anthropic", limit: 20 });
// -> PeopleSearchEnvelope { common: { results, cursor, total_matches } }

await client.linkedin.getCompany({ identifier: "microsoft" });
// -> CompanyEnvelope { common: { name, industry, employee_count, ... } }

await client.linkedin.getPosts({ identifier: "microsoft" });
// -> PostsEnvelope { common: { results, cursor, author_type } }

await client.reddit.search({ query: "notion alternative", type: "comments" });
// -> RedditSearchEnvelope { common: { type: "comments", comments, cursor, returned_count } }

await client.reddit.getSubredditPosts({ subreddit_name: "selfhosted", sort: "new" });
// -> RedditPostsEnvelope { common: { results, cursor, returned_count } }

await client.x.getProfile({ identifier: "nasa" });
// -> XProfileEnvelope { common: { screen_name, name, followers, description, ... } }

await client.x.search({ query: "veezee api", type: "recent" });
// -> XSearchEnvelope { common: { type: "recent", tweets, cursor, returned_count } }

await client.resolveUrl({ url: "https://www.linkedin.com/in/williamhgates" });
// -> UrlResolutionEnvelope { common: { type: "person", id, handle, canonical_url } }
// client.reddit.resolveUrl and client.x.resolveUrl work the same way for their own URLs.

await client.getUsage();
// -> UsageEnvelope { common: { plan, platforms, balance_remaining, recent_receipts, upgrade_url, manage_url } }

await client.checkout({ pack: "flex" | "production" });
// -> { checkout_url }. Needs no key: paying with none mints a fresh account and its key.
```

Every response is an **envelope**: `{ entity, platform, canonical_url, data_as_of, schema_version, common, platform_fields, freshness, usage }`. `usage.credits_charged` and `usage.receipt_id` are on every call; `freshness` tells you how old the data is. Free-tier trial-key calls also carry `usage.free_tier_hint`, explaining what budget the call drew from and that paying upgrades this same key.

## Error handling

Non-2xx responses throw a typed `VeezeeError` (extends `Error`) carrying the API's problem+json body:

```ts
import { VeezeeError } from "@veezee/sdk";

try {
  await client.linkedin.getProfile({ identifier: "someone" });
} catch (err) {
  if (err instanceof VeezeeError) {
    console.error(err.code, err.message); // message is a next-turn instruction
    if (err.is_retriable) {
      // safe to retry as-is; the client already retried transient errors internally
    }
  }
}
```

The client retries `RATE_LIMITED`, `CONCURRENCY_LIMIT`, `UPSTREAM_UNAVAILABLE`, `INTERNAL`, and network/timeout failures up to 3 attempts with exponential backoff, honoring `retry_after_seconds` when present. Every metered call gets one `Idempotency-Key` (UUID), reused across its own retries: retries never double-charge. Terminal errors (bad input, insufficient credits, auth) throw immediately without retrying.

Errors a payment can fix also carry `upgrade_url` (give it to your human), `credits_required` (on `INSUFFICIENT_CREDITS`: the credits the call needed), and `offer` (typed `OfferV1`): pack prices, `checkout_url` (the same link as `upgrade_url`), and `resume`, which says exactly how to retry after payment.

A call made with no resolvable key throws `code: "KEY_REQUIRED"` carrying `mint_url`: run `vz init` or call `client.mint()`, then retry. A management call (`listKeys`, `createKey`, `revokeKey`, `rotateKey`, `setBalanceAlert`) made with no resolvable management token throws `code: "AUTH_REQUIRED"`: run `vz login`, then retry.

## CLI

The package ships a `veezee` bin, with `vz` as a short alias for the same binary (noun-verb, mirrors the methods above):

```
veezee init [--force]
veezee linkedin profile get <identifier> [--sections a,b] [--realtime]
veezee linkedin search [--keywords x] [--title x] [--company x] [--past-company x] [--school x] [--first-name x] [--last-name x] [--limit n] [--cursor x] [--freshness x]
veezee linkedin company get <identifier>
veezee linkedin posts get <identifier>
veezee reddit search <query> [--type posts|comments|subreddits|users] [--sort x] [--range x]
veezee reddit subreddit <name> [--include-settings]
veezee reddit subreddit-posts <name> [--sort x] [--range x] [--include-promoted]
veezee reddit user <username> [--sections comments,posts,subreddits]
veezee reddit post <id1,id2,...> [--detail concise|full] [--comment-id x]
veezee x search <query> [--type recent|popular|people]
veezee x profile <identifier> [--by screen_name|id]
veezee x tweets <identifier> [--mode posts|posts_and_replies|highlights] [--no-retweets]
veezee x tweet <tweet_id>
veezee resolve-url <url>
veezee usage
veezee login [--device] [--no-browser]
veezee logout
veezee keys list [--account <id>]
veezee keys create [--label <name>] [--account <id>]
veezee keys revoke <key_id> [--account <id>]
veezee keys rotate <key_id> [--account <id>]
veezee alert <threshold|off> [--account <id>]
```

Run `vz init` first (once): it mints a free trial key and writes it to `~/.veezee/config`, masked hint only ever printed to stdout, never the raw key. `vz init --json` prints the same result as JSON (still no raw key) for scripting. `vz init --force` re-mints and overwrites the stored key.

For example, `vz reddit search "notion alternative" --type comments` or `vz x profile nasa`. Every platform namespace also has its own `resolve-url`: `vz reddit resolve-url <url>`, `vz x resolve-url <url>`.

Add `--json` on any command for machine-readable output (the raw envelope; errors print the full error shape as JSON too). Every data command takes `--max-credits <n>` to cap spend on that call: if the quote exceeds the cap the API returns a typed error and charges nothing. Unknown flags are rejected with the valid-flag list (exit 2), never silently ignored. Auth resolves in order: `--key <apiKey>`, then the `VEEZEE_API_KEY` env var, then `~/.veezee/config` from `vz init`. `--base-url` overrides the API host. Exit codes: `0` success, `1` failure, `2` usage or invalid-input error, `4` auth required. `vz --version` prints the SDK version. Run `vz --help` (or no args) for examples.

`vz login` is a separate, optional step: it proves an email (browser callback by default, or `--device` for a headless code-entry flow) and stores a `vzm_` management token in `~/.veezee/config`, masked hint only ever printed to stdout. `vz keys list|create|revoke|rotate` and `vz alert <threshold|off>` use that token, resolved in order: `--key <vzm_...>`, then `VEEZEE_MGMT_TOKEN`, then the stored token. `vz keys create` and `vz keys rotate` print the raw `api_key` to stdout once (the "shown once" warning goes to stderr): store it immediately, it cannot be retrieved again. `vz logout` clears the locally stored token; it does not revoke it server-side.

## Agent skills

The package ships eight installable SKILL.md packs under `skills/`: prospect-enrich, candidate-sourcing, company-enrichment, account-research, post-voice-research, reddit-monitoring, launch-sentiment-sweep, and outreach-list-builder. After `npm install @veezee/sdk`, add them to your agent with the [skills CLI](https://skills.sh) local-path syntax:

```
npx skills add ./node_modules/@veezee/sdk/skills
```

The same packs also install straight from GitHub ([veezeehq/veezee-skills](https://github.com/veezeehq/veezee-skills)): `npx skills add veezeehq/veezee-skills`. Pack index and docs: [veezee.io/docs/skills](https://veezee.io/docs/skills).

## Development

```
npm install
npm run build   # tsc -> dist/
npm test        # compiles test/ and runs node --test
npm pack --dry-run
```

CI should run exactly this sequence (`npm ci && npm run build && npm test`) on Node 20+; this package has no separate lint step and no network calls in its test suite.

## Docs

Full API reference: https://veezee.io/docs. SDK page: https://veezee.io/docs/sdk. CLI page: https://veezee.io/docs/cli. Hosted MCP servers (same tools, no install): https://veezee.io/docs/clients, listed in the [MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=io.veezee) as `io.veezee/linkedin`, `io.veezee/reddit`, and `io.veezee/x-twitter`.
