Most developers learn idempotency as:
“Send an idempotency key so the same request does not happen twice.”
That is true.
But in financial systems, it is not enough.
Idempotency is not just about generating a UUID.
It is about making sure the same financial action cannot be executed twice, even when workers crash, providers timeout, webhooks arrive late, and clients retry.
This becomes even more important when the action touches money:
payment
transfer
refund
stablecoin transfer
blockchain transaction submission
AI-agent requested financial action
This is one of the core problems I’m thinking about while building Azums, a non-custodial durable financial execution layer for APIs and AI agents.
Azums launches with Paystack payments, Paystack transfers, Paystack refunds, Solana transactions, and Solana USDT transfers.
But before talking about Azums, let’s talk about the real problem.
The dangerous payment flow
Imagine this flow:
Client
→ Your API
→ Payment Provider
→ Provider processes request
Your system calls the provider.
Then the request times out.
What do you do?
Many systems do this:
timeout = failed
retry = try again
That is dangerous.
Because the provider may have already received and processed the request.
So your system may be in this state:
Your API thinks: failed
Provider state: maybe succeeded
Customer state: unknown
That is not failure.
That is an ambiguous outcome.
And ambiguous outcomes require a different design.
The wrong way to use idempotency
A common mistake is generating a new UUID for every retry.
Example:
const idempotencyKey = crypto.randomUUID();
await provider.transfer({
amount,
recipient,
idempotencyKey,
});
This looks fine until retry happens.
If the client or server creates a new key on retry, the provider may treat it as a new financial action.
That defeats the point of idempotency.
The key idea is:
Same financial action must map to the same idempotency identity.
For many systems, a better key is derived from stable business identity:
workspace_id + action_type + external_reference
Example:
workspace_123:paystack_transfer:order_789
or:
workspace_123:refund:payment_ref_456:refund_001
The key should represent the action, not the attempt.
Action vs attempt
This distinction matters.
A financial action is the thing the user or system wants:
Refund ₦10,000 for payment X
Transfer ₦50,000 to recipient Y
Submit signed Solana USDT transfer Z
An attempt is one try to execute that action.
One action may have multiple attempts.
But it should have only one final financial execution.
A simplified model:
financial_actions (
id,
workspace_id,
action_type,
idempotency_key,
payload_hash,
status,
provider_reference,
created_at,
updated_at
);
execution_attempts (
id,
financial_action_id,
attempt_number,
status,
started_at,
finished_at,
error_class,
provider_reference
);
The action is the root object.
Attempts are evidence.
Do not bill, receipt, or finalize based only on attempts.
Finalize based on the root financial action and external truth.
Payload fingerprinting
The same idempotency key should not allow different payloads.
Example:
First request:
{
"amount": 10000,
"recipient": "recipient_A"
}
Retry with same key but changed payload:
{
"amount": 50000,
"recipient": "recipient_B"
}
This should not execute.
The system should return conflict.
A common pattern:
idempotency_key + payload_hash
If the key exists and the payload hash matches, return the existing transaction.
If the key exists and the payload hash differs, reject with conflict.
Example behavior:
same key + same payload = return existing transaction
same key + different payload = 409 conflict
new key + valid payload = create new transaction
This prevents accidental or malicious mutation of a financial action.
Retries need execution boundaries
Retries are not always safe.
A worker can fail at different points:
- before provider call
- during provider call
- after provider accepted request
- after provider completed request
- before local database update
- before receipt creation
- before callback delivery
Each point requires different handling.
If failure happens before provider submission, retry may be safe.
If failure happens after provider submission, retry may duplicate money movement unless the system first checks provider truth.
So the system needs an execution boundary:
Before provider submission:
safe to retry
After provider submission:
do not blindly retry
verify provider state first
This is where many systems break.
Webhooks are not enough
Webhooks are useful, but they are not enough.
A webhook can be:
delayed
duplicated
missed
delivered out of order
received before internal state is ready
A strong system treats webhook events as signals, not final truth.
The safer model:
Webhook = fast signal
Provider verification = direct check
Reconciliation = external truth comparison
For Paystack, that means verifying payment, transfer, or refund status against Paystack.
For Solana, that means checking signature, confirmation, finality, token movement, amount, recipient, and network.
For Solana USDT, that also means verifying the correct token mint and token account movement.
Receipt creation should happen after truth
A receipt should not just mean:
“Our worker finished.”
A receipt should mean:
“We have enough evidence to state the final outcome of this financial action.”
That receipt should include:
transaction id
action type
amount or asset
rail
provider or chain reference
final outcome
policy decision
approval reference if needed
reconciliation status
receipt hash
created timestamp
This is important because customers do not only need logs.
They need proof.
What about AI agents?
AI agents make this problem more urgent.
If an AI agent can request a refund or transfer, the system must answer:
Who is the agent?
What can it request?
What amount is allowed?
Does this require approval?
Was a one-use permission created?
Was the permission consumed?
Was execution submitted once?
Was the outcome verified?
Was a receipt created?
An AI agent should not directly call the payment provider.
It should request a financial action through a control layer.
That control layer should enforce policy, approval, idempotency, receipts, reconciliation, and exceptions.
This is one of the reasons I’m building Azums.
A better lifecycle
A safer financial execution lifecycle looks like this:
Request received
→ identity verified
→ idempotency checked
→ payload fingerprinted
→ policy evaluated
→ approval required if needed
→ one-use permission created if needed
→ execution queued
→ provider or chain execution submitted once
→ outcome tracked
→ receipt created
→ callback delivered
→ reconciliation checked
→ exception opened if reality disagrees
This is more work than simply calling a provider API.
But this is the work that makes financial execution trustworthy.
How Azums approaches this
Azums is being built as a durable financial execution layer for APIs and AI agents.
The first launch scope is narrow:
Paystack payments
Paystack transfers
Paystack refunds
Solana transactions
Solana USDT transfers
Azums is non-custodial.
It does not hold customer funds or private keys.
The goal is to control execution around financial actions:
identity
idempotency
policy
approval
one-use permissions
provider or chain execution
receipts
callbacks
reconciliation
exceptions
audit proof
The key belief is:
Financial systems should never silently fail, duplicate money movement, or pretend uncertainty is success.
Final thought
Idempotency is not just a UUID.
It is a correctness contract.
It says:
This financial action has one identity, one payload, one lifecycle, and one final truth.
When you combine idempotency with durable execution, receipts, reconciliation, and exceptions, you get a system that can survive the messy reality of financial infrastructure.
That is the kind of system I believe more teams will need as APIs and AI agents start touching real money.
I’m building Azums to explore that future.
Follow the build at getazums.com


Top comments (0)