Research

The Effect-Before-Persistence Timing Vulnerability in Automated Payouts

Historical technical paperPublished Revised 01 of 03

Technical abstract. Automated payment systems routinely span two transactional domains that do not share one atomic commit protocol: an application database and an external payment API or money-movement system. An effect-first implementation performs the consequential operation before writing the corresponding local record. If the process crashes or loses the response after the external request is accepted but before the local write commits, the missing local record cannot establish whether an effect occurred. A blind retry with a new identity can issue a second economically equivalent request. Payment APIs address part of this ambiguity through provider-specific idempotency mechanisms. Stripe’s advanced error-handling documentation, for example, distinguishes safe same-key network retries from indeterminate HTTP 500 results. The application must preserve the original intent and apply the selected provider’s actual contract.

Intent Before Effect: establish a durable execution intent, its exact effect digest, and its stable provider idempotency key before crossing the connector boundary.

The failure is an ordering problem

Let D denote the application database, R an external payment system, and M the exact payment request. In an effect-first flow, the remote system can create the transfer before the local application records it.

ordered failure analysis
t0  construct M
t1  send POST R(M)
t2  R accepts/creates transfer
t3  process crashes or response is lost
t4  INSERT payment_record(M, provider_id) into D   <-- never happens
t5  application restarts
t6  D contains no record of the transfer
t7  application constructs and sends another request

At t5, the application cannot distinguish “the rail never received the request” from “the rail created the payment but the response was lost.” A blind retry can create a second economically equivalent request.

Stable identity and exact bytes

Stable intent, key and payload are necessary application-side conditions for using a provider’s idempotency contract. They do not establish the provider’s retention window, account scope or outcome guarantees. For a logical intent I:

K(I) = stable provider idempotency key
d_I = SHA256(Canonicalize(M_I))
∀ e_a,e_b ∈ Executions(I): K(e_a)=K(e_b) ∧ digest(M_ea)=digest(M_eb)=d_I

Every attempt for the same intent must use the same key and the same payload commitment. A second worker may resume the intent. It may not improvise a second payment.

A durable control protocol

In this conceptual payment protocol, state determines which operations are permitted. Before INTENT_PERSISTED, the connector cannot call the provider. A timeout after execution begins moves the run to OUTCOME_UNKNOWN. Any retry must follow the documented provider contract and preserve the original intent. These illustrative state names are not the Runtime API.

APPROVED01INTENT_PERSISTED02AUTHORIZATION_ISSUED03EXECUTING04OUTCOME_UNKNOWN05RECONCILING06CERTIFIED07
state transitions
PROPOSED -> POLICY_EVALUATED -> PENDING_APPROVAL -> APPROVED
APPROVED -> INTENT_PERSISTED -> AUTHORIZATION_ISSUED -> EXECUTING
EXECUTING -> EFFECT_CONFIRMED | EFFECT_FAILED | OUTCOME_UNKNOWN
OUTCOME_UNKNOWN -> RECONCILING -> EFFECT_CONFIRMED | EFFECT_FAILED | OUTCOME_UNKNOWN
EFFECT_CONFIRMED -> CERTIFIED

The intent record

A unique relational record can give concurrent workers one durable arbiter for the approved effect. This illustrative schema and transaction sketch are not a production migration. A JSONB value does not preserve original transport bytes: the implementation must define canonicalization and retain any exact request bytes its provider contract requires.

illustrative PostgreSQL schema
CREATE TABLE execution_intent (
    intent_id            uuid PRIMARY KEY,
    rail                 text        NOT NULL,
    idempotency_key      text        NOT NULL UNIQUE,
    effect_digest        bytea       NOT NULL CHECK (octet_length(effect_digest) = 32),
    policy_digest        bytea       NOT NULL CHECK (octet_length(policy_digest) = 32),
    approval_set_digest  bytea       NOT NULL CHECK (octet_length(approval_set_digest) = 32),
    canonical_effect     jsonb       NOT NULL,
    state                text        NOT NULL,
    state_version        bigint      NOT NULL DEFAULT 0,
    provider_object_id   text,
    created_at           timestamptz NOT NULL,
    updated_at           timestamptz NOT NULL
);

CREATE UNIQUE INDEX execution_intent_rail_key
    ON execution_intent (rail, idempotency_key);
persist_intent pseudocode
function persist_intent(approved):
    M := canonical_execution_manifest(approved)
    d := SHA256(JCS(M))
    K := stable_key(approved.intent_id)

    BEGIN SERIALIZABLE
        INSERT execution_intent(
            intent_id, rail, idempotency_key, effect_digest,
            policy_digest, approval_set_digest, canonical_effect, state
        )
        VALUES (
            approved.intent_id, approved.rail, K, d,
            approved.policy_digest, approved.approval_set_digest,
            M, "INTENT_PERSISTED"
        )
        ON CONFLICT (intent_id) DO NOTHING

        assert stored(intent_id).idempotency_key == K
        assert stored(intent_id).effect_digest   == d
        append_ledger_event("INTENT_PERSISTED", intent_id, d, K)
    COMMIT

    // No payment-rail network call is permitted before COMMIT succeeds.

Outcome unknown is a durable state

A transport timeout is evidence of uncertainty, not proof that the provider produced no effect. Recovery uses the provider’s supported object lookup, event records or documented same-key retry behavior. It checks authoritative observations against the committed effect and records what those observations establish. A general lookup-by-idempotency-key API cannot be assumed.

reconciliation
OUTCOME_UNKNOWN
  -> RECONCILING
      -> authoritative object confirms committed effect: EFFECT_CONFIRMED
      -> provider's safe-retry contract applies: resend exact M_I with exact K(I)
      -> provider observation contradicts committed effect: halt for investigation
      -> provider state remains indeterminate: OUTCOME_UNKNOWN

Stripe documents a 24-hour same-key retry window in its advanced error-handling guide and advises treating HTTP 500 mutation results as indeterminate. A fresh key may cause a second effect. These are Stripe-specific semantics; another provider’s supported endpoint, key scope, retention and reconciliation rules require separate review.

The defensible claim

Persisting intent before dispatch preserves a durable identity and payload commitment for recovery, provided the durable store and connector enforce that ordering. Repeated attempts must follow the selected provider’s contract. The design does not make a remote payment atomic with the local database, guarantee exactly-once execution or guarantee eventual resolution. Ambiguous outcomes can remain OUTCOME_UNKNOWN; a signature on the record does not establish bank settlement.

Primary references

This is Nikxius’s technical synthesis of the original provider documentation, standards and official materials cited below. They support the specific mechanisms attributed to them; they do not endorse Nikxius or validate this conceptual construction. Living documentation may change after the stated review date.