This morning a browser script of mine announced 88 new scrapers in a Discord channel. The log said 88 posted. The channel showed one.
Discord had silently dropped the other 87. The script was sending every 2.5 seconds, each send looked fine from the outside, and nothing raised an error. Slowing to 7 seconds between sends fixed the delivery. The more useful lesson was why the log was wrong in the first place.
The script logged the action, not the result. It pressed Enter and wrote "posted". Those are different events, and going back through my posting scripts I found the same mistake three more times.
An HTTP 200 that means no
My X poster used to log success after clicking the post button. X answers its CreateTweet call with HTTP 200 even when it refuses the post, and puts the reason in an errors array. A duplicate comes back as code 187. A refused thread was logged as posted.
The fix was to watch the response and treat only an accepted tweet as success:
page.on('response', async (r) => {
if (!r.url().includes('CreateTweet')) return;
const j = await r.json();
if (j.errors) createErrors.push(j.errors);
else createdIds.push(j?.data?.create_tweet?.tweet_results?.result?.rest_id);
});
If createdIds is empty after the click, nothing was posted, whatever the button appeared to do.
The right action on the wrong element
Discord's search field and its message composer are both editable boxes with role="textbox", and search comes first in the DOM. My selector grabbed search. A whole promo message was typed into it, Enter ran a query, and the page showed "No results". The script thought it had posted.
Now the selector requires the composer's own label, and the script refuses to type if the label does not start with "Message".
A check that could never pass
Tonight I found the opposite bug. After sending, my verifier looked for the raw URL on the page. Discord renders markdown links as their titles, so the URL never appears, and a message that had arrived was reported as uncertain.
A verifier that always fails trains you to ignore it. The check now looks for the first line of the message in the channel, once the composer has emptied.
The parser that lost a tweet
My thread parser expected a --- after each tweet. A file whose last tweet had no terminator lost that tweet, and the thread went out one short with no warning. It now compares how many sections a file declares with how many it parsed, and aborts if they differ.
What I do now
- Log the observed result. "Clicked" and "delivered" get different words.
- Check from the receiving side. A cleared input box only tells you the input is empty.
- Separate three outcomes: confirmed, definitely not sent, and unclear. Only "definitely not sent" is safe to retry. Retrying "unclear" is how you post twice.
- Test the checker against a real success. If it cannot pass, it is not a check.
The bulk Discord script still only paces its sends and does not verify each one, so for that one I count what reached the channel by hand.
When did a green log last lie to you?
I build Scrapemint, a set of data scrapers on Apify, and these scripts handle its announcements. This post was written with help from an AI assistant, based on my own code and logs.
Top comments (0)