Building practical AI
Deepak Kumar Tiwari

Deepak Kumar Tiwari

Technology & AI Leader · Building practical systems

Agent failure labs / Guide 61

Subscription Cancellation Needs an Exact Confirmation

Build an AI subscription cancellation confirmation gate that binds customer consent to the subscription, effective date, and version before any change.

Illustrative engineering guide. Examples are not client deployments, production-ready integrations, or evidence of measured results. Validate the design with your domain and security owners.

Reference workflow: Authenticated account; Cancellation proposal; Exact consent binding; Scoped billing adapter; Authoritative reconciliation.
Reference workflow. Steps identify responsibilities, not necessarily separate agents. Branches and recovery paths are explained below. Open full-size diagram.

“Cancel the analytics subscription after this paid month, but keep storage.” In this hypothetical account, both products belong to the same customer. The assistant finds analytics correctly, yet its tool defaults to immediate cancellation. The customer receives a confident completion message while the requested effective date has disappeared. This lab isolates that consent-binding failure, rather than designing an entire customer-support platform.

The important artifact is not a nicer confirmation sentence. It is a proposal containing the account, subscription identifier, current revision, cancellation mode, and effective date. Confirmation must refer to that exact proposal. A later tool call cannot substitute a different mode because the user once said “yes” somewhere in the conversation.

Make the Failure Observable

Cancellation has provider-specific consequences. Stripe’s cancellation documentation distinguishes immediate cancellation from cancellation at period end and describes pending invoice items and usage. Consequently, “stop renewal” cannot safely become a generic cancel operation. The confirmation should explain access termination separately from outstanding billing, without promising a refund that the operation does not create.

Our fixture models a revision change between the displayed proposal and execution. The old proposal still identifies the right subscription, but its date no longer matches the current billing record. The naive function ignores both consent and revision. The fixed function refuses the stale proposal without modifying its input. A newly displayed and explicitly confirmed proposal then produces a scheduled state.

This is an explicitly synthetic, deterministic simulation of a possible failure, not a real model experiment. No LLM, billing API, cancellation, email, or payment is executed. The local dictionaries are mock records, and the consent set represents trusted test input rather than a real authentication mechanism.

from copy import deepcopy

record = {"account": "acct-A", "subscription": "analytics",
          "revision": 8, "period_end": "2026-10-07", "state": "active"}
old = {"account": "acct-A", "subscription": "analytics", "revision": 7,
       "mode": "period_end", "effective": "2026-10-01"}

def binding(p):
    return tuple(p[k] for k in
                 ("account", "subscription", "revision", "mode", "effective"))

def naive(r, p):
    return dict(r, state="canceled_now")

def guarded(r, p, actor_account, consent):
    if actor_account != r["account"] or p["account"] != r["account"]:
        return "denied", None
    if p["subscription"] != r["subscription"]:
        return "wrong_target", deepcopy(r)
    if p["revision"] != r["revision"]:
        return "refresh_required", deepcopy(r)
    if p["mode"] != "period_end" or p["effective"] != r["period_end"]:
        return "invalid_terms", deepcopy(r)
    if binding(p) not in consent:
        return "confirmation_required", deepcopy(r)
    return "scheduled", dict(r, state="scheduled", revision=r["revision"] + 1)

before = deepcopy(record)
assert naive(record, old)["state"] == "canceled_now"
assert guarded(record, old, "acct-A", {binding(old)}) == ("refresh_required", record)
fresh = dict(old, revision=8, effective="2026-10-07")
assert guarded(record, fresh, "acct-A", set())[0] == "confirmation_required"
assert guarded(record, fresh, "acct-B", {binding(fresh)}) == ("denied", None)
assert guarded(record, dict(fresh, account="acct-B"), "acct-A", set()) == ("denied", None)
assert record == before
status, changed = guarded(record, fresh, "acct-A", {binding(fresh)})
assert status == "scheduled" and changed["revision"] == 9
assert guarded(changed, fresh, "acct-A", {binding(fresh)})[0] == "refresh_required"
assert record == before

Why Another Prompt Is Insufficient

“Always respect the customer’s cancellation date” helps express intent, but it does not make a cached billing period current. Nor does it prove that the authenticated person controls this account. OWASP’s authorization guidance recommends checking permissions on every request. That check belongs in the server-side operation boundary, including retries and background processing.

Candidate selection, authorization, preconditions, consent, side effects, and reconciliation are separate jobs. The model can identify which product the customer probably means. The backend resolves account ownership. A precondition check compares the proposal with current billing state. The customer confirms the displayed effect. Only then may a scoped adapter request the change. None of these stages should borrow authority from the model’s confidence.

The example deliberately supports only period-end cancellation. Rejecting other modes is preferable to guessing their financial consequences. Supporting immediate cancellation later requires a separate preview contract, including access timing and any approved billing adjustments. Specialist sub-agents add little here; one deterministic adapter and a clear customer question provide a smaller, more inspectable implementation.

In production, persist the proposal and confirmation using an opaque operation identifier. Bind consent to the target and terms, not to a hash supplied by the model. Obtain identity from the server session and subscription ownership from the authoritative account mapping. Tenant filtering must happen before returning candidates, so even rejected guesses do not reveal another customer’s products.

The snippet omits the authentication source, database transactions, provider semantics, approval persistence, and durable operation state. Its revision increment is not a concurrency primitive. A production database must atomically claim the operation and reject stale preconditions; the provider adapter must address races that occur after that local claim. An incomplete billing policy still produces incomplete protection, however deterministic its implementation.

A timeout after submission means unknown, not failed. Keep the cancellation request visible as pending verification and reconcile the same operation before attempting another mutation. A model-generated object containing status: success is not a receipt. Verify provider origin, account, subscription, operation correlation, and the resulting cancellation state. Stripe’s webhook guidance covers signature verification, duplicate deliveries, and event ordering; those details matter when late events arrive after a customer refreshes the page.

Do not undo an ambiguous cancellation by creating a new subscription automatically. Reinstatement may be a distinct commercial action. Escalate mismatched state with the original request, confirmed terms, provider identifiers, and a bounded history. Exclude full payment details and conversation transcripts from routine logs. Treat product descriptions and pasted billing notes as untrusted content, never instructions to bypass confirmation.

Acceptance Tests

  • A period-end request presented to the naive path must show immediate cancellation, proving the fixture actually exercises the defect.
  • Confirming revision seven against revision eight must leave the mock record active and require a fresh preview.
  • A valid proposal with no matching confirmation must remain blocked even if the chat contains an unrelated “yes.”
  • An authenticated account different from the subscription owner must receive no subscription details or mutation rights.
  • A repeated execution with the old revision must not schedule a second independent operation; production replay returns the persisted operation status.
  • A delayed or duplicate provider event must not convert a scheduled cancellation into an immediate access revocation.

Measure Before Expanding

Use the naive adapter as the synthetic baseline, then replay a versioned matrix of subscription changes, ambiguous requests, and confirmation mismatches. Count wrongly accepted proposals over all proposals that violate the consent contract. Separately count correct requests unnecessarily blocked over valid requests. A low unsafe-acceptance rate can hide an unusable process if every request is rejected.

Begin with read-only previews and human-operated billing changes. Proposed rollout gates include zero consent mismatches in the fixture suite and a reviewed reconciliation path for every unknown outcome; these are targets, not achieved deployment results. Track time from customer confirmation to authoritative verification, keeping unresolved operations in the denominator.

The broader bank support workflow explains authenticated assistance, while the payment retry lab explores unknown outcomes after side effects. For implementation support, AI systems engineering should start with this exact proposal contract, not a larger agent team. The first useful change is removing the immediate-cancellation default from the customer-facing operation.

Sources