OpenAcid

Four primitives. One wrap. Your agent actions become atomic, crash-safe, and self-auditing.

OpenAcid

Created At

Open Agents

Project Description

OpenAcid is a durable execution library for AI agents that hold real money.


THE PROBLEM

Autonomous AI agents are increasingly given their own wallets, signing keys, and discretion over real funds — rebalancing portfolios, executing swaps, paying bills, custodying user deposits. Account abstraction (EIP-7702 mainnet, May 2025) and CDP-style server wallets have made this commonplace, and iNFT (ERC-7857) is making agent state portable and valuable. Over the next eighteen months, a 10–100× increase in agents holding non-trivial value is on track to land. And every one of those agents quietly inherits a class of bugs that no library has shipped a real fix for.

It's a six-headed bug, all variations of "the agent silently lost money":

  1. PROCESS CRASH MID-BROADCAST — the agent signs and broadcasts a tx, then the host crashes (OOM, redeploy, lost socket) before the result is persisted. On restart the agent has no memory of having sent it, re-broadcasts the same intent, and pays 2× gas + 2× slippage. Worse, in a rebalancing loop the portfolio over-rotates.
  2. CONCURRENT RETRIES — two parts of the agent (parallel tools, a parent + retry wrapper, a webhook + cron) race for the same logical action. Both broadcast. Both succeed. Double execution.
  3. LLM-LOOP REPLAY — the planner emits the same tool call twice because the first response was slow, or because the model re-derives the same plan after observing a stale state. Wasted tokens and a duplicate transaction.
  4. MULTI-STEP PARTIAL FAILURE — a swap is approve → swap → stake. The approval mines. The swap fails. The agent now has a standing ERC-20 allowance pointing at a router it didn't end up using — phishing exposure, audit failure, half-rotated position.
  5. STALE-DATA DRIFT — the agent acts on price data that's hours old because the cache never invalidated. The trade is technically successful but economically wrong.
  6. POSTCONDITION VIOLATIONS — the action "succeeded" (no exception, tx mined) but the wallet is left in an invalid state (allowance count wrong, balance below floor, slippage above cap) and nobody notices until next week.

Every team running on-chain agents has hit three or more of these in production. Today they patch them with bespoke code: Redis locks that don't survive restart, hash-of-args caches that don't reconcile with chain state, "fingers crossed" exception handlers, hand-rolled retry-with-revoke logic. Nobody has shipped the obvious shared library.

WHY EXISTING TOOLS DON'T SOLVE IT

  • p-retry, axios-retry — retry policies, no idempotency, no chain awareness.
  • bullmq, inngest — heavy queue infra, no saga compensations, no chain semantics.
  • Stripe / Twilio idempotency keys — service-side dedup; useless when you are the service.
  • Defender Sentinel, Safe tx service — chain-side reliability; doesn't help mid-agent-loop.
  • Tenderly virtual testnets — pre-execution simulation; doesn't recover from real crashes.
  • Temporal, Inngest workflows — backend durability; not shaped for LLM loops + on-chain finality + signed receipts.
  • Langfuse, LangSmith — flat observability traces; no causal model, no enforcement.

The gap is agent-shaped durability semantics — exactly-once execution, atomic multi-step, invariant enforcement, and signed receipts — exposed as a small library, not a workflow engine.


THE SOLUTION

openacid borrows the four classical database guarantees — Atomicity, Consistency, Isolation, Durability — and exposes them as four composable npm primitives:

import { saga, invariant, idempotent, receipted } from '@openacid/acid'

saga(steps, compensations) // A — multi-step rollback via compensating txs invariant({ pre, post }) // C — predicates enforced at action boundaries idempotent({ key, storage }) // I — in-flight tracking + dedup, crash-safe receipted({ storage, signer }) // D — signed chained receipts, persisted to 0G

Wrap any agent action in receipted(invariant(idempotent(saga(action)))) and a single nested call makes it crash-safe, deduplicated, invariant-enforced, and self-signing an audit trail. Postgres taught your backend ACID semantics. openacid teaches your agents.


WHAT'S IN THE BOX

  • The library, the four primitives, in TypeScript on npm (@openacid/acid).
  • A 0G Storage adapter (@0gfoundation/0g-storage-ts-sdk) for content-addressed receipts and saga state — in-flight markers on 0G KV, receipts and saga state as content-addressed blobs.
  • A viem chain adapter that reconciles in-flight broadcasts on restart — queries chain by tx hash or nonce, detects mined/replaced/pending, skips re-broadcast if the tx already landed.
  • An ENS adapter: each agent gets a subname under openacid.eth, and the latest receipt CID is published into ENS text records — so alice-bot.openacid.eth is a tamper-evident activity log queryable by any ENS resolver, no library required.
  • ReceiptRegistry.sol deployed on 0G Chain (chainId 16602), anchoring merkle roots of receipt batches on-chain — any verifier can call ecrecover against the struct hash to validate a receipt without installing the library.
  • A working rebalancing agent (plain TypeScript loop + viem) running its reasoning on 0G Compute, executing a multi-step Uniswap V4 swap saga on Base Sepolia, that survives kill -9 mid-broadcast and resumes without re-broadcasting — proving "five txs, exactly five."

How it's Made

Core library — pnpm monorepo, built with tsup for dual ESM/CJS output, tested with vitest. The four primitives (saga, invariant, idempotent, receipted) are
higher-order functions with a uniform signature: take an async function, return an async function with added guarantees. Zero runtime dependencies beyond their own
adapter interfaces — chain-agnostic and framework-agnostic by design. The only public configuration surface is three adapter interfaces: StorageAdapter, ChainAdapter, SignerAdapter. Swap the adapters, same guarantees. The library validates composition order at construction time and emits warnings when the nesting order changes the semantic guarantees — for example, wrapping idempotent outside saga means saga state isn't deduplicated across retries, which is almost never what you want.

Install and use:
pnpm add @openacid/acid
npm: https://www.npmjs.com/package/@openacid/acid

import { saga, invariant, idempotent, receipted } from '@openacid/acid'

const rebalance = receipted({ storage, signer },
invariant({ pre: walletBalanceOK, post: noOrphanAllowances },
idempotent({ key: (a) => rebalance:${a.deadline}, storage },
saga({
steps: [
{ id: 'approve', do: (ctx) => approve(token, router, ctx.amount) },
{ id: 'swap', do: (ctx) => router.exactInput(ctx.swapArgs) },
{ id: 'stake', do: (ctx) => vault.deposit(ctx.outAmount) },
],
compensations: {
approve: (ctx) => approve(token, router, 0n),
},
storage, }))))

await rebalance({ targetRatio: 0.6, deadline: blockNumber })
One wrap. Crash-safe. Deduplicated. Invariant-enforced. Self-auditing.

0G Storage (@0gfoundation/0g-storage-ts-sdk) is the primary durability backend. In-flight idempotency markers and result caches go to 0G KV via Batcher/KvClient for low-latency atomic reads. Receipts and saga state are written as content-addressed blobs via indexer.upload / ZgFile — the CID is derived from the content, so a
receipt is immutable by construction; "updating" a receipt means creating a new one, not overwriting. Large saga states are streamed via the stream method on the
adapter rather than chunked put calls.

The notably hacky part: the cas (compare-and-swap) operation underpinning idempotent's in-flight markers is built on top of 0G KV's versioned writes. Two concurrent
callers racing on the same idempotency key will have exactly one win and one block. No distributed lock manager, no external coordinator — the storage layer itself is the mutex. This is the entire isolation guarantee, and it survives process restarts because the marker lives in 0G KV, not in process memory.

0G Compute (@0gfoundation/0g-compute-ts-sdk) drives the reasoning layer of the example agent. Rather than pinning a model name at build time, the model is resolved at runtime via broker.inference.getServiceMetadata() — the 0G Compute catalog is dynamic and provider-dependent, so hardcoding a model string is a reliability hazard. Auth is handled via on-chain signed requests or a CLI-issued Bearer token (0g-compute-cli inference get-secret --provider <address>). Local-dev fallback is Anthropic
Claude Sonnet 4.6 — same agent logic, swapped inference backend, zero code changes in the agent itself.

Receipt signing uses EIP-712 typed data via viem's signTypedData / verifyTypedData. The domain separator includes chainId 16602 (0G Galileo testnet), making receipts
chain-scoped — a receipt signed on 0G Chain cannot be presented as valid on any other chain. The full Receipt struct (callId, fnName, inputHash, outputHash, txRefs, timestamps, retries, prevReceipt) is part of the typed schema, so an auditor reading the signature sees structured fields, not an opaque hash. ReceiptRegistry.sol
(Solidity 0.8, deployed on 0G Chain via Foundry) accepts merkle roots of receipt batches and verifies signatures via ecrecover on the struct hash — no off-chain
library needed. Every N receipts, the library posts a root to the contract, giving any verifier a trustless on-chain anchor without storing every receipt on-chain.

Chain-aware crash recovery is the most technically interesting piece. When idempotent begins executing a call, it writes an in-flight marker to 0G KV containing the
broadcast tx hash. On process restart, if a marker exists without a completed record, the viem chain adapter queries 0G Chain by that tx hash and determines pending / mined / replaced / failed. If mined: mark complete, return cached result, skip re-broadcast. If replaced: surface the replacement to the caller rather than hiding it — silent re-broadcast on a replaced tx is a double-spend vector. If pending: block until finality, then resolve. This is what makes the kill -9 demo work: five
transactions intended, exactly five mined, restart handled without a single duplicate.

Example agent — a plain event loop + viem executing a Uniswap V4 approve → exactInput swap → vault deposit saga, wrapped in the full
receipted(invariant(idempotent(saga()))) stack. The noOrphanAllowances postcondition checks that the ERC-20 allowance count after execution equals the count before. If the swap fails and the approval is left standing, the postcondition fires, triggers the saga's compensation chain, and revokes the allowance back to zero. No orphan
state. No phishing exposure. The compensation for approve is approve(token, router, 0n) — one line, called automatically in reverse step order.

ENS — each agent gets a subname under openacid.eth via a custom subname registrar built with viem ENS helpers. The @openacid/adapter-ens package hooks into the
receipted primitive's onReceipt callback and mirrors receipt.latest and receipt.head CIDs into ENS text records on every write. The result: alice-bot.openacid.eth resolves to the latest receipt CID via any standard ENS resolver, which fetches the full signed receipt from 0G Storage — complete, tamper-evident audit trail,
queryable from a standard ENS lookup, no library installed.

background image mobile

Join the mailing list

Get the latest news and updates