{
  "markdown": "# TTCtl\n\n[![CI](https://github.com/alexey-pelykh/ttctl/actions/workflows/ci.yml/badge.svg)](https://github.com/alexey-pelykh/ttctl/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/ttctl?logo=npm)](https://www.npmjs.com/package/ttctl)\n[![npm downloads](https://img.shields.io/npm/dm/ttctl?logo=npm)](https://www.npmjs.com/package/ttctl)\n[![GitHub Repo stars](https://img.shields.io/github/stars/alexey-pelykh/ttctl?style=flat&logo=github)](https://github.com/alexey-pelykh/ttctl)\n[![License](https://img.shields.io/github/license/alexey-pelykh/ttctl)](LICENSE)\n[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-3.0-4baaaa.svg)](CODE_OF_CONDUCT.md)\n\nPersonal-productivity CLI and MCP server for your own [Toptal Talent](https://talent.toptal.com) profile.\n\n## ⚠️ Unofficial — Personal Use Only\n\n**TTCtl is NOT affiliated with, endorsed by, or supported by Toptal LLC.** It is a personal-productivity tool that interacts with Toptal Talent's web/mobile interfaces using your own session, exclusively for your own profile's data.\n\nThis tool is built for **fair use** and shared in case it's useful to other Toptal talents who want similar tooling for their own profiles. It is **NOT** to be used for:\n\n- Spam or unsolicited outreach to recruiters / clients\n- Mass automation of job applications, profile updates, or messaging\n- Engagement-signal manipulation, application bombing, recruiter scraping\n- Operating against profiles that aren't your own\n- Any behavior that violates [Toptal's Terms of Service](https://www.toptal.com/tos) or burdens the platform\n\nIf you encounter misuse of TTCtl, please see [Abuse reporting in SECURITY.md](SECURITY.md#abuse-reporting).\n\nArchitectural friction in the codebase (sequential rate limits, single-credential design, no batch parallelism on automation-prone endpoints) is intentional. Don't remove it.\n\n\"Toptal\" is a trademark of Toptal LLC. The name appears in this project's description as **nominative fair use** only — TTCtl is not a Toptal brand.\n\n## What It Does\n\nTTCtl gives you (and your AI assistants, via [MCP](https://modelcontextprotocol.io)) programmatic access to your own Toptal Talent profile:\n\n- **Profile** — view and update your talent profile (basic info, skills, employment, education, certifications, industries, portfolio, visas, resume, external links, reviews, photo)\n- **Applications** — review your activity items (applications, availability requests, interviews, engagement signals); per-status-group counts via `applications stats`; confirm / reject open Interest Requests via `applications confirm` / `applications reject`; fetch interview detail (interviewer with TopChat thread handle, client-side contact, scheduled slot, agenda link, prep-guide ref) via `applications interview show <id>`; read your interview prep notes via `applications interview notes show <jobId>`; fetch availability-request detail (status, kind, recruiter Fixed rate, comment, lifecycle timestamps, job) via `applications availability-request show <id>`\n- **Engagements** — view current and past engagements; manage engagement breaks; per-status counts via `engagements stats`\n- **Jobs** — browse opportunities (incl. the algorithmic `jobs recommended` feed, the per-job `jobs match-quality` match-score breakdown, and the per-job `jobs rate-insight` rate-intelligence panel); manage saved / viewed / not-interested signals; configure search subscription; direct-apply via `jobs apply <id> --consent` (legal-compliance gate is mandatory). Add `--suggest-answers` to fetch your own historical answers to similar prior questions as advisory autocomplete suggestions (opt-in, off the critical apply path; failures degrade gracefully). The MCP tool `ttctl_jobs_apply_similar_answers` exposes the same surface to agents.\n- **Timesheets** — list, view, update, and submit timesheet billing cycles\n- **Availability** — view and update working hours and allocated weekly hours\n- **Contracts** — view talent-level contracts (Toptal Direct, MSA, etc.)\n- **Payments** — view payout history, aggregate payment totals (`payments summary`), and payment methods; show current hourly rate via `payments rate current` (lightweight) or the unified `payments rate show` projection; submit rate-change requests\n- **Surveys** — list pending surveys, submit structured answers, and add free-text feedback on post-interview / NPS prompts; submissions and feedback are irreversible and consent-gated\n- **Auth** — bootstrap config, sign in, check status, sign out\n\nSurfaces are gated to read-heavy / personal use. Operations that would enable mass automation against the platform are deliberately not exposed.\n\n### Out of scope\n\nThe following Toptal Talent surfaces are out of scope **by design** — they enable abuse (mass-automation or spam vectors), require third-party SDKs TTCtl doesn't depend on, target client-onboarding rather than the talent-side workflow, belong to staff- or sourcing-side workflows, or are one-time / mobile-only UI affordances with no recurring CLI value:\n\n- **Notification preferences** — `UpdateJobAlertSettings`, `UpdateSmsNotificationsSettings`, `UpdateTimesheetReminderSettings`, `UpdateNotificationSetting` (abuse-prevention — silencing alerts is a precursor to other abuse)\n- **Community / Slack interaction** — events, channels, messages, RSVP (third-party-SDK — community participation is human-side, not CLI-side)\n- **Referral flow** — send referral, claim reward, track referrer (abuse-prevention — programmatic referral generation is a spam vector)\n- **Become-client / consultations / coaching / gigs** — buyer-side workflows (client-onboarding — not talent-side profile management)\n- **Hire-me-page publishing** — public marketing page (one-time-action — not recurring profile state)\n- **Video pitches / Zendesk integration** — Ziggeo/CameraTag- and Zendesk-backed surfaces (third-party-SDK — not part of the GraphQL surface TTCtl targets)\n- **TopChat messaging surface** — messages and file downloads remain out of scope; only the per-interviewer discovery handle is surfaced via `applications interview show` (full surface tracked in #23)\n- **Mobile-only widgets** — Story, MobileTopTip, MarketCondition (mobile-only-UI — no CLI equivalent value)\n- **Quizzes / questionnaires** — onboarding-terminal forms (one-time-action — not recurring profile state)\n- **Staff-side workflows** — `SEND_CONTRACTS`, testimonial publishing, snapshot history (staff-side — Toptal-staff surfaces, not talent-side)\n- **Talent-signal sourcing** — `CreateTalentSignal`, `GetTalentSignalSetup` (sourcing-side — recruiter / staff identifying talent, not talent-side)\n\n> **Note**: The `scheduler.toptal.com` surface (interview scheduling) is currently absent rather than out by design — deferred until reverse-engineering research closes.\n\nSee [What It Does](#what-it-does) above for the positive scope.\n\n## Prerequisites\n\n- **Node.js** >= 22.19.0\n- A **Toptal Talent** profile (you must be a Toptal talent to use this — it has no value to anyone else)\n- Recommended: **[1Password CLI](https://developer.1password.com/docs/cli/get-started/)** (`op`) for credential resolution\n\n### Supported platforms\n\nTTCtl uses [`node-wreq`](https://www.npmjs.com/package/node-wreq) for Chrome\nTLS-fingerprint impersonation on the Cloudflare-protected surfaces (profile\nediting). That transport ships as a prebuilt native binary, available for:\n\n| OS      | Architecture           | libc          | Supported  |\n| ------- | ---------------------- | ------------- | ---------- |\n| macOS   | x64 (Intel)            | —             | ✅         |\n| macOS   | arm64 (Apple Silicon)  | —             | ✅         |\n| Linux   | x64                    | glibc         | ✅         |\n| Linux   | x64                    | musl (Alpine) | ✅         |\n| Linux   | arm64                  | glibc         | ✅         |\n| Linux   | arm64                  | musl (Alpine) | ❌ not yet |\n| Windows | x64                    | —             | ✅         |\n| Windows | arm64 (Windows on ARM) | —             | ❌ not yet |\n\nOn an unsupported platform, `npm install -g ttctl` still **succeeds** (the\nnative binary is an optional dependency) and mobile-gateway commands keep\nworking — but any Cloudflare-protected command (anything that edits your\nprofile) fails with a clear `NATIVE_MODULE_UNAVAILABLE` error naming your\nplatform and this supported set. If you need one of the two not-yet-supported\ntargets, please [open an issue](https://github.com/alexey-pelykh/ttctl/issues).\n\n## Installation\n\n```sh\nnpm install -g ttctl\n```\n\nOr run directly with npx:\n\n```sh\nnpx ttctl --help\n```\n\n### Hardened install (recommended)\n\nFor maximum supply-chain safety, install with `postinstall` hooks disabled:\n\n```sh\nnpm install -g --ignore-scripts ttctl\n```\n\n`--ignore-scripts` blocks `preinstall` / `postinstall` / `prepare` hooks\nacross the entire dependency tree at install time. This prevents a\ncompromised transitive dep (PhantomRaven, Shai-Hulud, etc.) from\nexecuting code on your machine during install — closing the\nmost-exploited supply-chain attack vector against package-manager users.\n\nTTCtl itself ships no install-time hooks. Disabling them is purely a\ndefense against malicious behavior in dependencies you don't directly\ncontrol. See [SECURITY § User-install supply-chain hardening](SECURITY.md#user-install-supply-chain-hardening).\n\n## Quick Start\n\n```sh\n# 1. Install\nnpm install -g ttctl\n\n# 2. Bootstrap a config interactively (recommended)\n#    Walks you through Form A (1Password reference; vault + item picker\n#    when the `op` CLI is installed) or Form B (literal credentials,\n#    explicit warning). Output: ~/.ttctl.yaml at mode 0600.\nttctl auth init\n\n# 3. Sign in (captures the bearer back into ~/.ttctl.yaml under auth.token)\nttctl auth signin\n\n# 4. Verify\nttctl auth status\n\n# 5. View your profile\nttctl profile show\n```\n\n> **`ttctl auth init`** scaffolds a fresh `~/.ttctl.yaml` interactively. Choose Form A (1Password reference, recommended) or Form B (literal credentials, discouraged — guarded by an explicit warning). On Form A with the `op` CLI installed, a vault picker and a LOGIN-category item picker run. Without `op` or on JSON-shape failure, the prompt falls back to a freeform `op://VAULT/ITEM` reference. The output file is written atomically at mode `0600` and refuses to overwrite an existing file unless `--force` is passed. The command is interactive only — pipe stdin or a non-TTY context exits non-zero with a clear message.\n\n> **`ttctl auth signin`** runs the `EmailPasswordSignIn` GraphQL mutation, captures the session bearer, and writes it BACK into the same `~/.ttctl.yaml` file under `auth.token` (atomic write; comments preserved; mode `0600`). All subsequent commands replay it as `Authorization: Token token=<X>` on every GraphQL request. The `op` CLI is invoked only at signin time to resolve your credentials. There is **no separate token file** — your config and your live session live in one YAML.\n\n> **Hand-authoring** the config also works (drop a YAML file at `~/.ttctl.yaml`, `chmod 600`); see the [Configuration](#configuration) section below for the four valid `auth` shapes. `ttctl auth init` is the recommended starting point.\n\n## Configuration\n\nTTCtl uses a single config file — **no profiles**. The config-file path is resolved deterministically (highest precedence wins):\n\n1. `--config <path>` flag — explicit override (per-invocation).\n2. `TTCTL_CONFIG_FILE` env var — process-scoped (CI, direnv).\n3. `~/.ttctl.yaml` — POSIX home dotfile (only fallback).\n\nXDG paths (`$XDG_CONFIG_HOME/ttctl/config.yaml`, `~/.config/ttctl/config.yaml`) and the current-working-directory `./.ttctl.yaml` are **not** auto-discovered. To use a project-local config, point `TTCTL_CONFIG_FILE` at it via direnv:\n\n```sh\n# .envrc in your project root (requires `direnv` installed and `direnv allow` run)\nexport TTCTL_CONFIG_FILE=\"$PWD/.ttctl.yaml\"\n```\n\n### Sync-root exclusion\n\nTTCtl **refuses to persist** the captured bearer when the config file lives under a known cloud-sync prefix:\n\n| Path prefix                   | Service            |\n| ----------------------------- | ------------------ |\n| `~/Library/Mobile Documents/` | macOS iCloud Drive |\n| `~/Dropbox/`                  | Dropbox            |\n| `~/iCloud Drive/`             | iCloud (alt name)  |\n| `~/OneDrive/`                 | OneDrive           |\n| `~/Google Drive/`             | Google Drive       |\n| `~/Box/`, `~/Box Sync/`       | Box                |\n\nPersisting a bearer under any of these would silently replicate it off-host. If you need a project-local config, keep it OUTSIDE these directories and route via `TTCTL_CONFIG_FILE` (direnv recipe above). The sibling-name guard prevents false positives — `~/DropboxOther/` is fine.\n\n### `auth` — four valid lifecycle states\n\nThe `auth` block is **structured** with optional `credentials` and `token` fields. Four valid shapes correspond to the lifecycle states:\n\n#### Form A — credentials via 1Password reference (RECOMMENDED, signin populates `auth.token`)\n\n```yaml\n# Single 1Password account (or relying on `OP_ACCOUNT` env / op default)\nauth:\n  credentials: \"op://Personal/ttctl\"\n\n# Multiple 1Password accounts — pin one explicitly\nauth:\n  credentials: \"op://my-account/Personal/ttctl\"\n```\n\nTTCtl parses `op://[ACCOUNT/]VAULT/ITEM` and runs `op item get ITEM --vault VAULT [--account ACCOUNT] --format json`, then resolves credentials by matching fields with `purpose: USERNAME` and `purpose: PASSWORD` — the canonical semantic identifiers 1Password sets automatically for **LOGIN-category** items (including browser-autosaved logins where the field labels are the HTML form input names like `user[email]`).\n\nThe optional `ACCOUNT` segment is required only when you have multiple `op` accounts configured (`op account list`); single-account setups can omit it. ACCOUNT may be the account UUID, the shorthand set via `op account add`, or the sign-in email — TTCtl forwards the value verbatim to `op` for validation.\n\n#### Form A2 — per-field 1Password references (signin populates `auth.token`)\n\n```yaml\n# username and password each resolve from their own field-level op:// ref\nauth:\n  credentials:\n    username: \"op://Personal/ttctl/username\"\n    password: \"op://Personal/ttctl/password\"\n\n# account prefix works on field refs too (parity with Form A)\nauth:\n  credentials:\n    username: \"op://my-account/Personal/ttctl/username\"\n    password: \"op://my-account/Personal/ttctl/password\"\n```\n\nWhen `credentials` is an **object of two `op://` field references**, TTCtl runs `op read --no-newline [--account ACCOUNT] op://VAULT/ITEM/FIELD` once per field. `op read` resolves the field by id or label, so the canonical `username` / `password` field ids work even on browser-autosaved LOGIN items. The optional leading `ACCOUNT` is split off and forwarded as `--account`, for parity with Form A.\n\n> A 4-segment field ref is always read as **account-prefixed** (`op://account/vault/item/field`). 1Password **sectioned** references (`op://vault/item/section/field`, also 4 segments) are therefore **not supported** — the section would be mis-taken as the account. Store the credential field at the item top level (LOGIN items do this by default).\n\n**Routing is by container shape, never by segment count** (`op://a/b/c` is account/vault/item as a bare string but vault/item/field as an object value). A bare-string `credentials` value is always single-item (Form A), item-level only. An object value is per-field (this form) when its values are `op://` refs, or literal (Form B) when they are an email/password pair.\n\n#### Form B — literal credentials (dev/testing only)\n\n```yaml\nauth:\n  credentials:\n    username: \"you@example.com\" # value is your Toptal email; field name matches 1P USERNAME purpose\n    password: \"hunter2\"\n```\n\n> Discouraged for daily use. Plaintext credentials in config files leak through backups, sync clients, and accidental commits. Form A is what you want.\n\n> **Item-level (Form A) vs per-field (Form A2).** Form A reads both USERNAME and PASSWORD from one LOGIN item; Form A2 points each credential at its own field. A bare-string reference with a `/field` suffix is rejected — per-field requires the object shape (one string cannot carry two references).\n\n#### Form C — token only (out-of-band bootstrap)\n\n```yaml\nauth:\n  token: \"user_<24hex>_<20alnum>\"\n```\n\nThe bearer is supplied directly. `ttctl auth signin` is not applicable (refused with `NO_CREDENTIALS`). Useful for the E2E sandbox and for deployments that bootstrap a token via a side-channel.\n\n#### Form D — credentials + token (post-signin Form A or B)\n\n```yaml\nauth:\n  credentials: \"op://Personal/ttctl\"\n  token: \"user_<24hex>_<20alnum>\"\n```\n\nThe on-disk shape after a successful `ttctl auth signin` against a Form A or B config. Both fields coexist; signout removes the `token` field (returning to Form A or B) without touching `credentials`.\n\n## Pagination\n\nTTCtl exposes the wire's actual pagination model per service. The Toptal Talent platform uses **multiple coexisting pagination idioms** — there is no single uniform shape — and the wire rejects requests that mix idioms (PR [#383](https://github.com/alexey-pelykh/ttctl/pull/383) empirically proved this: an attempt to add `offset` to a limit-only field was rejected with HTTP 400 across 8 E2E test failures).\n\nRather than invent a uniform translation layer that would lie about what the wire accepts, ttctl flags name what the wire arg names; their types match what the wire arg accepts. This keeps AI / script callers honest: a flag name maps 1:1 to a wire argument (with **one** documented translation — see row 2).\n\n| Service                                              | CLI flags                            | MCP keys                       | Wire idiom                                                      |\n| ---------------------------------------------------- | ------------------------------------ | ------------------------------ | --------------------------------------------------------------- |\n| `jobs list`, `applications list`, `engagements list` | `--page` / `--per-page`              | `{page, perPage}`              | Offset-list `(page, pageSize)`                                  |\n| `payments payouts list`                              | `--page` / `--per-page` (translated) | `{page, perPage}` (translated) | Offset-pagination wrapper `(offsetPagination: {offset, limit})` |\n| `timesheet pending` _(post-#374 re-spike)_           | `--limit`                            | `{limit}`                      | Limit-only wrapper `(pagination: {limit})`                      |\n| _(future engagements-payments)_                      | `--limit` + `--after <id>`           | `{limit, after}`               | Limit+forward-cursor wrapper `(pagination: {limit, after})`     |\n| _(future performed-actions)_                         | `--before` / `--after` / `--limit`   | `{before, after, limit}`       | Bare bidirectional cursor `(before, after, limit)`              |\n\nHeterogeneity is the deliberate Sage move: a uniform `--cursor <token>` layer would forfeit scriptability for offset services (no integer-composable pages); verb decomposition (`show-all` vs `list`) would double the command tree without buying disambiguation the flag types already provide. Per-service `--help` is the canonical recovery surface for the specific flag shape.\n\nThe 5-row grammar is locked in [ADR-007 — Pagination flag grammar](hq/engineering/adr/ADR-007-pagination-flag-grammar.md).\n\n## Applying to jobs\n\nTTCtl can apply to a job opportunity directly via `ttctl jobs apply <job-id>`. This is a **destructive** wire operation — it creates a `JobApplication` record on Toptal's side and there is no `withdraw` operation in TTCtl (see [ADR-008](hq/engineering/adr/ADR-008-application-funnel-write-side.md) § What We're NOT Solving). Prefer `--dry-run` to preview the wire payload first.\n\n### Consent gate\n\n`--consent` is **required** — it represents your explicit acceptance of Toptal's apply terms (a legal-compliance attestation). The flag has no default; auto-filling on your behalf is forbidden per ADR-008 § Decision Part 4. Absence raises `CONSENT_REQUIRED` with no wire call issued.\n\n```sh\nttctl jobs apply <job-id> --consent\n```\n\n### Preview the question inventory\n\nMost jobs require answering matcher / expertise questions before the apply mutation accepts the request. `--show-questions` fetches the inventory WITHOUT issuing the apply mutation (read-only path; does not require `--consent`):\n\n```sh\nttctl jobs apply <job-id> --show-questions\n```\n\nThe output lists each question's identifier, prompt text, and the pre-apply context (`canApply`, `suggestedRate`, `rateValidation`). Per the recovered SDL (#438), the identifier maps differently across the two answer arrays: matcher answers use it as `id`; expertise answers use it as `questionId` (see the JSON shape below). `ttctl jobs show <job-id> --with-questions` is a related read that inlines the same question inventory into the job's full detail view.\n\n### Supplying answers via `--answers-file`\n\nPass a JSON file containing per-question answers. The file shape follows the ADR-008 § Decision Part 2 grammar (the same shape `applications confirm` uses for Interest Requests). Matcher answers and expertise answers use **different key shapes** per the recovered SDL (#438): matcher answers carry the question identifier at `id`; expertise answers carry it at `questionId` and also accept `other` / `subjectId` (both nullable):\n\n```json\n{\n  \"matcherAnswers\": [\n    { \"id\": \"<matcher-question-id>\", \"answer\": \"<your-answer>\" },\n    { \"id\": \"<matcher-question-id>\", \"answer\": \"<your-answer>\" }\n  ],\n  \"expertiseAnswers\": [\n    { \"questionId\": \"<expertise-question-id>\", \"other\": null, \"subjectId\": null },\n    { \"questionId\": \"<expertise-question-id>\", \"other\": \"free-text-context\", \"subjectId\": \"<subject-id>\" }\n  ]\n}\n```\n\nThen:\n\n```sh\nttctl jobs apply <job-id> --consent --answers-file ./apply.json\n```\n\nPass `-` instead of a path to read the JSON from stdin. The wrapper shape AND inner items are validated against the recovered Zod schemas (per #438) BEFORE any wire call — malformed JSON or an unknown key refuses with a `VALIDATION_ERROR` envelope and no mutation is issued.\n\n`--pitch-file <path>` (or `-` for stdin) attaches a `PitchInput` payload alongside the answers; only one flag may claim stdin per invocation.\n\n### Dry-run preview\n\n`--dry-run` emits the full apply preview envelope (operation name, surface, transport, GraphQL variables) without issuing the wire mutation. Use it to verify the payload shape before committing to the destructive call:\n\n```sh\nttctl jobs apply <job-id> --consent --dry-run\n```\n\nThe `--consent` gate still applies under `--dry-run` — the legal-compliance attestation is required even for previews (the gate is a CLI-level refusal, not a wire-level one).\n\n## Interest Requests\n\nWhen a recruiter expresses interest in your profile via Toptal's Availability Request (AR) flow, you respond via `applications confirm` / `applications reject`. Confirming an AR is **destructive** — it transitions the AR to `AVAILABILITY_REQUEST_CONFIRMED` and creates a `JobApplication`. There is no withdraw operation; prefer `--dry-run` to preview the wire payload first.\n\n### Discovering the AR id\n\nThe `<id>` argument is the **AvailabilityRequest id**, NOT the activity-item id. Discover it by inspecting the activity row:\n\n```sh\nttctl applications show <activityId>\n```\n\n…and look for the `Availability request: <id>` line in the output.\n\n### Confirming an Interest Request\n\n```sh\nttctl applications confirm <ar-id>\n```\n\nWhen both `--rate` and `--kind` are omitted, the service pre-fetches the AR metadata and auto-fills:\n\n- `kind` from `metadata.__typename`\n- `rate` from `metadata.offeredHourlyRate` (Fixed-kind only)\n\nFlexible-kind ARs have no recruiter-pinned rate; callers MUST pass `--rate <decimal>` for `FLEXIBLE` / `MARKETPLACE_FLEXIBLE` ARs.\n\n### Optional `--answers-file` / `--pitch-file`\n\nWhen the AR carries matcher / expertise questions or a pitch prompt, supply answers via the same JSON-file grammar as `jobs apply`:\n\n```sh\nttctl applications confirm <ar-id> \\\n  --answers-file ./answers.json \\\n  --pitch-file ./pitch.json\n```\n\nBoth flags accept `-` to read from stdin (at most one stdin claim per invocation). The answers file shape is identical to the one shown in [Applying to jobs](#applying-to-jobs) above. Pitch payloads are validated against the recovered `PitchInput` Zod schema (per #438); extra unknown keys reject with a field-path error.\n\n### Rejecting an Interest Request\n\n```sh\nttctl applications reject <ar-id> --reason <key>\n```\n\nThe `<key>` comes from the server-localised inventory at `ttctl applications reject-reasons`. Some keys require an accompanying `--comment <text>` — the reject-reasons inventory marks which.\n\n## MCP Integration\n\nTTCtl implements the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) so AI assistants can interact with your Toptal Talent profile through natural language. Run `ttctl mcp` to start an MCP server on stdio.\n\n### MCP Client Configuration\n\n<details>\n<summary><b>Claude Desktop</b></summary>\n\nAdd to `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"ttctl\": {\n      \"command\": \"npx\",\n      \"args\": [\"ttctl\", \"mcp\"]\n    }\n  }\n}\n```\n\n</details>\n\n<details>\n<summary><b>Claude Code</b></summary>\n\n```sh\nclaude mcp add ttctl -- npx ttctl mcp\n```\n\n</details>\n\n<details>\n<summary><b>Cursor</b></summary>\n\nAdd to `.cursor/mcp.json` in your project root:\n\n```json\n{\n  \"mcpServers\": {\n    \"ttctl\": {\n      \"command\": \"npx\",\n      \"args\": [\"ttctl\", \"mcp\"]\n    }\n  }\n}\n```\n\n</details>\n\n> **Trust model**: any process that can spawn `ttctl mcp` gets full access to your Toptal Talent profile through your saved auth token. Don't grant MCP access to untrusted AI agents. See [SECURITY.md](SECURITY.md).\n\n### Data-handling — what MCP tools return into your assistant's context\n\nWhen an AI assistant calls a TTCtl MCP tool, the tool's response payload — your profile, your engagements, your payouts, your contracts — enters the assistant's context window. From that point, **your AI-assistant host decides** what happens to that data: whether it's persisted to chat history, indexed into a vector database, surfaced to other tools loaded in the same session, or uploaded as part of telemetry.\n\nTTCtl cannot pin host behaviour. The defenses live in your operator choices. Three concrete recommendations:\n\n1. **Pick a host that matches your privacy posture.** Current host defaults (as of 2026-05; expected to drift) — Claude Code: per-session, opt-in cross-session persistence; Claude Desktop: default-on local chat-history persistence; Cursor: default-on local persistence + default-on vector-DB indexing of tool outputs; Windsurf: similar to Cursor; ChatGPT desktop: vendor-managed, assume persistent. The vector-indexing hosts (Cursor / Windsurf) can re-surface your Toptal data into a future, unrelated conversation if a semantic query matches — this is the highest-friction property of the persistence destinations.\n2. **Use private / ephemeral chat modes** for sessions that exercise TTCtl tools returning third-party free-text — namely engagement comments, application messages, job descriptions, contract clauses, and review comments. Operators who routinely browse jobs or read engagement notes via the assistant should consider a host-side workspace dedicated to TTCtl tasks (Cursor: separate workspace; Claude Code: per-project scoping).\n3. **Do not co-load TTCtl with outbound tools you don't control.** A web-fetch tool, an arbitrary-filesystem-write tool, or a sibling MCP server with its own apply path makes cross-tool exfiltration a realistic threat: an adversarial instruction embedded in a Toptal engagement note could chain into the sibling tool's apply path, with no `dryRun` gate. The mitigation is session-level — keep TTCtl in sessions where the loaded tools are operator-trusted.\n\nThe full threat model — including the per-tool audit, severity rubric, mitigation trade-off analysis, and residual risk register — is in [`docs/security/mcp-leakage-threat-model.md`](docs/security/mcp-leakage-threat-model.md). The companion section in [SECURITY.md § MCP Trust Model](SECURITY.md#mcp-trust-model) covers the input-side defenses (state-change mitigation via `dryRun`, file-upload sandbox).\n\n## Troubleshooting\n\n### Debug logs for `auth signin` / `auth signout`\n\nWhen the post-signin write-back to `~/.ttctl.yaml` does something unexpected — silent overwrite, mtime-drift refusal, lock contention, mysterious \"Refusing to write\" message — turn on the structured debug log to see exactly which invariant fired and what the file's state was at each step:\n\n```sh\nTTCTL_DEBUG_CONFIG=1 ttctl auth signin\n```\n\nOutput is one JSON object per line on **stderr** (so `-o json` mode on stdout stays clean). The bearer token is **never** logged. Pipe through `jq` to inspect:\n\n```sh\nTTCTL_DEBUG_CONFIG=1 ttctl auth signin 2> debug.log\njq -c '.' debug.log\n```\n\nEach record carries `ts` (ISO-8601), `op` (`persist` or `clear`), `path`, and `event`. Event-specific fields tell you what happened:\n\n| Event               | Meaning                                  | Useful fields                                         |\n| ------------------- | ---------------------------------------- | ----------------------------------------------------- |\n| `prewrite_checks`   | Symlink/sync-root gates passed           | `symlink_ok`, `syncroot_ok`, `perm_ok`                |\n| `lock_acquired`     | Got the cross-process write lock         | `wait_ms` (was there contention?)                     |\n| `stat_baseline`     | Captured pre-write file state            | `mtime_ms`, `mode_octal`                              |\n| `tempfile_written`  | Temp file flushed to disk                | `temp_path`, `size_bytes`                             |\n| `final_state`       | Mode applied to temp before rename       | `mode_applied`                                        |\n| `mtime_drift_check` | Concurrency re-check vs baseline         | `delta_ms`, `pass` (false ⇒ another writer raced you) |\n| `rename_completed`  | Atomic rename to canonical path          | —                                                     |\n| `lock_released`     | Lock released in `finally`               | `duration_ms` (total hold)                            |\n| `error`             | Something threw inside the locked region | `error_class`, `error_message`, `lock_held_at_error`  |\n\nIf you ever see an `error` event without a following `lock_released`, that would be a leaked lock — file an issue. The error path always emits `lock_released` (the release lives in the `finally`, runs regardless of which invariant fired).\n\nWhen `TTCTL_DEBUG_CONFIG` is unset (or set to anything other than literal `1` — `0`, `true`, empty string all count as \"off\"), the logger is silent and pays zero per-call overhead.\n\n### Remote kill-switch (wire-break defense)\n\nTTCtl is reverse-engineered from Toptal's APK; the wire format can change without notice. As a defense-in-depth signal, the CLI fetches a small JSON manifest at startup from `https://raw.githubusercontent.com/alexey-pelykh/ttctl/main/status/known-broken.json` and emits a warning (or refuses to run) when the running version is listed as known-broken. The MCP server does the same at `buildServer` time, plus a recurring ~24h refetch — but always warns, never refuses (refusing a long-lived server interrupts in-flight tool calls).\n\n- **Latency cost**: synchronous fetch with a 3 s timeout. Typical latency 50–200 ms against raw.githubusercontent.com; worst-case 3 s before the action runs.\n- **Privacy**: the fetch sends only the default Node `fetch` headers — no version, no account identifier, no telemetry. Install-count tracking is intentionally deferred.\n- **Fail-silent**: every error path (network, timeout, 404, malformed manifest) is swallowed. The kill-switch can NEVER itself cause a denial-of-service when the maintainer's repo is briefly unreachable.\n- **Override** — disable the check entirely with:\n\n  ```sh\n  export TTCTL_DISABLE_KILL_SWITCH=1\n  ```\n\n  Captured at module load (matches the `TTCTL_DEBUG_CONFIG` / `TTCTL_DEBUG_MCP` / `TTCTL_MCP_FILE_UPLOAD_ALLOW_ANY` conventions). Any other value (empty string, `0`, `true`) keeps the check enabled.\n\nThe detection sources, severity tiers, response cadence, and C&D / legal posture are documented in [`docs/operations/wire-breakage-runbook.md`](docs/operations/wire-breakage-runbook.md). The manifest schema and update procedure live in [`status/README.md`](status/README.md).\n\n## How TTCtl works under the hood\n\nThe Toptal Talent platform exposes three GraphQL endpoints:\n\n- `https://www.toptal.com/gateway/graphql/talent/graphql` — used by the mobile app; accepts plain HTTPS + session cookies\n- `https://www.toptal.com/api/talent_profile/graphql` — used by the web profile editor; **Cloudflare-protected** (requires browser TLS fingerprint impersonation)\n- `https://scheduler.toptal.com/api/graphql` — used by the scheduler; separate Cloudflare zone\n\nTTCtl uses two transports:\n\n- **Stock** ([`undici`](https://github.com/nodejs/undici)) for the mobile gateway endpoint\n- **TLS-impersonating** ([`node-wreq`](https://www.npmjs.com/package/node-wreq) with the `chrome_147` profile) for the Cloudflare-protected endpoints\n\nAuthentication is **bearer-token based** (per ADR-005 in the private `ttctl/research` repo): TTCtl POSTs `EmailPasswordSignIn` in persisted-query mode (SHA-256 hash from a captured operations catalog), captures the returned session token, persists it back into the same `~/.ttctl.yaml` config under `auth.token` (atomic write, mode `0600`), and replays it as `Authorization: Token token=<X>` on every subsequent GraphQL request. Cookies are NOT used; Chrome TLS impersonation alone passes Cloudflare on the protected surfaces.\n\nThe reverse-engineering artifacts (APK decoded, GraphQL operations catalog, schema synthesis, ADRs) live in a separate **private** repository (`ttctl/research`). They are not redistributable.\n\n## Disclaimer\n\nTTCtl is an **independent personal-productivity project** not affiliated with, endorsed by, or officially connected to **Toptal**, Toptal LLC, or any Toptal entity.\n\nThis software is provided to you, the Toptal talent, as a tool for managing **your own** profile data more efficiently. Using it to interact with profiles other than your own, to scale outreach beyond what a human can do manually, to manipulate platform-side metrics, or to extract data about other parties violates the spirit of fair use and Toptal's Terms of Service.\n\nThe maintainer disclaims any warranty and accepts no liability for use that violates platform terms. If Toptal LLC objects to TTCtl's existence and reaches out in good faith, the maintainer will work with them to address the concern.\n\n\"Toptal\" is a trademark of Toptal LLC. Use of the name in this project's description is **nominative fair use** only — to identify the third-party platform that TTCtl interacts with.\n\nFor a longer write-up of the project's posture on trademark use, Terms of Service, the AGPL choice, and the fair-use tradition this kind of tool sits in, see [`docs/legitimacy.md`](docs/legitimacy.md).\n\n## Support\n\nTTCtl is reverse-engineered from Toptal's APK; the wire format can change without notice, and the maintainer's only signal that something has shifted is **your report**. There is no telemetry, no error-reporting endpoint, no install-count tracking — the channels below are the entire return path.\n\n### Wire broke for you — `ttctl <command>` failing\n\nIf a TTCtl command errors or returns an unexpected response (404, cryptic GraphQL error, `WIRE_SHAPE_ERROR` envelope, etc.), open a [**Wire broke for me**](https://github.com/alexey-pelykh/ttctl/issues/new?template=wire-broke-for-me.yml) issue. The template captures `ttctl --version`, the failing command, the error text, and whether `ttctl auth status` still succeeds — enough for the maintainer to triage against the kill-switch manifest at [`status/known-broken.json`](status/known-broken.json) and the [wire-breakage runbook](docs/operations/wire-breakage-runbook.md).\n\nBefore filing, check the kill-switch manifest at <https://raw.githubusercontent.com/alexey-pelykh/ttctl/main/status/known-broken.json> — if your version is listed, the maintainer is already aware.\n\n### MCP host failed to invoke a tool\n\nIf an MCP host (Claude Desktop, Claude Code, Cursor, Windsurf, etc.) could not surface or invoke a TTCtl tool — the failure is at the **host ↔ server boundary**, not the Toptal wire — open an [**MCP host failed to invoke a tool**](https://github.com/alexey-pelykh/ttctl/issues/new?template=mcp-tool-issue.yml) issue. The template captures host product + version, the failing tool name, the host's surfacing of the error, and the optional `TTCTL_DEBUG_MCP=1` server-side trace.\n\n### Questions, ideas, \"is this expected?\"\n\nUse [Discussions](https://github.com/alexey-pelykh/ttctl/discussions) for questions, usage help, ideas, or \"did anyone else see this?\" — the issue tracker is reserved for confirmed breakages and feature requests.\n\n### Security vulnerabilities\n\n**Do not** open a public issue for security vulnerabilities. See [SECURITY.md § Reporting a Vulnerability](SECURITY.md#reporting-a-vulnerability) for the coordinated-disclosure channel.\n\n### Abuse reporting\n\nSee [SECURITY.md § Abuse reporting](SECURITY.md#abuse-reporting) if you've observed TTCtl being used in ways that violate Toptal's Terms of Service (mass automation, recruiter scraping, engagement-signal manipulation, etc.).\n\n### Privacy when reporting\n\n`~/.ttctl.yaml` carries your captured bearer token (`auth.token`, shape `user_<24hex>_<20alnum>`). **Never paste it in a public issue or Discussions thread.** TTCtl's debug emitters (`TTCTL_DEBUG_CONFIG`, `TTCTL_DEBUG_MCP`) are designed to never print bearer values, but transcripts from your shell may include them — scan before pasting and redact as `<REDACTED>`.\n\n## Code of Conduct\n\nThis project adopts the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md) (version 3.0). The Code applies to all project spaces and to anyone interacting with the project — issue tracker, pull requests, MCP server interactions, and any other channel.\n\nTo report a concern, email <alexey.pelykh@gmail.com>. Pseudonymous reports are accepted; truly anonymous reports are not feasible (the maintainer reads the inbox directly). Best-effort response within 7 days.\n\n## License\n\n[AGPL-3.0-only](LICENSE)\n\n### What AGPL means for you\n\n- **Using `ttctl` as a CLI tool or MCP server** does not make your code AGPL-licensed. Running the tool, scripting around it, or connecting it to your applications is normal use — no license obligations arise.\n- **Using `@ttctl/core` as a library** (importing it into your code) means your combined work is covered by AGPL-3.0. If you distribute that combined work, you must make its source available under AGPL-compatible terms.\n- **Modifying and distributing TTCtl itself** requires you to share your changes under AGPL-3.0.\n- **Commercial licensing** is not currently offered. TTCtl is a personal-productivity project, not a product.\n",
  "bytes": 39297,
  "sha": "fc6b4e5f92bafe54e4693ea92d44bb62115859223af3f113a02d8b354f4646b3",
  "repo_slug": "alexey-pelykh/ttctl",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_alexey_pelykh_ttctl_baf42bbe/readme"
}