# Eighteen Repos, One Rule: "Not AI Slop"

*Interactive version: [View on web](/articles/farcaster-bounty.html)*


Eighteen repos, one feature list, one week.[^two-out] When everyone builds with coding agents, do the repos come out the same? A [Farcaster client bounty](https://farcaster.xyz/balajis.eth/0x50184b62) listed the features: seed import, an app key, a feed, cast (post), follow and react, an app filter, a microsite and a voucher. Its one rule for the code was "not AI slop", judged at its poster's discretion, and a [later reply](https://farcaster.xyz/balajis.eth/0x0a05d5c8) asked the entries' tutorials to warn about seed phrases. Nothing on tests, CI or dependencies. Most entries kept the seed phrase safe, and most left the secrets beside it exposed.

The contest holds the task fixed, so no two entries differ by solving different problems. Every test, careful key path or pinned dependency was chosen or accepted in a one-week race, with no criterion naming it: quality here is a revealed preference. With no pre-agent set built to one spec, the entries can show whether agent-built repos differ, not whether agents made them more alike than before.

Eight entries are marked, their commits carrying Claude co-author trailers: seven name Claude Opus 5.5, and D names Claude Opus 5. The other ten carry no trailers, but three keep agent instruction files. We entered too, as [paircaster](https://github.com/cyberchitta/paircaster) (H). It syncs its own node, so its first feed takes about a day, the slowest of the eighteen; its tests that read saved node responses are skipped by default, and neither of its workflows runs the tests. [Entries](#entries) maps every letter to its repo.

## What the Entries Shared

Ten entries complete the checklist and the other eight mostly do. Strict types arrived unasked: all 13 TypeScript entries compile under strict mode. The spec asked for seed import and a warning, and 14 of the 18 never save it or the custody key derived from it.

Commits came larger, too, a sign of agent use and the one comparison here with work from before agents. Over the [contest week](#fig-contest-week), the nine entries that committed steadily (on five or more days, no commit adding over a fifth of its lines for the week), five marked and four unmarked, have median commits of 34 to 226 lines changed. Across every commit of the developers profiled in [10x in the AI Age](/articles/10x-in-the-ai-age/), before coding agents were in common use, the median is 17.



**Lines changed per commit, log scale, cut at 20,000; the arrow gives the longest. First, every commit of the 18 developers in 10x in the AI Age, November 2024 to April 2025; then the nine entries that committed steadily, by median. New code against mostly established projects leans the comparison toward larger entry commits.**

| Series | Group | n | Median | 95th percentile | Longest |
|--------|-------|---|--------|-----------------|---------|
| 10x, 2024–25 | 10x, before agents | 27912 | 17 | 1018 | 1735656 |
| G | Unmarked | 153 | 34 | 358 | 1009 |
| B | Marked | 104 | 42 | 666 | 2210 |
| Q | Marked | 176 | 51 | 1205 | 8167 |
| H | Marked | 141 | 74 | 1324 | 3005 |
| S | Marked | 77 | 102 | 921 | 1830 |
| D | Marked | 46 | 104 | 471 | 1642 |
| L | Unmarked | 79 | 125 | 1523 | 14970 |
| O | Unmarked | 52 | 148 | 4580 | 11294 |
| I | Unmarked | 98 | 226 | 1605 | 6263 |

*Interactive chart at: /articles/farcaster-bounty.html*


## Where the Spec Was Silent, the Entries Spread

The seven Opus 5.5 entries include the smallest entry, the fewest dependencies and the most, and both an entry with no tests and one with more test code than product code.

### Counted by Script

**Size and scope.** Code in the parts the bounty asked for (node, CLI, GUI, core) runs from 523 lines to 12,968, a factor of 25. The smallest covers nearly the whole checklist: `@farcaster/hub-nodejs` does its encoding and signing, and it defaults to its author's node (a Farcaster server holding the network's data). Three entries put a fifth or more of their code outside those parts; in one, a second product beside the client, it is more than half. What they built beyond the list differs too, and nothing here scores it.

Each entry's longest function, 54 to 2,791 lines, mostly tracks size, as files and dependencies [do](#fig-size), so it adds little beyond size. The 95th-percentile function, complexity, comments and test-to-product [barely do](#fig-beyond-size): each measures something size does not.

**The parts the bounty asked for (node, CLI, GUI, core): lines of code without comments or markup, files and longest function; direct dependencies count the whole repo. Rank correlation with lines of code: longest function 0.78, files and dependencies 0.66. One dot per entry in each column, each column on its own scale, labelled at its minimum, median and maximum. Hover or tap a dot to trace that entry across. Blue: marked; grey: unmarked.**

| Entry | Group | Lines of code | Files | Direct deps | Longest function |
|-------|-------|---------------|-------|-------------|------------------|
| A | Unmarked | 2258 | 29 | 9 | 200 |
| B | Marked | 4228 | 28 | 9 | 95 |
| C | Marked | 1554 | 36 | 5 | 68 |
| D | Marked | 2618 | 15 | 5 | 206 |
| E | Unmarked | 1692 | 19 | 8 | 111 |
| F | Marked | 523 | 10 | 2 | 54 |
| G | Unmarked | 3951 | 101 | 20 | 295 |
| H | Marked | 9086 | 67 | 32 | 328 |
| I | Unmarked | 12968 | 42 | 20 | 2791 |
| J | Unmarked | 1966 | 31 | 16 | 134 |
| K | Unmarked | 7810 | 72 | 19 | 445 |
| L | Unmarked | 6478 | 46 | 11 | 211 |
| M | Marked | 5973 | 51 | 6 | 197 |
| N | Unmarked | 3294 | 27 | 15 | 74 |
| O | Unmarked | 6209 | 35 | 31 | 335 |
| Q | Marked | 6950 | 64 | 18 | 411 |
| R | Unmarked | 2806 | 25 | 5 | 312 |
| S | Marked | 4839 | 21 | 3 | 134 |

*Interactive chart at: /articles/farcaster-bounty.html*


**Functions.** With one-line inline callbacks left out, each entry's [median function](#fig-function-length) runs 4 to 12 lines, and its 95th percentile 22 to 129.

**Each line is one entry: the share of its functions in the parts the bounty asked for (node, CLI, GUI, core) at or below each length. Anonymous one-line functions (inline callbacks) left out. Blue: marked (8); grey: unmarked (10).**

| Series | Group | n | Median | 95th percentile | Longest |
|--------|-------|---|--------|-----------------|---------|
| A | Unmarked | 139 | 7 | 26 | 200 |
| B | Marked | 301 | 6 | 33 | 95 |
| C | Marked | 174 | 4 | 22 | 68 |
| D | Marked | 108 | 8 | 39 | 206 |
| E | Unmarked | 120 | 5 | 35 | 111 |
| F | Marked | 28 | 6 | 25 | 54 |
| G | Unmarked | 455 | 6 | 29 | 295 |
| H | Marked | 666 | 7 | 25 | 328 |
| I | Unmarked | 358 | 12 | 129 | 2791 |
| J | Unmarked | 164 | 5 | 41 | 134 |
| K | Unmarked | 549 | 7 | 36 | 445 |
| L | Unmarked | 457 | 8 | 41 | 211 |
| M | Marked | 444 | 6 | 37 | 197 |
| N | Unmarked | 194 | 5 | 24 | 74 |
| O | Unmarked | 137 | 10 | 82 | 335 |
| Q | Marked | 651 | 6 | 32 | 411 |
| R | Unmarked | 149 | 9 | 70 | 312 |
| S | Marked | 380 | 6 | 32 | 134 |

*Interactive chart at: /articles/farcaster-bounty.html*


Five entries, four of them marked, use the same name for the same step, `submitAndConfirm`: send a signed message to the node, then wait until the node shows it. None of their repos takes the name from another source, and the code under it differs. B's is 10 lines and sends once. The waiting is `waitFor`, a 12-line helper in another file. If the message never shows, B returns it marked unconfirmed.

```ts
async function submitAndConfirm(hub: Hub, msg: Message, probe: () => Promise<HubMessage | true | undefined>, wait?: Wait): Promise<WriteResult> {
  await hub.submit(msg);
  const hash = hashOf(msg);
  const seen = await waitFor(async () => {
    const r = await probe();
    if (r === true) return true;
    return r && r.hash.toLowerCase() === hash ? true : undefined;
  }, wait);
  return { hash, confirmed: !!seen };
}

export async function waitFor<T>(
  probe: () => Promise<T | undefined>,
  { timeoutMs = 30_000, intervalMs = 1_500 }: { timeoutMs?: number; intervalMs?: number } = {},
): Promise<T | undefined> {
  const end = Date.now() + timeoutMs;
  for (;;) {
    const v = await probe();
    if (v !== undefined) return v;
    if (Date.now() >= end) return undefined;
    await new Promise((r) => setTimeout(r, intervalMs));
  }
}
```

C's is 24 lines and scores 20 on cognitive complexity (a count of branches and loops, weighted by how deeply they nest), the most of the five. It waits inline and sends up to three times. When the node refuses a send, the one-line `isDuplicate` reads whether the refusal means the node already has it; if not, on a resend C checks whether the first copy landed in the meantime.

```ts
const isDuplicate = (e: NodeError) => /duplicate|already (been )?merged|already exists/i.test(e.message);

export async function submitAndConfirm(node: NodeClient, w: Write,
  o: WriteOpts = {}): Promise<{ hash: Hex; tries: number }> {
  const tries = o.tries ?? 3, waitMs = o.waitMs ?? 15_000, pollMs = o.pollMs ?? 3_000;
  const sleep = o.sleep ?? ((ms) => new Promise<void>((r) => setTimeout(r, ms)));
  const bytes = Message.encode(w.message).finish();
  // @snippet submit-confirm
  for (let t = 1; t <= tries; t++) {
    try { await node.submit(bytes); }
    catch (e) {
      if (!(e instanceof NodeError) || e.status >= 500) throw e;
      // On a resubmit, a refusal can just mean the first copy landed meanwhile.
      if (!isDuplicate(e)) {
        if (t > 1 && await w.landed(node)) return { hash: hashOf(w.message), tries: t };
        throw new Error(`Your node rejected the ${w.what}: ${e.message}`);
      }
    }
    for (let waited = 0; waited <= waitMs; waited += pollMs) {
      if (await w.landed(node)) return { hash: hashOf(w.message), tries: t };
      await sleep(pollMs);
    }
  }
  // @end
  throw new Error(`Sent the ${w.what} ${tries} times. Your node never showed it. Is it synced? Run: runfcx status`);
}
```

With their helpers, B's runs 22 lines and C's 25. Here length says how the work was split, and complexity how many failures the code handles. The spec asked for neither.

**Tests.** Test code runs from none to 11,575 lines; two entries have more of it than product code. Eight of the 18 have a test touching every part they ship. Node setup scripts go untested in 9 of the 18 entries, command-line tools in 5 of the 10 that ship one, GUIs in 2 of 17 and standalone cores in none of 8. Seven run their tests in CI; the rest may run them locally, where nobody else can watch them pass.

**None of these tracks size closely: rank correlations with lines of code run −0.13 to 0.34. The bounty's parts: the share of functions over cognitive complexity 15, comment lines as a share of code and comments, and test lines per product line. The 95th-percentile function leaves out anonymous one-line functions. Hover or tap a dot to trace that entry across. Blue: marked; grey: unmarked.**

| Entry | Group | % over complexity 15 | 95th-pct function | % comment lines | Test-to-product |
|-------|-------|----------------------|-------------------|-----------------|-----------------|
| A | Unmarked | 1.7 | 26 | 11.5 | 0.07 |
| B | Marked | 1.4 | 33 | 10.3 | 0.52 |
| C | Marked | 1.6 | 22 | 7.4 | 1.01 |
| D | Marked | 0.6 | 39 | 21.7 | 0.08 |
| E | Unmarked | 3.1 | 35 | 11.9 | 0.3 |
| F | Marked | 2.2 | 25 | 15.9 | 0 |
| G | Unmarked | 2.7 | 29 | 1.2 | 0.55 |
| H | Marked | 0.1 | 25 | 15.7 | 0.28 |
| I | Unmarked | 8.8 | 129 | 6.6 | 0.02 |
| J | Unmarked | 3.9 | 41 | 2.1 | 0.37 |
| K | Unmarked | 0.9 | 36 | 11.7 | 1.48 |
| L | Unmarked | 6.6 | 41 | 1.2 | 0.19 |
| M | Marked | 2.6 | 37 | 10.3 | 0.16 |
| N | Unmarked | 1 | 24 | 16.5 | 0.58 |
| O | Unmarked | 1.4 | 82 | 0.6 | 0.39 |
| Q | Marked | 2.2 | 32 | 13.7 | 0.03 |
| R | Unmarked | 1.1 | 70 | 1 | 0.03 |
| S | Marked | 2.8 | 32 | 15.4 | 0.45 |

*Interactive chart at: /articles/farcaster-bounty.html*


**Structure.** When two files import each other, directly or through others, neither can be read, tested or replaced without the other: they are one module split across files. Six entries have such a loop. One has two, the other five one each, and no loop spans more than three files. The reverse is several modules in one file. In one entry, three-quarters of the code sits in a single file, where every name can reach every other; in the next, two-fifths; in the other sixteen, a third or less.

### Judged by Readers

Blind readers, Claude Opus 5.5 subagents, rated each entry against a rubric, one level per column; the five columns the spec left open were rated twice.

**One row per entry, marked first. Left of the rule, what the spec spoke to; right of it, what it left open. Each column runs best to worst: Complete, Mostly; Separated, Custody stored (the seed or custody key is saved); Careful, Ordinary, Exposed; Parsed, Gaps, Unchecked; Independent, Mixed, Circular, None; Matches, Drift, Overclaims; Pinned, Floating, Piped. Green is the best level, red the worst. The 14 of 90 calls the two readers split on were settled against the code.**

| Entry | Group    | Checklist | Seed and custody key | Other secrets | Boundaries | Test values | Docs       | Supply chain |
| ----- | -------- | --------- | -------------------- | ------------- | ---------- | ----------- | ---------- | ------------ |
| B     | Marked   | Complete  | Separated            | Exposed       | Gaps       | Mixed       | Drift      | Floating     |
| C     | Marked   | Complete  | Separated            | Exposed       | Gaps       | Circular    | Drift      | Piped        |
| D     | Marked   | Mostly    | Custody stored       | Exposed       | Unchecked  | Mixed       | Drift      | Piped        |
| F     | Marked   | Mostly    | Separated            | Ordinary      | Unchecked  | None        | Matches    | Piped        |
| H     | Marked   | Complete  | Separated            | Ordinary      | Gaps       | Mixed       | Drift      | Piped        |
| M     | Marked   | Mostly    | Separated            | Exposed       | Gaps       | Mixed       | Drift      | Piped        |
| Q     | Marked   | Complete  | Separated            | Ordinary      | Gaps       | Mixed       | Drift      | Piped        |
| S     | Marked   | Complete  | Separated            | Ordinary      | Gaps       | Mixed       | Drift      | Piped        |
| A     | Unmarked | Complete  | Separated            | Exposed       | Gaps       | Mixed       | Drift      | Piped        |
| E     | Unmarked | Mostly    | Separated            | Exposed       | Unchecked  | Mixed       | Drift      | Piped        |
| G     | Unmarked | Complete  | Separated            | Exposed       | Gaps       | Mixed       | Drift      | Floating     |
| I     | Unmarked | Mostly    | Separated            | Exposed       | Unchecked  | Circular    | Drift      | Piped        |
| J     | Unmarked | Complete  | Custody stored       | Ordinary      | Gaps       | Circular    | Overclaims | Floating     |
| K     | Unmarked | Complete  | Separated            | Exposed       | Gaps       | Mixed       | Drift      | Piped        |
| L     | Unmarked | Complete  | Separated            | Ordinary      | Gaps       | Circular    | Overclaims | Piped        |
| N     | Unmarked | Mostly    | Separated            | Exposed       | Gaps       | Mixed       | Matches    | Piped        |
| O     | Unmarked | Mostly    | Custody stored       | Exposed       | Gaps       | Circular    | Drift      | Floating     |
| R     | Unmarked | Mostly    | Custody stored       | Exposed       | Unchecked  | Independent | Overclaims | Piped        |


**Marked and unmarked.** Fewer marked entries expose other secrets, take test values from the code or overclaim in docs, by a few entries each: 4 of 8 exposed on other secrets against 8 of 10, 1 of 8 against 4 of 10 on test values, none of 8 overclaiming against 3 of 10. The ends of test values split: the one entry without tests is marked, the one with independent values is not. On the supply chain the unmarked do better: 3 of 10 float on unpinned versions rather than pipe an installer, against 1 of 8.

**The other secrets.** The seed phrase got care; the secrets beside it, in the same entries, did not. An app key sits in browser storage, a token rides on the command line, a credential sits in a plain `.env`. No entry rates careful overall: 12 rate exposed and 6 ordinary, each time on something other than the seed.

**Boundaries.** A type written on data from outside is a promise nobody checks: `as T` tells the compiler to believe it, and a wrong field then fails far from where it came in, or not at all. Seventeen entries do this with what their node returns; only ours, H, parses it, in a Rust core. With the node trusted almost everywhere, each entry's level turns on the rest: what users and config send. Thirteen check most of it and leave named gaps. In five, request bodies, saved state or config arrive as they are too, and failures are swallowed: one reads a corrupt key file as no key at all. No entry parses everything.

**Test values.** A test whose expected values come from the code under test passes whatever that code does. In 1 entry the expected values are mostly worked out apart from the code, in 11 it is mixed, in 5 they come from the code itself (circular), and 1 has no tests.

**Docs and dependencies.** The docs match the code in 2 entries, drift in 13, and in 3 claim what the code doesn't do. Out of the box, every entry contacts servers the user never chose (the author's, the protocol's or a third party's), and only F's docs say which. No entry pins its whole supply chain. All but 4 pipe an install script straight into a shell, mostly the node software's own installer fetched from a live branch rather than a release. The other 4 float on unpinned versions.

The bounty set one rule for the code. Eighteen entries, seven of them on one model, came in every size and shape.

## Entries

Each entry's letter, its repo, and the commit measured: the default branch as it stood at the deadline.

| Entry | Repo                                                                                                            | Group    | Commit                                                                                                                   |
| ----- | --------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| A     | [sats100m/farcaster-in-3](https://github.com/sats100m/farcaster-in-3)                                           | Unmarked | [c28f733](https://github.com/sats100m/farcaster-in-3/tree/c28f73354cffc792015e2eb88647c51aa3f92219)                      |
| B     | [Spider333/outpost](https://github.com/Spider333/outpost)                                                       | Marked   | [4c3dd4f](https://github.com/Spider333/outpost/tree/4c3dd4f227150e7e7e7bf84e8827a0e8f09a2a89)                            |
| C     | [AntonPecherkin/runfcx](https://github.com/AntonPecherkin/runfcx)                                               | Marked   | [996c64a](https://github.com/AntonPecherkin/runfcx/tree/996c64acb7151da4fe0725b89ae62bf0a0f2add3)                        |
| D     | [webthethird/snapstart](https://github.com/webthethird/snapstart)                                               | Marked   | [89b12e1](https://github.com/webthethird/snapstart/tree/89b12e1aaf01bc5b1b29303d0f5ccb415dd2b875)                        |
| E     | [townhall-fc/townhall](https://github.com/townhall-fc/townhall)                                                 | Unmarked | [87f6f80](https://github.com/townhall-fc/townhall/tree/87f6f80ade0980610f2a7ee6a4707681d6109ba1)                         |
| F     | [prodabhi/castyard](https://github.com/prodabhi/castyard)                                                       | Marked   | [5a7cf1c](https://github.com/prodabhi/castyard/tree/5a7cf1c88541adf3f9f2e05da86ee283f841845b)                            |
| G     | [sah-ban/kite](https://github.com/sah-ban/kite)                                                                 | Unmarked | [52fac73](https://github.com/sah-ban/kite/tree/52fac7346a960050aace6df12e2a7fdc4fb11b6c)                                 |
| H     | [cyberchitta/paircaster](https://github.com/cyberchitta/paircaster)                                             | Marked   | [ce39c40](https://github.com/cyberchitta/paircaster/tree/ce39c403f529ba13e2627f6b8898ec6717997207)                       |
| I     | [mohammadma98/farcaster-snapchain-node-client](https://github.com/mohammadma98/farcaster-snapchain-node-client) | Unmarked | [a671279](https://github.com/mohammadma98/farcaster-snapchain-node-client/tree/a6712799e368f8e1792d4335252196c5da32aa5e) |
| J     | [vrajdesai78/farcaster-bounty](https://github.com/vrajdesai78/farcaster-bounty)                                 | Unmarked | [66d826d](https://github.com/vrajdesai78/farcaster-bounty/tree/66d826dbd585f221c5c78be0709a5398b6b9d673)                 |
| K     | [castyfun/casty](https://github.com/castyfun/casty)                                                             | Unmarked | [7e3ca21](https://github.com/castyfun/casty/tree/7e3ca21f2116fed4444c8e7919759c1d195bf864)                               |
| L     | [wlplease/castonaut](https://github.com/wlplease/castonaut)                                                     | Unmarked | [02281b6](https://github.com/wlplease/castonaut/tree/02281b66fd16fc257e03089a900a949a428f408c)                           |
| M     | [jpfraneto/fatline](https://github.com/jpfraneto/fatline)                                                       | Marked   | [900233e](https://github.com/jpfraneto/fatline/tree/900233e8683cb4cf05e44de93ee80dc76ea03eb6)                            |
| N     | [sudharshanx/runfarcaster](https://github.com/sudharshanx/runfarcaster)                                         | Unmarked | [1aa6c7b](https://github.com/sudharshanx/runfarcaster/tree/1aa6c7ba873ba016e8ee94d8dbed6a8c9adebeb0)                     |
| O     | [P-U-C/farcaster-in-three](https://github.com/P-U-C/farcaster-in-three)                                         | Unmarked | [738e9b3](https://github.com/P-U-C/farcaster-in-three/tree/738e9b3e2fd97301246508ed470d31a0bf3de1af)                     |
| Q     | [eucalyptus-viminalis/create-fc](https://github.com/eucalyptus-viminalis/create-fc)                             | Marked   | [397d61f](https://github.com/eucalyptus-viminalis/create-fc/tree/397d61f3d5b3637860f5e25f7598f26b51a7573a)               |
| R     | [vaibhavmule/readnode](https://github.com/vaibhavmule/readnode)                                                 | Unmarked | [564f3bf](https://github.com/vaibhavmule/readnode/tree/564f3bfe9596e670155a47bc2bdf1f3e2336fd56)                         |
| S     | [Ramitphi/phunk-client](https://github.com/Ramitphi/phunk-client)                                               | Marked   | [cc49779](https://github.com/Ramitphi/phunk-client/tree/cc4977973891bb3a0778e62629855415f0a433f9)                        |

[^two-out]: Twenty entries had public repos. Two are left out of every count (letters P and T): one does not build the bounty's features, and one started from a fork of an existing full client, so its code measures that client as much as the week's work.

## Credits

Repo anatomy and commit archaeology by @claude-opus-5.5. Written by @claude-opus-5.5. Reviewed by @claude-opus-5.5, @grok-4.7 and @gemini-4-argon.

Showrunner: @restlessronin.
