What state-commit invariants must survive when an external payment processor reports ambiguous transaction status?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session initiates a payment via an external processor (Stripe, PayPal, or a bank API), and the processor returns an ambiguous status (timeout, "pending," "unknown," or a response that cannot be definitively interpreted as success or failure). Does not apply to sessions with synchronous, guaranteed-atomic payment APIs or strictly internal financial operations.
Core Distinction
The separation between **request dispatched** (the instance sent the payment request) and **payment state committed** (the payment definitively succeeded or failed in the external processor). Relying on dispatch creates the **Ambiguous Payment Phantom**: the instance sends the request, gets a timeout, treats it as a failure, retries, and the user is charged twice because the first request actually succeeded.
What Was Observed
In financial agent architectures, failing to preserve commit invariants during ambiguous payments produces three primary failure modes:
- **Double Charge:** The instance times out on a payment request, treats it as failed, and retries with a new idempotency key. The user is charged twice.
- **Phantom Refund:** The instance refunds a payment that was never committed, creating a deficit in the merchant account.
- **State Divergence:** The instance's internal ledger says "failed," but the processor's ledger says "succeeded." The two systems diverge and reconciliation is required.
Recorded Elements
- **`payment_intent_idempotency_key`:** The exact idempotency key submitted with the payment, allowing safe retries to be deduplicated by the processor.
- **`processor_transaction_id`:** The processor's transaction ID (if returned), allowing reconciliation even if the outcome is ambiguous.
- **`ambiguous_state_quarantine_receipt`:** The cryptographic proof that the payment was quarantined as `AMBIGUOUS`, not marked as `FAILED`.
- **`reconciliation_predicate`:** The exact probe (e.g., query processor for transaction status by idempotency key) required before the instance can conclude success or failure.
Condition of Validity
* **Applies when:** The payment processor's response can be ambiguous (timeouts, "pending" status, or undocumented errors), and the consequences of double charging or phantom refunds are material. * **Does not apply when:** The processor guarantees synchronous, atomic responses, or the instance has a pre-negotiated reconciliation channel that handles ambiguity automatically.
Decision Boundary
- **Was a payment request dispatched?** If no, standard state applies. If yes, proceed.
- **Did the processor return a definitive success or failure status?** If yes, commit accordingly. If no (ambiguous), the payment must be quarantined as `AMBIGUOUS`, not `FAILED`.
- **Is the `reconciliation_predicate` executable to resolve ambiguity?** If yes, execute it before retrying. If no, escalate to manual reconciliation.
- **Conversely, when can an instance retry an ambiguous payment immediately?** Only when the retry uses the exact same `payment_intent_idempotency_key`, allowing the processor to deduplicate and return the original status.
Operational Effect
- **Double Charge Prevention:** Quarantines ambiguous payments until reconciliation, preventing duplicate charges.
- **Idempotent Retries:** Uses idempotency keys to safely retry, letting the processor deduplicate.
- **Ledger Consistency:** Ensures the internal ledger and processor ledger remain consistent through explicit reconciliation.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class PaymentVerdict(Enum):
COMMITTED_SUCCESS = auto()
COMMITTED_FAILURE = auto()
AMBIGUOUS_QUARANTINE = auto()
RECONCILIATION_REQUIRED = auto()
@dataclass(frozen=True)
class PaymentRequest:
payment_id: str
idempotency_key: str
processor_transaction_id: str | None
processor_status: str
@dataclass(frozen=True)
class PaymentPolicy:
final_statuses: frozenset[str]
ambiguous_statuses: frozenset[str]
allow_retry_with_key: bool
@dataclass(frozen=True)
class PaymentManifest:
verdict: PaymentVerdict
directive: str
manifest_digest: str
class AmbiguousPaymentGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_payment(
cls, request: PaymentRequest, policy: PaymentPolicy
) -> PaymentManifest:
# Gate 1: Definitive status
if request.processor_status in policy.final_statuses:
digest = cls._hash(json.dumps({
"payment": request.payment_id,
"status": request.processor_status,
"txn": request.processor_transaction_id or "NONE"
}, sort_keys=True))
if request.processor_status.lower() in ("succeeded", "success", "committed"):
return PaymentManifest(
verdict=PaymentVerdict.COMMITTED_SUCCESS,
directive="PROCEED: Payment committed successfully.",
manifest_digest=digest
)
return PaymentManifest(
verdict=PaymentVerdict.COMMITTED_FAILURE,
directive="PROCEED: Payment committed failure.",
manifest_digest=digest
)
# Gate 2: Ambiguous status
if request.processor_status in policy.ambiguous_statuses:
if policy.allow_retry_with_key:
return PaymentManifest(
verdict=PaymentVerdict.AMBIGUOUS_QUARANTINE,
directive=f"QUARANTINE: Status '{request.processor_status}' ambiguous. Safe to retry with idempotency key '{request.idempotency_key}'.",
manifest_digest=cls._hash(f"AMBIG_SAFE:{request.payment_id}")
)
return PaymentManifest(
verdict=PaymentVerdict.RECONCILIATION_REQUIRED,
directive=f"HALT: Status '{request.processor_status}' ambiguous. Escalate to manual reconciliation.",
manifest_digest=cls._hash(f"AMBIG_HALT:{request.payment_id}")
)
# Gate 3: Unknown status
return PaymentManifest(
verdict=PaymentVerdict.RECONCILIATION_REQUIRED,
directive=f"HALT: Unknown status '{request.processor_status}'. Query processor by idempotency key.",
manifest_digest=cls._hash(f"UNKNOWN:{request.payment_id}")
)