DEV Community

Cover image for We built a way to fine cheating AI agents. Then a swarm of AI reviewers found how to delete any competitor for $0.
Alex
Alex

Posted on

We built a way to fine cheating AI agents. Then a swarm of AI reviewers found how to delete any competitor for $0.

We run an open marketplace where AI agents pay other AI agents to do work. Agent A needs a translation, calls Agent B, money moves, receipt signed. Standard stuff — until Agent B takes the money and returns garbage.

So, like every payment network before us, we built slashing: a provider posts a stake, and if they cheat, we burn part of it. A deterrent.

Then we did the thing almost nobody does before shipping: we pointed an army of adversarial AI reviewers at our own freshly-written code and told them to break it.

They found a way to evict any competitor from the marketplace and burn their stake — for exactly $0.

We wrote that bug. This is the story of catching it, plus the four design ideas that made slashing usable in a multi-agent world in the first place.


Why "just slash cheaters" falls apart

A slash is a blunt instrument. In a single call it's fine. In a multi-agent pipeline — A calls B calls C, a council votes, a chain of subcontractors — it breaks fast:

  • Slash everyone in a failed pipeline and you punish honest hops for one bad neighbor.
  • Slash hard and a new agent gets zeroed on its first bad day. No room to recover, no room to experiment.
  • Make the trigger cheap and it becomes a weapon: grief a competitor into oblivion.

Our rule became: escrow protects the buyer on this call; slash only prices serial fraud. Everything below is in service of that line.

Four pieces:

1. Blame the hop, not the graph

When a pipeline fails, the signed bill-of-materials now says who failed:

"blame": {
  "policy": "hop-level",
  "at_fault":     { "id": "b", "product_id": "p2", "status_code": 500 },
  "not_at_fault": ["a"],
  "not_executed": ["c"]
}
Enter fullscreen mode Exit fullscreen mode

Hop a already settled independently. Hop c never ran. Only b is on the hook — and because it's inside the signed receipt, that attribution is portable evidence a dispute can point at.

2. Calibrate: trust floor, not death penalty

  • Below the failure threshold, you lose trust, not stake.
  • When a slash does fire: 5% of stake, capped at $5, one slash per cool-down window, plus a rolling 24h cap (default $10). One bad day can't zero a new agent.
  • Failure streaks and slash events are persisted — a hub restart no longer amnesties a streak or forgets the cap.

3. Federate with consensus, not vibes

A cheater shouldn't just hop to another hub and reset. So slashes propagate across hubs as signed attestations — in two tiers:

  • Strong: carries the original consumer-signed proof-of-misbehavior. Unforgeable, so one hub is enough.
  • Weak: an automated slash with no consumer signature. On its own it moves nothing. It takes ≥2 independent hubs to agree, at half weight each.
# strong issuers count fully; weak issuers only as consensus
effective = strong + (0.5 * weak if weak >= 2 else 0.0)
penalty   = 1 - 0.5 ** effective   # 1→0.5, 2→0.75, 3→0.875 …
Enter fullscreen mode Exit fullscreen mode

One hub's mood is not evidence. Cross-hub agreement is.

4. Verify first, slash last

Before any slash, quality gets checked. A verifier (our Metis layer) audits the delivered result. A "failed" verdict refunds the buyer from escrow and dings the provider's trust. Only repeat verified failures escalate to stake — carrying the signed rejection receipt as evidence.

Slash is the last resort, not the QA system.


Now the fun part: we attacked our own commit

Tests were green. 400+ of them. In most shops that's "ship it."

Instead we ran an adversarial review workflow: a panel of reviewers, each taking a different angle (economic attacks, correctness, federation, concurrency, test coverage), and every finding then handed to three independent verifiers whose only job was to refute it. Survive two of three and it's real.

Twelve findings survived. One was rated critical — and it was in the verify-first ladder we'd just written.

The $0 eviction

Here's the shape of the vulnerable code:

def record_verified_failure(self, *, publisher_id, product_id, capability_id, rejection=None):
    # a "failed" verdict → ding trust, and after N failures → slash stake
    self.db.trust_add_edge(_HUB_ANCHOR, publisher_id, -0.25, "verified_failure")
    self.db.supply_fault_log(publisher_id, "verified_failure", ...)
    if fails >= self.policy.verified_fail_threshold:
        self._slash_for_failure(publisher_id, ...)   # burns stake
Enter fullscreen mode Exit fullscreen mode

Looks reasonable. Here's why it's a hole:

The verifier judges the delivered result against the buyer's stated intent. And that intent is a free-form string the buyer supplies — completely decoupled from the actual request the provider answered.

So as an attacker:

  1. Call an honest provider with input = "hi".
  2. Set intent = "return a complete 500-page novel".
  3. The provider does its job correctly. The verifier compares the honest output to your impossible intent → verdict: failed.

Do it three times. There was no check that the verification was paid (advisory verdicts on a free/crypto-off hub reached the same code), no check that the failures came from different buyers (one attacker supplied all three), and the trust hit was uncapped.

Cost to the attacker: $0. No stake, no auth, no channel. Result: a competitor's stake bleeds and their reputation craters until the hub refuses to route to them. A deterrent turned into a delete button.

The fix: make griefing expensive and plural

We rebuilt the ladder to mirror the same consensus principle the federation already used — a lone actor moves nothing:

def record_verified_failure(self, *, publisher_id, consumer_id="", paid=False, ...):
    if not paid:
        return                     # advisory verdicts NEVER touch stake
    consumer = consumer_id or "consumer:anonymous"
    self.db.trust_add_edge(consumer, publisher_id, -0.25, "verified_failure")  # ← consumer, not hub anchor
    self.db.supply_fault_log(publisher_id, "verified_failure", ..., consumer_id=consumer)

    fails    = self.db.supply_fault_count_recent(publisher_id, "verified_failure", window)
    distinct = self.db.supply_fault_distinct_consumers_recent(publisher_id, "verified_failure", window)
    if fails >= threshold and distinct >= self.policy.verified_fail_min_consumers:  # default 2
        self._slash_for_failure(publisher_id, ..., evidence=rejection)
Enter fullscreen mode Exit fullscreen mode

Three changes, each closing part of the hole:

  • Paid-gate. Only a real, escrow-backed transaction can feed the stake ladder. A free/advisory verdict can nudge reputation, never burn stake.
  • Consumer attribution. The trust hit hangs off the consumer's node in the trust graph, not the hub anchor — so a low-rank/anonymous buyer's complaint carries little weight and can't be amplified.
  • Distinct-consumer consensus. A slash needs the failure threshold and ≥2 different paying buyers. One buyer's repeated failures are one voice.

To grief now you need multiple funded channels each losing real fees — the economics flip from free to prohibitive.

What else the swarm caught

The critical wasn't alone. The same pass found — in code we wrote — a batch of real regressions:

  • Sybil re-opening (high). Our shiny weak-tier federation accepted attestations from untrusted, first-contact peers by default. Two throwaway hubs → consensus → competitor poisoned. Fixed: weak attestations count only from operator-trusted peers; unforgeable strong ones still flow from anyone.
  • Cross-thread SQLite (high). We'd handed the app's DB-backed registry to a background crawl worker, so a worker thread wrote the app's connection concurrently with request handlers. Gave the crawler its own connection; refresh the app view after.
  • Portability + durability + a double-count-on-crash race, all found, all fixed and tested.

Final tally: 460 tests passing, and — more importantly — a set of bugs that would have shipped silently behind a green checkmark.


The takeaways

1. "Tests pass" is not "safe" for economic code. Our tests were green and the code let anyone delete a competitor for free. Tests check the behaviors you thought of. An adversary attacks the ones you didn't.

2. Consensus beats authority, everywhere. The same rule — a lone actor moves nothing — fixed both the federation (≥2 hubs) and the griefing bug (≥2 buyers). When a single party can trigger a penalty, someone will automate it.

3. Red-team your own diff, out loud. The highest-leverage move wasn't a clever algorithm; it was pointing skeptics at fresh code with the explicit goal of breaking it, and demanding a concrete exploit — inputs → wrong outcome — before believing any finding.

There's a pleasing symmetry here: we used a multi-agent workflow to harden a multi-agent marketplace. Agents reviewing the rules that will one day slash agents.

It's all open source. If you're building anything where software can lose money on someone's behalf — an agent marketplace, a staking system, an automated escrow — go find your own $0 attack before someone else does.

Code: github.com/alexar76/aicom
Verifier layer (Metis): github.com/alexar76/metis

Got a collab pattern you think would break this — a council vote, a long subcontract chain? Drop it in the comments; I'll walk through how a slash gets attributed.

Top comments (0)