Projects

Noon Deals Bot

2026·5 min read

Python
Telegram Bot
Web Scraping
GitHub Actions
Telegram (opens in a new tab)GitHub (opens in a new tab)

Channel: t.me/noon_hot_deals · Site: ديلز مصر — vette1123.github.io/noon-deals-bot

Noon Deals Bot preview
On this page8 sections
  • How it works
  • The static site
    • Getting indexed
  • Project layout
  • Local setup
  • Required secrets (GitHub Actions)
  • Running tests
  • Schedule

Channel: t.me/noon_hot_deals · Site: ديلز مصر — vette1123.github.io/noon-deals-bot

Auto-posts the best discounted products from noon.com Egypt to a Telegram channel, 6× a day. It is free to run and needs no login.

  • Scrapes noon's deal pages using curl_cffi (Chrome TLS impersonation, no paid API)
  • Attaches your influencer coupon code to every post as tap-to-copy text
  • Posts Arabic-formatted product cards with images to Telegram
  • Republishes the same deals as a static Arabic site on GitHub Pages so search traffic earns too
  • Optionally crossposts to a Facebook Page
  • Runs on GitHub Actions — no server required

How it works#

GitHub Actions (cron, 6×/day)
        │
        ▼
   fetch 6 category feeds  ── curl_cffi + Chrome fingerprint ──▶  noon.com
        │                     (rotating ~34 categories × 10 pages)
        ▼
   filter, then rank by expected commission in EGP
        │
        ▼
   post to Telegram channel with:
     • product image
     • Arabic-formatted card
     • tap-to-copy coupon code (e.g. gado)
     • "Buy now" button → noon.com product URL + affiliate UTMs
        │
        ▼
   commit updated state.json / posted.json / deals.json
        │
        ├──▶  crosspost to a Facebook Page (only if secrets are set)
        │
        ▼
   rebuild the static site from deals.json ──▶ GitHub Pages

Attribution model: two channels, both message-side, neither needs a login. Every "Buy now" link carries the affiliate campaign UTMs, and the message body shows a tap-to-copy coupon code that users paste at noon's checkout.

What gets posted: at least 25% off, at least EGP 150, not rated below 3.5 by 20+ buyers, and not unbranded-and-unreviewed filler. Survivors are ranked by expected commission in pounds — min(cap, category rate × price) times conversion odds — deduplicated by name, capped at 2 per seller. The top 12 go to Telegram; the top 20 are archived for the site.

Rates are per category and payout is capped at AED 50 per item, which makes expensive electronics (laptops 3%, mobiles 2%) the worst thing on the board and mid-priced beauty, toys and small appliances (8%) the best. See docs/MONETIZATION.md for the full table.

Feed cursor: noon's category browse paths are real product lists; each run walks 6 of them and advances through a rotation of ~34 categories × 10 pages (~14,000 products, roughly a 10-day cycle). A SKU already posted is on cooldown for 21 days.

The static site#

Every deal is archived to deals.json and rendered into an Arabic (RTL) site: a front page, one page per deal with Product structured data, a hub per category and per brand, a directory of each, a paginated archive, sitemap.xml, robots.txt and an RSS feed. The pages use no web fonts, CSS files or script bundles. Search traffic arrives on mobile data, and page speed affects ranking. It follows the device's light/dark setting, with a toggle that remembers an explicit choice.

It earns without an audience: a Telegram post reaches today's subscribers, a page that ranks reaches everyone who searches.

It follows two rules:

  • Deal pages never 404. A deal that ages out becomes a noindex tombstone linking on to its hubs, because a URL that took four months to rank is the one thing here that cannot be rebuilt.
  • priceValidUntil comes from when the deal was seen, not from the build, so a rebuild never re-certifies an old price as current.

Enable it once under Settings → Pages → Source: GitHub Actions; the publish-site job does the rest. To move it onto your own domain, set the SITE_DOMAIN repository variable and point the DNS at Pages — worth doing before it ranks, not after.

Getting indexed#

Every build re-emits two ownership files at the site root, and every deploy announces what changed:

  • Google Search Console: google<token>.html, served verbatim. Verify the property once, then submit sitemap.xml. A fork points GOOGLE_SITE_VERIFICATION at its own token.
  • IndexNow: <key>.txt plus a POST to api.indexnow.org after the deploy, listing only the pages that run changed: the front page, the new deal pages and the hubs they landed on. Bing, Yandex and Seznam then crawl in minutes instead of weeks. Google does not participate — there, sitemap.xml + lastmod is still the lever. The key is public by design (serving it is the proof of ownership), so it belongs in INDEXNOW_KEY as a variable, never a secret.

Neither can fail a run: a malformed token is dropped with a warning, and a rejected ping is logged and ignored.

Preview it locally:

python site_builder.py && python -m http.server -d public 8000

See docs/MONETIZATION.md for what earns, what is built, and what still needs a human.

Project layout#

File Purpose
main.py Entry point — orchestrates fetch → filter → post
scraper.py Fetches & parses noon.com catalog pages (inline JS payload + fallbacks)
filters.py Filters out already-posted SKUs, weak discounts and filler; ranks by expected commission
categories.py noon category codes, their commission rates, and their Arabic names
telegram_poster.py Formats & posts product cards to Telegram (includes coupon line)
archive.py Records deals into deals.json (1-year window, then tombstoned)
site_builder.py Renders deals.json into the static site in public/
facebook_poster.py Optional Facebook Page crosspost (no-op without secrets)
indexnow.py Announces changed URLs to Bing/Yandex/Seznam after each deploy
posted.json {sku: ISO timestamp} — per-SKU repost cooldown
state.json {"next_task": N} — cursor into the category-feed rotation
deals.json The published-deal archive the site is built from

Local setup#

pip install -r requirements.txt
cp .env.example .env  # fill in values
python main.py --dry-run   # prints what would be posted, skips Telegram
python main.py             # real run

noon.com sits behind Akamai, which can challenge cold requests, so a local run may fail at the fetch step with Akamai JS challenge. In CI the bot egresses through Cloudflare WARP, which avoids this. To debug parsing locally, save the page HTML from a real browser and feed it to scraper.parse_products_from_html.

Required secrets (GitHub Actions)#

Secret Purpose
TELEGRAM_BOT_TOKEN Bot that posts to the channel
TELEGRAM_CHANNEL_ID Channel handle (e.g. @noon_hot_deals)
NOON_COUPON_CODE (optional) Your noon influencer coupon. Defaults to gado.
FACEBOOK_PAGE_ID (optional) Enables the Facebook crosspost. Skipped when unset.
FACEBOOK_PAGE_TOKEN (optional) Long-lived Page token with pages_manage_posts.

Only the two Telegram secrets are required. There is no scraping API key, noon.partners login, session rotation or OTP flow.

Running tests#

pip install -r requirements-dev.txt
pytest

Schedule#

Cron 0 4,8,12,16,20,0 * * * UTC → 6×/day (every 4 hours). Triggered by .github/workflows/bot.yml or manually via workflow_dispatch.

More projects

  • phBot Plugins — Silkroad Online2026 · Python
  • Better Auth Starter2026 · Next.js
  • Social Media Downloader2026 · Next.js
All projects
  • How it works
  • The static site
    • Getting indexed
  • Project layout
  • Local setup
  • Required secrets (GitHub Actions)
  • Running tests
  • Schedule