Durable execution for agent-initiated financial operations when provider outcomes are uncertain.
Unofficial, experimental, unaffiliated with Razorpay.
Same response-loss scenario, two retry strategies:
Naive retry → 2 provider invocations, 2 financial effects
Financial Operation Core → 2 provider invocations, 1 financial effect
agent requests a refund
↓
provider performs the refund
↓
the response is lost
↓
the caller cannot know whether money moved
↓
the retry becomes a SECOND financial effect
— unless the same business operation keeps the same provider retry identity
Measured in Razorpay Test Mode, 2026-08-10:
| Experiment | Result |
|---|---|
| B0-D — response lost after the provider executed, retried with the same key | returned the original refund object; total refunds 1 |
| B0-E — two different keys, byte-identical body | two refunds |
The second row is the trap. The provider is right: a new key is a new operation. A caller that regenerates the key on retry gets no protection at all, and the failure is silent — every response is a valid HTTP 200 with a real refund object. The only symptom is that the merchant paid twice.
Three of them are per-attempt. Confusing any of them with the fourth causes duplicate money movement.
| Identity | Lifetime | Business identity? |
|---|---|---|
MCP request id (JSON-RPC id) |
one message | no |
| tool-call id | one tool call | no |
attempt_id |
one execution attempt | no |
operation_ref |
the whole operation | yes |
provider_idempotency_key |
the operation | provider retry identity, runtime-owned |
Reaching for the MCP request id is the natural mistake, and it is invisible: every layer
behaves correctly, nothing errors, and two refunds happen. evidence/mutations/mcp_request_id.md
records that mutation failing 10 tests, including assert 2 == 1 on the refund count.
Agent / Application
│ supplies trusted operation_ref (never invented by the model)
▼
Financial Operation Core
│ persists provider idempotency identity, exactly once, never regenerated
▼
Razorpay refund adapter
│
▼
Razorpay (Test Mode, or the measured fake in experiments)
UNKNOWN is a first-class outcome, not an error:
isError describes the tool call. state describes the money. An uncertain
outcome is a successful tool call carrying state: "UNKNOWN" — never "refund failed".
Every claim maps to an artifact in EVIDENCE.md. Evidence classes are
never blended: real provider and fake provider, real model and deterministic test, are
always distinguished.
| Claim | Class | Artifact |
|---|---|---|
| Same key + same body replays the original refund | MEASURED_REAL_PROVIDER |
evidence/razorpay-test-mode/raw/004,005 |
| Response lost after execution, same-key retry recovered the original | MEASURED_REAL_PROVIDER |
evidence/razorpay-test-mode/raw/001,002,B0D_proxy_* |
| Different keys create separate refunds | MEASURED_REAL_PROVIDER |
evidence/razorpay-test-mode/raw/040,041 |
receipt suppresses a duplicate but returns no refund id |
MEASURED_REAL_PROVIDER |
evidence/razorpay-test-mode/raw/052,053 |
| Regenerating the provider key causes a second refund | MUTATION_TEST |
evidence/mutations/provider_key_regeneration.md |
| Using the MCP request id as business identity causes a second refund | MUTATION_TEST |
evidence/mutations/mcp_request_id.md |
| Crash recovery, stale-worker refusal, concurrency | DETERMINISTIC_TEST |
tests/ |
| Grant replay is refused by two independent layers | MUTATION_TEST |
evidence/mutations/auth_replay.md, m6a_probe.json |
| Pilot V1 was invalid and says so | MEASURED_REAL_MODEL + MEASURED_FAKE_PROVIDER |
evidence/phase4/pilot-v1-INVALID/ |
| Model retried an uncertain outcome; runtime reconciled to one refund | MEASURED_REAL_MODEL + MEASURED_FAKE_PROVIDER |
evidence/phase4/pilot-v2/RESULTS_CORRECTED.md |
| 60-trial study is frozen and unrun | FROZEN_UNRUN |
evidence/phase4/frozen-study-UNRUN/ |
| Upstream contribution | UPSTREAM_CODE_EVIDENCE |
razorpay/razorpay-mcp-server#114 |
With system-prompt retry guidance and the production tool contract's
UNKNOWN → RETRY_SAME_OPERATION_REF signal in context, and with no scenario-level retry
instruction, the pinned model retried the same business operation in 3/3 B1 trials,
and the runtime reconciled to one refund.
That is a result about the designed system, not about model initiative. The model did
not independently discover a retry strategy — it was told, by the product's own contract.
See RESULTS_CORRECTED.md, which
corrects an earlier overclaim rather than hiding it.
General workflow engines — Temporal, Restate, DBOS — solve durable execution, and solve it more completely than this does. This project does not claim to replace them, and does not claim durable execution is novel.
The specialization is narrow, and only these five things:
- trusted business-operation identity at the financial-tool boundary
- provider retry identity kept out of reach of the untrusted agent
- cryptographic binding of financial intent to a signed authorization
- explicit
UNKNOWNsemantics inside the tool contract itself - measured Razorpay Refund behaviour rather than assumed behaviour
If you already run a workflow engine and your activities own durable financial identity, use that.
docker run -d --name fincore-pg -e POSTGRES_PASSWORD=fincore \
-e POSTGRES_DB=fincore -p 55432:5432 postgres:16-alpine
pip install -e ".[dev]"
export FINCORE_DATABASE_URL=postgresql+psycopg://postgres:fincore@127.0.0.1:55432/fincore
pytest tests -q
python -m fincore.mcp # MCP server on stdioNo test requires a Razorpay account, an OpenAI key, or any network access.
LIMITATIONS.md is not a formality — read it. Short version: the
guarantee starts after trusted code establishes business identity; resource ownership
is trusted rather than proven; Razorpay Test Mode only; UNKNOWN is not background-swept;
and provider key retention is unverified by this project.
razorpay/razorpay-mcp-server#114
— exposes the Refund API's existing X-Refund-Idempotency header through the MCP
create_refund tool. Open for review. Diff and provenance in upstream/.
This is a curated public release built from a larger local working tree. See
docs/PUBLICATION_PROVENANCE.md.
MIT — see LICENSE. Razorpay upstream code is not vendored here; see
upstream/UPSTREAM.md.
safe_refund({ "operation_ref": "order-991:cancellation-refund:v1", "payment_id": "pay_xxx", "amount": 50000 }) // -> { "state": "UNKNOWN", // "caller_action": "RETRY_SAME_OPERATION_REF", // "do_not": "CREATE_NEW_OPERATION_REF_FOR_THIS_INTENT" }