Andrew Dorokhov

Freelancing / job search · Own product

An Upwork job monitor that keeps the whole feed and lets my filters decide what reaches Telegram

A personal tool on the official Upwork API: a worker copies every new posting into Postgres, my own filters run over that archive, and the matches arrive in Telegram.

new postings processed a day
~3,900
Telegram alerts sent a day
~500
of the daily Upwork API quota in use
< 2%

The challenge

What the client needed

On Upwork a good posting collects proposals fast. A freelancer who opens it hours later competes with everyone who saw it first, and I can't spend the whole day refreshing the feed.

Upwork's saved searches are too coarse for how I choose work. I want to skip some countries and categories, set a minimum rate, check the client's spend, rating and payment status, and decide what happens to the many hourly postings that name no rate at all.

I also wanted history. A filter I write next month should run over everything posted this month, so the tool has to keep every posting, including the ones no filter wanted at the time.

The approach

How I solved it

01

Mirror first, filter later

On every sweep the worker asks the official Upwork GraphQL API for the newest postings with no search criteria and stores each one in Postgres, full API payload included. Results come newest first, so a sweep stops at the first page with nothing new, and a caught-up sweep costs a single request. Filters are plain functions over the stored rows and never call the API.

02

One alert per match, kept until delivered

The worker stores a posting once, keyed by its Upwork id, and announces it at most once per filter. A match Telegram did not accept stays marked as undelivered, so after an outage or a bad token the pending alerts go out with the first sweep that gets through.

03

Careful with the API

Some fields the schema declares required can arrive empty, and under GraphQL rules one such value drops the whole posting from the results. The query reads them from nullable fields, partial responses go to the log while the run carries on, and each row keeps the raw payload, so a renamed field leaves one column empty while the data stays in the payload.

04

Rates that mean what they say

About a third of hourly postings name no rate. A filter with a rate bound skips them unless I tick a box to keep them, and it compares against the top of the client's range: a $20–60 posting clears a $50 minimum, because $50 is inside what the client offered.

05

New rules, old postings

After I change a filter, Re-scan archive runs it over every stored posting on my server and marks the new matches as already announced. The feed and the match counts update straight away.

06

Third version of the idea

The first version dates back to 2018. By 2022 it was a Yii2 app with a polling daemon, a cleaner that kept a week of postings plus starred ones, exclusion filters, one-click search presets and manual 1–10 scores, and its database held 574,606 postings. Gotcha keeps the filters and presets that proved useful and rebuilds the rest on the official API, Postgres and Telegram.

In motion

Highlights

01

The feed, with filters marked

The feed lists every posting in the archive, newest first. Each row puts the client's spend, rating and payment status next to the rate, and green badges show which of my filters picked it up.

The jobs feed scrolling, with the filter sidebar and the green filter badges on matching postings highlighted.
02

A new filter over old postings

Set keywords and categories, press Re-scan archive, and the filter runs over every stored posting on the server. The match count appears straight away, computed from the archive.

Filter editor with keyword fields and the Re-scan archive button highlighted, then the filters list with 36 matches for the new filter.
03

What the market pays

Stats rank skills by the median rate and budget across the archive, and a skill needs enough postings to be ranked at all, so a couple of outliers can't top the list.

Stats page scrolling through hourly and fixed-price tables of skills ranked by median, with a bar for each.

See it in action

The product

The feed: every mirrored posting, with the filters that matched it.
A filter: keywords to require and exclude, categories to include and skip.
Contract and client rules: rates, budgets, experience, client spend, rating and payment status.
One posting with everything the API returns and the filters it matched.
Best-paid skills by median rate and budget over the last 30 days.

The outcome

What the client got

  • Every posting the Upwork search returns reaches the archive with the next sweep, whether or not any filter wants it.
  • About 3,900 new postings a day go into the archive and through every filter with the next sweep after they show up in search.
  • About 500 alerts a day reach Telegram from my filters, and the worker retries any message Telegram turns down.
  • The worker uses less than 2% of the daily request quota Upwork allows.
  • A new filter runs over the whole history in one click, straight from the archive.
  • Stats rank skills by median hourly rate and fixed budget, with a minimum number of postings so a few outliers can't top the list.
  • A Status page shows API usage, Telegram delivery and every sweep.
  • A push to main builds one Docker image for the app, the worker and the migrations, and the deploy keeps the running version up if a migration fails.

Need something similar built?

Send a short brief about your project. Within 24 hours you get an estimate or a clear next step, even if we don't work together.