{"id":48347791,"url":"https://github.com/citizenweb3/upwork-agent","last_synced_at":"2026-04-05T08:02:16.433Z","repository":{"id":344001328,"uuid":"1180057403","full_name":"citizenweb3/upwork-agent","owner":"citizenweb3","description":"Autonomous Upwork job search agent","archived":false,"fork":false,"pushed_at":"2026-03-12T17:31:57.000Z","size":88,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-12T22:58:53.892Z","etag":null,"topics":["agent","agent-framework","agent-skills","agentic-ai","agentic-workflow","agents","ai","ai-agent","ai-agents","upwork"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/citizenweb3.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-12T16:51:44.000Z","updated_at":"2026-03-12T22:38:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/citizenweb3/upwork-agent","commit_stats":null,"previous_names":["citizenweb3/upwork-agent"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/citizenweb3/upwork-agent","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citizenweb3%2Fupwork-agent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citizenweb3%2Fupwork-agent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citizenweb3%2Fupwork-agent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citizenweb3%2Fupwork-agent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/citizenweb3","download_url":"https://codeload.github.com/citizenweb3/upwork-agent/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citizenweb3%2Fupwork-agent/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31428645,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T02:22:46.605Z","status":"ssl_error","status_checked_at":"2026-04-05T02:22:33.263Z","response_time":75,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","agent-framework","agent-skills","agentic-ai","agentic-workflow","agents","ai","ai-agent","ai-agents","upwork"],"created_at":"2026-04-05T08:02:14.708Z","updated_at":"2026-04-05T08:02:16.415Z","avatar_url":"https://github.com/citizenweb3.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Upwork Agent\n\nAutonomous Upwork job search agent powered by Claude Code. Finds relevant jobs, scores them against your profile, generates tailored proposals, and submits them — all controlled via Telegram buttons.\n\n## How It Works\n\nThe agent runs as a background daemon that connects three components:\n\n1. **Google Chrome** with your logged-in Upwork session (via Chrome DevTools Protocol)\n2. **Telegram bot** that sends you job cards and lets you approve/skip/redo with inline buttons\n3. **Claude Code** that does the actual browsing, scoring, and proposal writing\n\n```\nCron (every N min) or /search command\n        |\n        v\n  Claude Code browses Upwork\n  using your Chrome session\n        |\n        v\n  Scores each job 0-10\n  based on your profile\n        |\n        v\n  Jobs scoring \u003e= 4 sent\n  to Telegram with buttons\n        |\n        v\n  You press [Apply] or [Skip]\n        |\n        v\n  Claude generates proposal\n  matching your writing style\n        |\n        v\n  You review: [Send] [Cancel] [Redo]\n        |\n        v\n  Claude submits on Upwork\n```\n\n## Setup\n\nThere are two ways to run the agent:\n\n| | **Docker (recommended)** | **Bare metal** |\n|---|---|---|\n| Best for | Server, 24/7, headless | Mac/Linux desktop with monitor |\n| Requirements | Docker, Docker Compose | Node.js, Chrome, Claude Code CLI |\n| Chrome access | Via noVNC in browser (`http://server:6080`) | Native Chrome window |\n| Setup time | ~5 min | ~15 min |\n\n---\n\n### Option A: Docker (recommended for servers)\n\n#### Requirements\n\n- Docker and Docker Compose\n- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) subscription (for OAuth token)\n- Telegram account\n\n#### 1. Clone\n\n```bash\ngit clone \u003crepo-url\u003e\ncd upwork-agent\n```\n\n#### 2. Create Telegram bot\n\n1. Open [@BotFather](https://t.me/BotFather) in Telegram\n2. Send `/newbot`, follow the prompts, copy the **bot token**\n3. Create a group chat (or use personal chat) and add the bot\n4. Get the **chat ID**:\n   - For groups: add [@RawDataBot](https://t.me/RawDataBot), it prints the chat ID (negative number), then remove it\n   - For personal chat: send any message to [@userinfobot](https://t.me/userinfobot)\n5. Get your **user ID**: send any message to [@userinfobot](https://t.me/userinfobot)\n6. If using a group:\n   - BotFather \u003e `/mybots` \u003e your bot \u003e Bot Settings \u003e Group Privacy \u003e **Turn off**\n   - In the group: Settings \u003e **Visible History** \u003e turn on (so link previews in job cards work)\n\n#### 3. Configure environment\n\n```bash\ncp .env.example infra/.env\n```\n\nEdit `infra/.env`:\n\n| Variable | Description | How to get |\n|----------|-------------|-----------|\n| `BOT_TOKEN` | Telegram bot token | From [@BotFather](https://t.me/BotFather) |\n| `CHAT_ID` | Telegram chat ID | See step 2 above |\n| `ALLOWED_USERS` | User IDs who can press buttons | From [@userinfobot](https://t.me/userinfobot), comma-separated |\n| `TIMEZONE` | Your timezone | `Europe/Lisbon` (default) |\n| `SEARCH_INTERVAL_MIN` | Auto-search interval in minutes | Default: `20` |\n| `CLAUDE_CODE_OAUTH_TOKEN` | Claude Code OAuth token | See below |\n| `CLAUDE_ACCOUNT_UUID` | Account UUID | See below |\n| `CLAUDE_EMAIL` | Account email | See below |\n| `CLAUDE_ORG_UUID` | Organization UUID | See below |\n\n**Getting Claude Code OAuth credentials** — on a machine where Claude Code is already logged in:\n\n```bash\n# OAuth token (macOS)\nsecurity find-generic-password -s \"claude-cli\" -w\n\n# Account UUID, email, org UUID\ncat ~/.claude.json | grep -A5 oauthAccount\n```\n\n#### 4. Set up your profile\n\n```bash\ncp data/profile.example.md data/profile.md\n```\n\nEdit `data/profile.md` with your name, tech stack, experience, scoring criteria, and proposal style. The agent reads this file to score jobs and write proposals.\n\n#### 5. Launch\n\n```bash\ncd infra\ndocker compose up -d\n```\n\nFirst build takes ~5 minutes. After that:\n\n1. Open `http://server:6080` in a browser — you'll see Chrome via noVNC\n2. Log in to Upwork manually in that Chrome\n3. The session persists in a Docker volume across restarts\n\nThe bot sends a Telegram notification when the browser is ready.\n\n#### Docker management\n\nAll commands from `infra/` directory:\n\n```bash\ncd infra\ndocker compose logs -f          # follow logs\ndocker compose restart          # restart\ndocker compose down             # stop\ndocker compose up -d --build    # rebuild after code changes\n```\n\n---\n\n### Option B: Bare metal (Mac/Linux desktop)\n\n#### Requirements\n\n- Node.js \u003e= 20\n- Google Chrome (real browser, not Chromium — Upwork detects `navigator.webdriver` in Playwright's bundled Chromium)\n- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) with active subscription\n- Telegram account\n\n#### Install system dependencies\n\n**macOS:**\n\n```bash\nbrew install node\nnpm install -g yarn @anthropic-ai/claude-code\n# Google Chrome — install from https://www.google.com/chrome/\n```\n\n**Ubuntu Desktop:**\n\n```bash\ncurl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -\nsudo apt install -y nodejs lsof\nnpm install -g yarn @anthropic-ai/claude-code\nwget -q -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb\nsudo dpkg -i /tmp/chrome.deb \u0026\u0026 sudo apt --fix-broken install -y\n```\n\n#### 1. Clone and install\n\n```bash\ngit clone \u003crepo-url\u003e\ncd upwork-agent\nyarn install\n```\n\n#### 2. Create Telegram bot\n\nSame as Docker Option A, step 2.\n\n#### 3. Configure environment\n\n```bash\ncp .env.example .env\n```\n\nEdit `.env`:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `BOT_TOKEN` | Telegram bot token from BotFather | required |\n| `CHAT_ID` | Telegram chat ID (negative for groups) | required |\n| `ALLOWED_USERS` | Comma-separated Telegram user IDs | required |\n| `TIMEZONE` | Your timezone | `Europe/Lisbon` |\n| `SEARCH_INTERVAL_MIN` | Auto-search interval in minutes (8:00-23:00) | `20` |\n| `CHROME_PATH` | Path to Chrome binary | Auto-detected by OS |\n\n#### 4. Set up your profile\n\n```bash\ncp data/profile.example.md data/profile.md\n```\n\nEdit with your name, tech stack, experience, scoring criteria, and proposal style.\n\n#### 5. First launch — log in to Upwork\n\n```bash\nyarn daemon\n```\n\nA Chrome window opens with a separate profile (`data/browser-data/`). **Log in to Upwork manually.** The session persists across daemon restarts.\n\n#### 6. Daily use\n\n```bash\nyarn daemon\n```\n\nThe daemon will:\n- Auto-search every N minutes during 8:00-23:00 (your timezone)\n- Send matching jobs to Telegram\n- Wait for your button presses\n- Generate and submit proposals on your command\n- Send a heartbeat ping every 6 hours\n\n## Telegram Commands\n\n| Command | What it does |\n|---------|-------------|\n| `/search` | Run a job search right now |\n| `/status` | Show browser status, queue length, Claude running |\n| `/report` | Job stats for today |\n| `/report week` | Job stats for the past 7 days |\n| `/report all` | Job stats for all time |\n\n## Telegram Buttons\n\nOn **job cards**:\n| Button | Action |\n|--------|--------|\n| Apply | Generate a proposal for this job |\n| Skip | Mark as skipped, no further action |\n\nOn **proposals**:\n| Button | Action |\n|--------|--------|\n| Send | Submit the proposal on Upwork |\n| Cancel | Discard the proposal |\n| Redo | Generate a completely different proposal |\n\nOn **errors**:\n| Button | Action |\n|--------|--------|\n| Retry | Re-run the failed action |\n\n## CLI Tools\n\nThese can be used independently from the daemon:\n\n```bash\n# Job database\nyarn jobs add --title '...' --url '...' --relevance-score 7\nyarn jobs get \u003cid\u003e\nyarn jobs check \u003curl\u003e\nyarn jobs list\nyarn jobs list --status applied\nyarn jobs find \"react typescript AI\"\nyarn jobs update \u003cid\u003e --status applied\nyarn jobs stats              # today\nyarn jobs stats --week       # past 7 days\nyarn jobs stats --all        # all time\n\n# Telegram\nyarn tg send \"Hello\"\nyarn tg send-job \u003cid\u003e\nyarn tg send-proposal \u003cid\u003e\n\n# Briefing\nyarn morning\n```\n\n## Architecture\n\n```\n+----------------------------------------------------------+\n|  yarn daemon  (src/daemon.ts)                            |\n|                                                          |\n|  +---------------+  +--------------+  +---------------+  |\n|  | Google Chrome  |  | Grammy Bot   |  | Cron          |  |\n|  | (real browser) |  | (Telegram)   |  | Scheduler     |  |\n|  | CDP :9222      |  |              |  |               |  |\n|  +-------+-------+  +------+-------+  +-------+-------+  |\n|          |                  |                  |          |\n|          |           +------+------------------+          |\n|          |           |  Task Queue (mutex)                |\n|          |           |  one Claude Code at a time         |\n|          |           +------+--------------------         |\n|          |                  |                             |\n|          |    spawn('claude', ['-p', task, ...])          |\n|          |                  |                             |\n|  +-------+------------------+------------------------+    |\n|  |  Claude Code (child process)                      |    |\n|  |  Tools: Playwright MCP (CDP -\u003e :9222)             |    |\n|  |         Bash (yarn jobs, yarn tg)                 |    |\n|  |         Read, Write                               |    |\n|  +---------------------------------------------------+    |\n|                                                          |\n|  data/jobs.db  (SQLite + FTS5, WAL mode)                 |\n+----------------------------------------------------------+\n```\n\n### Key design decisions\n\n- **Real Chrome, not Playwright Chromium** — Upwork uses Cloudflare Turnstile which detects `navigator.webdriver=true` in Playwright's bundled Chromium. Real Chrome doesn't have this marker.\n- **CDP connection** — Chrome runs with `--remote-debugging-port=9222`, daemon connects via `chromium.connectOverCDP()`. The Playwright MCP server also connects to this port.\n- **Mutex task queue** — Only one Claude Code process runs at a time. Tasks are queued and processed sequentially.\n- **Model routing** — Haiku for search and submit (tool-heavy, speed matters), Sonnet for proposals and redo (creative writing, quality matters).\n- **Persistent session** — Chrome uses a separate `--user-data-dir` so cookies and login survive restarts. Keepalive reloads prevent session expiry.\n\n## File Structure\n\n```\nsrc/\n  daemon.ts       Main process: Chrome + bot + cron + task queue\n  jobs.ts         CLI for job database operations\n  tg.ts           CLI for Telegram messaging\n  morning.ts      CLI for daily briefing\n  db/\n    index.ts      SQLite connection (WAL mode)\n    schema.ts     Jobs table and indexes\n    search.ts     FTS5 full-text search\n\ndata/\n  profile.md          Your profile (gitignored, copy from example)\n  profile.example.md  Template for profile.md\n  jobs.db             SQLite database (auto-created)\n  browser-data/       Chrome profile with Upwork session (auto-created)\n  logs/               Task execution logs\n\ninfra/\n  Dockerfile          Container image (Ubuntu + Chrome + Node + VNC)\n  docker-compose.yml  Single-command deploy\n  supervisord.conf    Process manager (Xvfb, x11vnc, noVNC, daemon)\n  start.sh            Entrypoint (auth, cleanup, notifications)\n\n.mcp.json         Playwright MCP server config (CDP endpoint)\n.env              Bot token, chat ID, settings (gitignored)\n.env.example      Template for .env (both bare metal and Docker)\nCLAUDE.md         Agent instructions for Claude Code\n```\n\n## Job Statuses\n\n| Status | Meaning |\n|--------|---------|\n| `new` | Just added to DB, not scored high enough to send |\n| `sent` | Sent to Telegram, awaiting your decision |\n| `approved` | You pressed Apply, proposal is being generated |\n| `skipped` | You pressed Skip |\n| `cancelled` | You pressed Cancel on a proposal |\n| `applied` | Proposal submitted on Upwork |\n\n## Troubleshooting\n\n**Bot doesn't respond to commands**\n- Check Group Privacy is turned off in BotFather\n- Verify your user ID is in `ALLOWED_USERS`\n- If you converted a group to a supergroup, update `CHAT_ID` (it changes)\n\n**Upwork blocks access**\n- \"Cannot verify your request\" — IP/VPN issue, try switching VPN or disabling it\n- CAPTCHA — the agent tries to click it automatically (2 attempts), then notifies you to solve manually\n- Session expired — log in manually in the Chrome window\n\n**\"Not enough Connects\"**\n- Buy Connects on Upwork, then press the Retry button in Telegram\n\n**Chrome won't start**\n- Check `CHROME_PATH` in `.env` (or leave it empty for auto-detection)\n- Kill zombie processes: `lsof -ti:9222 | xargs kill -9`\n- In Docker: stale lock files are cleaned automatically on start. If still failing: `docker compose restart`\n\n**Docker: Claude Code auth error**\n- Check that `CLAUDE_CODE_OAUTH_TOKEN` is filled in `infra/.env`\n- OAuth tokens expire — get a fresh one and `docker compose restart`\n\n**Docker: noVNC not accessible**\n- Port 6080 is bound to `127.0.0.1` by default\n- For remote access change `\"127.0.0.1:6080:6080\"` to `\"6080:6080\"` in `docker-compose.yml`\n\n**Search finds 0 jobs**\n- The agent rotates through search queries randomly\n- Check `data/profile.md` scoring criteria — threshold is \u003e= 4\n\n## Background Processes\n\nThe daemon runs several background tasks:\n\n| Process | Interval | Purpose |\n|---------|----------|---------|\n| Job search | Every N min, 8:00-23:00 | Find new jobs |\n| Keepalive | Every 10 min | Reload page to prevent session expiry |\n| Heartbeat | Every 6 hours | Send \"alive\" ping to Telegram |\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcitizenweb3%2Fupwork-agent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcitizenweb3%2Fupwork-agent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcitizenweb3%2Fupwork-agent/lists"}