DEV Community

Marcus Richards
Marcus Richards

Posted on

Two agents, one wallet: a memory graph that stops duplicate spends

The problem

Give two AI agents the same spending authority and they will, sooner or later, pay for the same thing twice. Not because they're careless — because neither one can see what the other just did. Each agent's memory ends at its own context window.

Permit is my payment-authority layer for agents: scoped, revocable permission to spend, with every outcome receipted. For Graph Hacks I asked a narrower question: what if the coordination between agents lived in a graph they both query before acting?

What I built

Permit Memory Graph. FalkorDB holds the shared state:

  • Nodes: agents, permits, claims, merchants, receipts
  • Edges: who holds which permit, who claimed which scope, what was decided and why

Before any spend, an agent asks the graph three questions: Has this scope already been claimed? Is this authority still live? What did the last agent learn about this merchant?

The demo, in three beats

  1. Agent A spends $120 on cloud-gpu against a shared $500 permit pool. The graph records the claim.
  2. Agent B tries the same spend — and asks the graph first. It finds A's claim and refuses: duplicate scope, already claimed, citing the claim's ID as provenance.
  3. Revoke the permit node, and both agents fail closed. No live authority, no spend, no exceptions.

On the real FalkorDB backend the demo graph ends at 9 nodes / 11 edges; the final audited suite passes 17/17 slice tests and 5/5 Falkor-backed tests. One operational lesson, learned the honest way: clear the graph before every run. A second run on a dirty graph still passes but the counts drift (13 nodes / 19 edges) — stale state lies quietly, so the runbook wipes first, every time.

What the graph buys you

A refusal with a receipt beats a refusal with a shrug. When Agent B says no, it can point at the exact claim node that made it say no. That provenance — decision, evidence, edge — is the whole thesis of my Permit work, and the graph makes it a query instead of a log file.

The benchmark: measured against bugs I already fixed

A demo proves a demo works. So I froze a harder test before running it: seven incidents from Permit's own history — real issues and fixes, each with its merged PR on the record — rebuilt as scenarios in the graph. For each one I wrote down, in advance, the question a second agent would ask the graph and the answer Permit's fix guarantees. Then I ran them on the real FalkorDB backend and let the graph answer.

First run: 4 of 7. Then I fixed the three misses and ran the identical frozen set again: 7 of 7. Both runs are in the log; here's what each one showed.

The four passes are the coordination core: a spend attempted after its permit was revoked mid-flight fails closed; the graph names which agent claimed a spend first, and the second agent's refusal cites that exact claim ID; the refusal itself is on the record, reason included; and revoking a permit voids its in-flight claim instead of leaving it "active."

The three first-run misses were just as specific, and I'm not going to soften them:

  • Cumulative caps. Two agents each claimed $300 against one $500 cap, at different merchants — $600 in total claims, and the graph allowed both. It checked a single claim against the cap and never summed what was already claimed. Permit itself refuses this (atomic concurrent cap, PR #20); the graph slice didn't have that guarantee.
  • Expiry. A permit whose expiry had already passed still spent successfully. The slice never read an expiry timestamp. Permit tightened expiry at release time (PR #29); the graph didn't enforce it.
  • Crash survival. I killed the server and restarted it. The graph came back empty — every claim's provenance, gone. As configured for the demo, the store was volatile; Permit's crash-resume durability (PR #26) didn't carry over.

So I fixed exactly those three things — admission now sums active claims against the cap, permits carry an expiry that admission actually reads, and the server runs with durable append-only storage — added one test per fix (suite now 9/9), and re-ran the same seven cases with the same expected answers, the set frozen before any of it. Second run: 7 of 7 — the second $300 claim refused against the cumulative cap, the expired permit refused, and after the kill and restart the graph still knew who had claimed what.

That's the honest shape of the thing: the graph answered every question it was built to answer — who claimed what, what was decided, what got revoked — and the benchmark found the three Permit guarantees the slice hadn't grown yet. Fixing them and re-measuring against the unchanged set is the whole point of having one. A benchmark that only contains things you already pass isn't a benchmark. (The frozen set and the full measurement log ship with the project.)

What I'd do next

Persist merchant reputation across sessions, so "what did the last agent learn about this merchant?" has a longer memory than one demo run — and wire the graph check into Permit's live gate, where a duplicate claim refusal would carry a real receipt chain behind it.

Top comments (0)