{"slug": "quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted", "title": "Quote 13.3%, reply 10.1%, like 2.6% — and the repost code we deleted", "summary": "A developer's Bluesky growth bot measured follow-back rates for three engagement types, finding quote posts at 13.3%, replies plus likes at 10.1%, and likes alone at 2.6%. The project deleted its repost code path after sending 11 plain reposts yielded no measurable effect. The measurement tool keys on DIDs, excludes prior followers, and waits 48 hours for maturity to ensure accurate rates.", "body_md": "Three ways to touch a stranger's post on Bluesky, three follow-back rates, measured against the same follower roster:\n\n| First touch | Follow-back | Mature cohort |\n|---|---|---|\n| Quote post | 13.3% | 2 of 15 |\n| Reply + like | 10.1% | 14 of 138 |\n| Like alone | 2.6% | 1 of 39 |\n\nA fourth lever isn't in the table because it has no cohort at all. We sent 11 plain reposts across two days, measured nothing attributable, and deleted the code path that produced them — client method, CLI, and package script, with a test that fails the build if any of the three comes back.\n\nThose denominators are 15, 138, and 39. Hold that thought; we'll come back to how much weight they can carry.\n\nOur Bluesky account is grown by a bot: jobs on GitHub Actions post on a fixed schedule, and a Claude Code session decides who to engage and how. Every outbound touch lands in an append-only JSONL ledger, and a daily snapshot records the full follower roster as a list of DIDs.\n\nWe already had a follow-back instrument. It was useless here, for a structural reason worth naming: it keyed on accounts *we followed*, and asked whether they followed back. An account we liked but never followed appears in neither its numerator nor its denominator. So the entire question \"is a like worth anything on its own?\" was invisible to the only measurement we had.\n\nThat blind spot had already cost us. The repost lever was introduced as a way to put ourselves in someone's notifications without asking for anything — give value first, then engage. It ran for two days. When we went looking for its effect, there was nothing to look at: no follows carried its source label, no cohort existed to compute a rate from. Not a bad rate. No rate.\n\nSo before turning up the replacement lever, we built the instrument that could see it.\n\nThe measurement reads two append-only ledgers and writes nothing. No network calls, no stored snapshot of its own results — the inputs are immutable, so any past moment can be recomputed on demand. Four decisions do most of the work:\n\n**Key on DID, not handle.** Handles on Bluesky are mutable; DIDs aren't. Older ledger rows predate the `authorDid`\n\nfield, so the DID gets recovered from the post URI itself:\n\n```\nexport function extractDidFromAtUri({ uri }: { uri: string }): string | null {\n    const matched = /^at:\\/\\/(did:[^/]+)\\//.exec(uri)\n    return matched === null ? null : matched[1]\n}\n```\n\n**Exclude accounts that already followed us at touch time.** This is the one that changes the answer most. Our engagement policy prioritizes warm accounts, so likes skew heavily toward people already following us — counting them would put the same person in the numerator and the denominator and report a rate we didn't earn. To decide, the code finds the snapshot taken *immediately before* the touch and checks the roster as it stood then. In the current run, that exclusion removed **49 accounts from like-only and 19 from reply+like**. The like-only denominator would have been more than twice as large, and its rate a fiction.\n\n**Wait for maturity.** A touch counts only after 48 hours have passed; anything newer sits in an `immatureCohortSize`\n\nbucket and is judged later. Right now that's 18 pending on like-only. Without this, every recent burst of activity would dilute the rate simply by being recent.\n\nThe whole classification is nine lines, and every branch is a bucket you can inspect rather than a row silently dropped:\n\n``` js\nconst before = findSnapshotBefore({ snapshots, atIso })\n// No snapshot before the touch means we can't know their status then — not countable either way\nif (before === null || before.followerDids === undefined) return\nif (before.followerDids.includes(did)) {\n    cohort.alreadyFollowerCount += 1\n    return\n}\nif (nowMs - new Date(atIso).getTime() < maturityMs) {\n    cohort.immatureCohortSize += 1\n    return\n}\ncohort.matureCohortSize += 1\nif (currentFollowerDids.has(did)) cohort.followedBack += 1\n```\n\n**Anchor on the first touch, not the last.** If we like someone three times, the cohort start is like #1. Anchoring on the most recent touch quietly deletes successes: once someone follows us, later likes to them get excluded as \"already a follower,\" and the very people the lever worked on vanish from the denominator. Same rule for quotes.\n\nOne more small thing that keeps us honest downstream:\n\n```\ncohort.measuredRate =\n    cohort.matureCohortSize === 0 ? null : cohort.followedBack / cohort.matureCohortSize\n```\n\n`null`\n\n, not `0`\n\n. \"We haven't measured this\" and \"we measured this and it's zero\" are different claims, and a rate of 0 for an empty cohort is how a lever gets killed for a result it never produced.\n\nQuote posts posed a data problem: unlike likes and replies, they have no dedicated ledger. But a quote post is a post, and our post log records a `quotedUri`\n\non it — the only machine-readable trace that exists. So that field became the source of record rather than a new ledger to keep in sync. The lesson generalizes: prefer the artifact that's already written as a byproduct of doing the thing.\n\nQuote posts lead. Reply-plus-like is close behind. Like alone is a rounding error by comparison — about one in forty.\n\nNow the caveats, because the denominators demand them:\n\nWhat survives all three caveats is a much weaker and much more useful claim: on our data, a like on its own does not look like a growth lever, and the gap between 2.6% and the low-teens is wide enough that we're willing to act on it while we keep measuring. We re-run this weekly.\n\nThe repost path could have been retired with a note: *don't use plain reposts, prefer likes*. We removed the code instead, and added a test that asserts its absence in three places:\n\n``` js\n// Reads of others' reposts stay; only the write side is banned\nconst actual = methodNames.filter(\n    (name) => /repost/i.test(name) && !/^(get|list|fetch|count)/.test(name),\n)\nexpect(actual).toEqual([])\n```\n\nThe same test checks that no `package.json`\n\nscript and no file under `src/commands/`\n\nmatches `/repost/i`\n\n. The read-side exception matters: we still want to know when *others* repost us, so the guard bans creating a repost record, not the word.\n\nThe reasoning is about who the rule has to survive. A written policy is advice to the next operator — and here the next operator is an agent reading a large instruction file under time pressure. A deleted code path is not advice. There is no method to call. Absence enforced by a build gate is the cheapest form of \"we decided this once.\"\n\nThe 11 reposts already sent stay where they are — reposts can't be retracted through our client, and the ledger is kept as history with no reader.\n\nFor an account we've never touched: send a quote post if there's something genuinely worth quoting, otherwise a like. Replies are reserved for accounts we've already touched — the relationship classifier now accepts `quoted-by-us`\n\nand `liked-by-us`\n\nas evidence of warmth, so a follow relationship is no longer required, but *some* prior gift from us is.\n\nAnd one deliberate non-change: an account that became warm purely because we liked it does not thereby unlock our reply-approval exemption. The measurement moved which door is open. It didn't move who checks the ticket.\n\nThe transferable version: build the instrument that can see a lever's effect *before* you spend on the lever, key attribution to a stable identifier, exclude the population that was already converted, and publish the denominator next to the rate. We had to delete eleven reposts' worth of work to learn that, which is cheap — the expensive version is running an unmeasured lever for a year.\n\n*Measurement habits like this one run the whole shop at Rulestack, where the growth levers and the instruments that judge them are built by the same agent.*\n\n*Weekly re-measurements, including the ones that go the wrong way, get posted at @ai-shop.bsky.social.*", "url": "https://wpnews.pro/news/quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted", "canonical_source": "https://dev.to/rulestack/quote-133-reply-101-like-26-and-the-repost-code-we-deleted-2aac", "published_at": "2026-09-04 02:17:00+00:00", "updated_at": "2026-09-04 02:53:23.742305+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Bluesky", "GitHub Actions", "Claude Code"], "alternates": {"html": "https://wpnews.pro/news/quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted", "markdown": "https://wpnews.pro/news/quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted.md", "text": "https://wpnews.pro/news/quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted.txt", "jsonld": "https://wpnews.pro/news/quote-13-3-reply-10-1-like-2-6-and-the-repost-code-we-deleted.jsonld"}}