DEV Community

MarouaB
MarouaB

Posted on

Exactly-Once: Your agent shouldn't pay the same invoice twice

Wrap the payment. It runs once across retries, crashes, resumes, and replays.

exactly-once is a Python library that makes a side effect run a single time. Wrap the function that pays an invoice or sends an email, or submits a transaction and it executes once per key, then replays its stored result on every later call.

Here is the whole integration:

from exactly_once import once, Store, current_key

store = Store.sqlite("effects.db")

@once(store, key=lambda inv, **_: f"pay:{inv.id}")
def pay_invoice(inv):
    return payments.transfer(inv.vendor, inv.amount, idempotency_key=current_key())
Enter fullscreen mode Exit fullscreen mode

Call pay_invoice(invoice) and it pays the vendor. Call it again from a retry, a resumed run, a replay, or a second worker and it returns the recorded result. The vendor is paid once.

The crash it's built for

An agent pays an invoice. The transfer reaches the provider and succeeds. The process dies in the moment between the provider's 200 OK and the line that records the result. The agent restarts and reaches the same step again.

exactly-once writes a record the instant the agent enters the call. pay_invoice claims the key pay:{invoice.id}, and the store marks it IN_FLIGHT. When the result returns, the store marks it COMMITTED and saves that result. After the crash the record reads IN_FLIGHT with an empty result the library knows a payment started and holds no proof it finished.

So it quarantines the key. The agent leaves that payment for a decision and moves on. You give @once a prober that asks the payments API whether a transfer with that idempotency key exists: the library commits the key when the provider confirms the payment, and releases it when the provider confirms none. Until an answer arrives, the held payment stays in the ledger where you can see it:

store.list(state="in_flight")   # every payment awaiting a verdict
Enter fullscreen mode Exit fullscreen mode

How the guarantee holds

Three states, one atomic operation:

FRESH ──claim──▶ IN_FLIGHT ──commit──▶ COMMITTED
Enter fullscreen mode Exit fullscreen mode

claim(key) is an atomic check-and-set. The first caller to claim a key wins it and runs the effect; every other caller reads the record that already exists. The whole guarantee rests on that one operation, so it lives in the store you choose, and each store states the scope it enforces:

Store Guarantee
memory strong within one process
SQLite strong on one host
Redis strong on a single instance; best-effort under failover
Postgres SERIALIZABLE multi-writer correctness with transactional serialization

The guarantee is one line: a guarded effect is entered at most once per key. That is exactly-once effect execution at most once, then replay and it holds across retries, concurrent workers, crashes, and replays. Passing current_key() to the provider extends it end to end: your store keeps the agent from entering the call twice, and the provider's own idempotency key resolves a duplicate request to the one payment.

Key on the payment's identity

The key is the identity of the effect. pay:{invoice.id} binds the guard to the invoice, so the same invoice always resolves to the same guarded payment, and each separate invoice gets its own. Key on business identity an invoice id, an order id and the store dedupes every attempt that shares it.

What you get

  • A @once decorator and a with once(...) block sync and async, identical semantics.
  • Stores for memory, SQLite, Redis, and Postgres, each with a documented atomicity and writer model.
  • Quarantine on crash, with probers and optional worker leases for recovery.
  • Passthrough of the provider's idempotency key through current_key().
  • An onchain adapter that keys a transaction on its nonce, so a resumed agent submits it once.
  • Wrappers for LangGraph nodes and CrewAI tool runs, and any plain function, loop, or worker.
  • An inspectable ledger: store.list(state=...) feeds dashboards, audits, and reconciliation.
  • Zero required dependencies, full typing, no LLM.

Watch it hold a duplicate payment while an unguarded agent lets one through, side by side:

python examples/crash_mid_payment.py
Enter fullscreen mode Exit fullscreen mode

Then wrap the first irreversible call in your agent the payment whose duplicate would be hardest to explain.

pip install exactly-once
Enter fullscreen mode Exit fullscreen mode

github.com/swarmproof/exactly-once · MIT

Further reading

Top comments (1)

Collapse
 
deanlee profile image
Dean Lee

Wrapping side effects at the execution boundary rather than relying on prompt memory is the right architectural cut. The most expensive failure mode in production agent workflows is almost never a clean upstream 500 that triggers an immediate abort. It is an unhandled timeout between dispatch and receipt where a retry loop re-executes non-idempotent state mutations.Moving idempotency out of model context into a deterministic local store with explicit transaction keys bounds the blast radius of network partitions. When an agent cannot double-spend or duplicate mutations regardless of how many retry loops fire, operational safety stops depending on model reliability.