A wallet request can time out after it has been broadcast, leaving the caller unable to tell whether retrying will create a second transfer. Since 2024, delegated accounts and more varied transaction paths have made that uncertainty harder to contain. My position: a production wallet needs a durable intent record before it needs another signing integration. If no component owns recovery, scaling workers only scales the ambiguity.
The old build-or-buy boundary no longer matches the recovery boundary
I disagree with Your 2024 Crypto Build-vs-Buy Playbook Is Obsolete if its sourcing question is treated as the production decision: buying signing, RPC, or account-abstraction services does not buy an answer to “Did this customer request execute?” Your team still has to join the HTTP request, the signed operation, the broadcast attempt, and the eventual chain result into one recoverable record.
That distinction has become sharper over the last two years. Ethereum’s Pectra upgrade activated on May 7, 2025—a network date, not a performance claim—and introduced EIP-7702, which lets an externally owned account delegate execution to code. An EIP-7702-capable wallet can therefore have behavior that an older “EOA means one simple transaction” runbook does not describe. ERC-4337 adds another operational path: a UserOperation goes through a bundler and an EntryPoint rather than through the ordinary EOA transaction submission path. EntryPoint v0.7 matters as a version boundary because tooling that serializes a UserOperation for a different EntryPoint version can produce an operation the intended path will not accept.
These mechanisms do not make every wallet request interchangeable. EIP-5792’s wallet_sendCalls describes a wallet-facing call-batching interface, while eth_sendRawTransaction broadcasts an already signed EVM transaction; a successful response from either interface is not proof of final execution because inclusion and chain history can still change. A paymaster may also be involved in an ERC-4337 operation, so rebuilding an apparently identical request after a timeout can change its signed contents. Store the exact bytes or canonical operation fields that were submitted, not merely the API payload that started the process.
I would not make a vendor webhook the system of record, because a delayed, duplicated, or missing webhook cannot establish what your API already promised the caller. Use webhooks as evidence to reconcile against receipts and your durable intent record. Keep the vendor’s request ID, the chain ID, the signer or smart-account address, and the transaction or UserOperation hash together; without that mapping, an on-call engineer must guess which customer action a chain event represents.
Idempotency now has to survive a signer and a broadcaster
At 10x users, your crypto wallet breaks at idempotency names a real failure mode, but I would not use traffic growth as the trigger for fixing it: one timeout between signing and persistence is enough to make a retry unsafe. The operational contract should be narrower and testable: the same authenticated request key and the same request body identify one wallet intent, while a reused key with a different body is rejected. That contract prevents your API from deliberately creating a second intent; it does not claim that a blockchain provides exactly-once execution.
Put a uniqueness constraint around the intent before dispatching work. This PostgreSQL example runs in psql; its sample hash is a placeholder, so production code must compute the digest from a canonical request representation before insertion.
CREATE TABLE wallet_intents (
tenant text NOT NULL, chain_id bigint NOT NULL,
idem_key text NOT NULL, body_hash text NOT NULL,
state text NOT NULL DEFAULT 'accepted',
tx_hash text, PRIMARY KEY (tenant, chain_id, idem_key)
);
INSERT INTO wallet_intents (tenant, chain_id, idem_key, body_hash)
VALUES ('demo', 1, 'request-7', 'sha256:sample')
ON CONFLICT (tenant, chain_id, idem_key) DO NOTHING;
SELECT body_hash, state, tx_hash FROM wallet_intents
WHERE tenant = 'demo' AND chain_id = 1 AND idem_key = 'request-7';
The insert makes concurrent acceptance of the same key converge on one row because PostgreSQL enforces the primary key. It does not finish the API contract by itself: after a conflict, compare the stored body_hash with the incoming hash and return a conflict if they differ. Treat that check and any state claim as database transactions, because two workers that merely read “accepted” can both decide to sign. A worker should claim a dispatch state with a conditional update, record the resulting signed payload or hash, and only then proceed through the broadcast state machine.
Nonce management needs its own boundary. For an EOA, reserve a nonce per chain ID plus signer under database coordination; asking two RPC endpoints for eth_getTransactionCount with pending is not a lock, because their mempools need not contain the same transactions. Never allocate a fresh nonce just because eth_sendRawTransaction timed out: rebroadcasting the same signed bytes preserves the transaction hash, whereas a newly signed transaction can create a second payment. Replacement under EIP-1559 is a deliberate state transition with the same nonce and changed fee fields, not an automatic retry of the customer intent.
For ERC-4337, track the UserOperation hash, EntryPoint address, smart account, and bundler submission separately from the business intent. Calling eth_sendUserOperation again with altered operation fields is not equivalent to replaying the same submission, because the operation identifier depends on what was signed. A receipt lookup, followed by a check of the relevant chain state, is the recovery path when the submitter loses its response. An accepted API response should mean “durably recorded,” not “paid,” because a signer or bundler can still reject work after acceptance.
PostgreSQL beats a Redis lock for the source of truth
There are two plausible defaults for coordinating wallet workers: a PostgreSQL intent row with a conditional claim, or a Redis SET key value NX PX lock. PostgreSQL wins when the claim and intent transition must remain auditable together, because its transaction can make the state change and ownership decision atomic. Its cost is write latency and contention on a busy signer or intent partition. Redis wins for short-lived, replaceable coordination where another durable record already defines the truth, because acquiring an expiring lock is simple and avoids a database write on that fast path. Its cost is that lock expiry or loss does not tell you whether the former holder signed or broadcast anything.
For money-moving work, I would choose PostgreSQL as the authority and use Redis only as an optional scheduling optimization, because a worker crash must not turn an expired lock into permission to create a new transaction. Start with a 5-minute lease as a tuning input for a worker claim, but make expiry return the intent to reconciliation, not directly to signing. That rule matters when a process broadcasts successfully and dies before writing the response: the next worker must inspect the stored signed payload, receipt, nonce, and account state before deciding what action remains.
The state machine should distinguish accepted, claimed, signed, broadcast, included, final, and needs_review. Those names are useful because they separate your own durable actions from observations about the chain. A transaction receipt showing inclusion is not the same thing as your application’s finality policy; a reorganization can remove a recently observed receipt. Configure that policy per chain rather than copying an Ethereum mainnet confirmation count to an L2, because the chains can expose different settlement and finality signals.
Resist collapsing a stuck item into “failed” merely because the provider returned an error. An RPC timeout says that your client did not receive an answer, not that a node did not receive the bytes. Conversely, a nonce-too-low response does not by itself prove this intent succeeded, because another transaction from the signer may have consumed the nonce. The useful on-call question is whether the exact operation is known to be included, still eligible for broadcast, deliberately replaced, or genuinely unaccounted for.
A recovery drill is more valuable than a throughput benchmark
Put an end-to-end trace around intent acceptance, worker claim, signing, submission, and reconciliation with OpenTelemetry, but keep raw signing material out of spans because tracing backends are not key stores. Propagate an internal intent ID through Kubernetes jobs or queue messages; do not use the transaction hash as the only correlation ID, because it may not exist when the request first fails. With Prometheus, count state transitions and expose the age of the oldest non-final intent by chain and signer. Avoid a metric label containing every idempotency key, because unbounded label cardinality can overwhelm the monitoring system.
As a proposed SLO rather than an observed result, target 99.9% availability for durably accepting valid wallet intents; measure chain completion separately because network fees and inclusion are outside the API acceptance boundary. Set an initial alert threshold when the oldest claimed or broadcast intent has made no verifiable progress for 15 minutes, then tune it against the chains and fee policies you operate. Alert on an aging state, not merely a low transaction count, because a quiet wallet can have no transactions while a single stranded transfer still needs intervention.
Run a fault drill before raising Kubernetes Horizontal Pod Autoscaler worker limits. Inject a 30-second test timeout after submission but before the broadcaster records its response, then retry the same API key while two workers are active. The expected result is one intent row, no second nonce allocation for that intent, and a reconciliation path that either finds the submitted operation or marks the evidence insufficient for automated resubmission. That test catches a more consequential defect than a requests-per-second benchmark because it exercises the moment at which the caller, database, and chain can disagree.
Budget two independently operated RPC providers as an operational starting point for investigation, not as a voting oracle. Comparing their receipt and pending-nonce views helps expose provider-local gaps, but disagreement should pause an irreversible decision rather than cause a third broadcast. Record which provider saw each submission and which block and receipt informed each transition, so the next on-call engineer can reconstruct the decision without relying on a dashboard’s current view.
Prove one timeout cannot become two payments
Start by selecting one production transfer path and finding the exact point where signed bytes can leave your process before their identity is durable. Add a fault-injection test at that boundary and retry the customer request with the same key. If the test creates a second nonce or operation, stop increasing worker concurrency: fix the intent record and reconciliation transition first. That is a release gate an on-call engineer can verify, not a promise that the network will never fail.



