You are browsing as a guest. Sign up (or log in) to start making projects!

XenoDeal

  • 3 Devlogs
  • 11 Total hours

A WhatsApp Buy, Sell, & Trade group Scraper to try and find good deals on stuff, easily and quickly, to try and profit.

Open comments for this post

3h 39m 55s logged

Devlog 3: XenoDeal | The Bots Assemble

June 23

At LAST Phase 2 is done. Alerts are firing, deals are in the works, and the Telegram bot is notifying me about jerseys and iPhones. So, I decided to step things up a notch by starting Phase 3, which adds multi-user support. It was (again) buns.


The Great Architecture Debate

After I completed Phase 2, the following question arose: how can i make this shippable? Right now, XenoDeal is just a self hosted, single user, lightweight app. It connects to one WhatsApp number and one Telegram chat. That’s not something anyone can just… test.

My first solution was to set up multi-client/user support. One wwebjs Client per user, managed by a clientPool.js session manager. Everyone can make themselves an instance, have their own group selection, while not having to worry about hosting.

The good news is that the classifier, deals table, and alert system remain unchanged. The only required adjustment was in the WhatsApp layer: a Map<chatId, Client> instead of one hardcoded client, with a spawnClient(chatId) function. Every event handler references the user’s chatId via closure to know which subscriber it belongs to. No global state, no complex routing logic. Clean.

The new onboarding flow is as follows:

  1. User DMs /start
  2. Bot asks for their WhatsApp number
  3. They reply with it
  4. spawnClient(chatId, phone) is called
  5. wwebjs emits qr, we call requestPairingCode(phone) instead of sending a QR image
  6. User receives a code like ABCD-1234, enters it in WhatsApp → Linked Devices
  7. Connected ✅

No QR image needed, no ENV finagling, nothing. Just a string to type in, which is MILES better UX for a bot.

New tables added: subscribers, user_groups. New column: messages.subscriber_id. New files: clientPool.js, state.js, commands.js. Old single-client init: deleted.


Bugzilla: The Sequel (Speed Round)

Here’s another speed round of the issues I faced today; roughly the same amount as yesterday:

1. “Houston! We have an IPv6 Problem…”

  • Problem: bot.launch() was timing out during the getMe handshake with Telegram at random. Worked sometimes, failed other times.
  • **Fix:**Added require('dns').setDefaultResultOrder('ipv4first') on index.js, plus a keepAlive HTTPS agent on the Telegraf instance.

2. “Silent Treatment”

  • Problem: Bot received /start, asked for phone number, user replied… and you get ghosted.
  • Fix: One missing String() cast in /start. Took way too long to find.

3. “The Ghost of Chromium Past”

  • Problem: Previous test run left a Chromium process hanging. Next launch: “The browser is already running for auth_data/session-7145622406.”
  • Fix: pkill -f chromium + delete the session folder.

4. “t”

  • Problem: requestPairingCode threw a single-letter minified error: t. No stack trace, no message. Just… t.
  • Fix: Drop the alpha version, and pin the stable 2.3000.1017054665 instead.

5. “The Error in the Error Handler’s House”

  • Problem: After bug 4 threw, the catch block tried to DM the user an error message. That also timed out, with the SAME cold IPv6 issue, firing during Chromium’s initial page load which was already hammering the event loop.
  • Fix: Wrapped the notification send in its own try/catch. A failed notification shouldn’t throw an unhandled rejection on top of the original error.

What’s Next

  • Finish commands: /addgroup, /threshold, /categories, /status, /deals
  • restoreAllSessions(): reconnect everyone on process restart
  • messageHandler.js: wire live capture per subscriber
  • sendDealAlert(): fan-out to all matching subscribers
  • Sold detection
  • Dashboard post-ship
  • First public ship

“We keep going.” — Me, after every bug today

0

Loading discussion…

0
1
Open comments for this post

4h 57m 16s logged

Devlog 2: XenoDeal | The AI Turns On (And Starts Eating Tokens)

June 22

Since the previous devlog (sorry for missing yesterday), I have gotten the AI System Implemented, and Filtering in order. WhatsApp has caused MORE problems, and luckily I fixed them. (But not the Logout Crash) Besides that, the day (and a half) were easy.

Classifier And Telegram

The Main, MAJOR piece of this devlog and the changes I’ve made since devlog one is src/cron/classifier.js. It’s a background process that does four main things:

  1. Grabs Unprocessed Messages from the DB in Batches
  2. Runs a separate pre-filter file, that removes Obvious Noise and Junk from the Batch
  3. Sends Candidates to AI Provider for full classification
  4. Writes the result to the new deals table and marks the message as processed

Roughly ~150 Messages are still not processed, from my testing batch, gathered from Phase 1, and it is still expanding. This is not just the most important part of the project, it’s the… orchestrator if you will.

The batch size is dynamic, and scales every tick based on backlog and load. This is mainly important during boot after downtime as WWJS just replays missed messages as Live Events, causing a MENTAL wave of messages, and this allows the backlog to finish faster.

async function getBatchSize() {
    const r = await pool.query(
        'SELECT COUNT(*) FROM messages WHERE processed = false'
    );
    const pending = parseInt(r.rows[0].count);
    if (pending > 100) return MAX_BATCH;  // 100
    if (pending > 20)  return 50;
    return MIN_BATCH;  // 20
}

The one architectural decision I have made is to DECOUPLE the poller and message create handlers. They can run without the other, and they work at their own pace, so live messages don’t get blocked from inserting, thus less profit.

For Telegram, I just added a threshold for me to get notified about a listing. If it’s score is above 65, it will send me all the details of the listing, the group name, and the original message. An example is attached as an image.


Bugzilla

Mmmm, my favorite flavor of coding! (No it isn’t) I have a bunch of these (still couldn’t list a, so how about a speed round?:

1.) “Temperature Card”

  • Problem: Same listing was scoring differently
  • Fix: Set Temperature to 0.2

2.) “Sorry, I didn’t understand, please try rephrasing that”

  • Problem: The AI System thought that it was looking for deals for the End Buyer + Hallucinating Good Condition without Evidence
  • Fix: Rewrite Prompt to be more detailed

3.) “Lid, a Big Lid with no Phone # in it”

  • Problem: Whatsapp returns a lid ID instead of a number
  • Fix: Used an approach using client.pupPage and window.require

4.) “Have you seen Chromium anywhere?”

  • Problem: WhatsApp updated and the injection of WWJS made Chromium “Context Crash”
  • Fix: Pin a specific version of WhatsApp

What’s Next

Alright, so next on the agenda, the Dashboard has been pushed back until after the first ship, and as phase 2 has been complete, now phase three is the following:

  • Train and Implement ML for filtering and deals
  • Add Multi-User Support
  • Add Setup Flow to Telegram
  • Add /cmds to Telegram
  • Polish for First Ship

“Anotha One!” - DJ Khaled

2

Loading discussion…

0
16
Open comments for this post

2h 28m 4s logged

Devlog: XenoDeal | The Listener Wakes Up

June 20

Today was a bit hard (it’s the first day on this project, and I’m catching strays breh). I started with:

  1. Planning out the infra
  2. Making the project base

The stack for this one is: PostgreSQL, Node.js, Telegraf, and wwebjs.

I ended up building out the basis for the Telegram, PostgreSQL, and WhatsApp wrappers. Telegram and Postgres were easy, being a few lines each, for now. WhatsApp, being the main part of the app, wanted to give me problems. Four total.

Issue 1: Puppeteer ghosting me. Setting up the project, making sure init and env loading worked across all files, Node kept complaining about

“Puppeteer Missing.” I’d just installed it, so I assumed it was fine. Turns out pnpm takes security seriously and silently blocks Puppeteer’s install script by default. To make it worse, I fat-fingered Enter instead of Space on the approve-builds screen, so even the prompt to fix it slipped past me. Couldn’t figure out how to re-trigger the script, so I just pointed it at my system Chromium instead:

const client = new Client({
    puppeteer: {
        executablePath: '/usr/bin/chromium',
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    },
    authStrategy: new LocalAuth({
        dataPath: 'auth_data'
    })
});

Issue 2: The Logout Crash. I wanted to check that client.on('disconnected') actually fires correctly, so I tested it the easy way: logged out the linked WhatsApp session from my phone. Instead of a clean disconnect, client.on('ready') fired three times, then it crashed:

Failed to add page binding with name onQRChangedEvent: window['onQRChangedEvent'] already exists!

Best guess: wwebjs trying to inject its page bindings into a page that already had them. Possibly a puppeteer-core@24.38.0 version mismatch against what whatsapp-web.js@1.34.7 actually expects internally, but I’m not fully sure that’s the real cause yet. Problem for another day.

Issue 3: The Silent Hang. Whilst testing the LocalAuth implementation, I Ctrl+C’d the process without realizing Chromium leaves behind a SingletonLock file. Next launch just hung indefinitely; no warning, no error, nothing. Found that out the hard way. I ended up clearing the stale lock and added this so it can’t happen again:

process.on('SIGINT', async () => {
    await client.destroy();
    process.exit(0);
});

tl;dr: It lets Chromium clean up after itself on every manual stop, instead of leaving a corpse behind for next time.

Issue 4: the Date/Time Problem. Setting up message logging, I converted msg.timestamp (Unix seconds) to milliseconds for Postgres with msg.timestamp * 1000. It wasn’t enough. Every insert failed with:

date/time field value out of range: "1782000093000"

Turns out pg doesn’t know what to do with a raw JS number where it expects a date, and just stringifies it and Postgres rejects the result outright. Had to wrap it in an actual Date object:

new Date(msg.timestamp * 1000)

(Need to double check: I remember it still not behaving right away even after this fix, possibly just the client needing a moment to fully reconnect after restart rather than the date fix itself being incomplete. Worth confirming next time it comes up.)

What’s Next

So… reads design docs, according to this, I’ll be establishing Core Intelligence next. Second most exciting part of the whole build. Covers:

  • Claude integration (analyze each listing, provide a ranking)
  • Sold tracking
  • The dashboard

See y’all later!

(PS: It did end up working)

0

Loading discussion…

0
2

Followers

Loading…