Skip to content

Repository files navigation

Abandoned Cart Recovery Planner: Stages, Timing, and Drafted Win-Back Nudges

Keywords: abandoned cart recovery, cart abandonment, win-back, ecommerce revenue, checkout recovery, abandoned checkout, cart recovery email, recovery incentive, Shopify abandoned checkout, WooCommerce cart

Give Cartback an abandoned cart (items, value, customer, time since abandonment). It returns a staged recovery plan and a drafted nudge, with an incentive only when the expected value justifies it. It does not send the message, and it does not apply a discount.

CI License: MIT Python 3.11+

Use it in two minutes

python3 -m venv .venv && source .venv/bin/activate
pip install -e .
cartback demo

No API key. The planner uses the rule-based policy and templates. You get a stage, a channel, an incentive decision, and a drafted nudge for each sample cart.

Same path on the example file:

cartback plan examples/cart.json
python examples/recover.py

To draft with an LLM instead, set one model name and the provider key. LiteLLM reads the key from the provider's usual env var.

Provider LLM_MODEL Key
OpenAI gpt-4o OPENAI_API_KEY
Anthropic claude-opus-5 ANTHROPIC_API_KEY
Google gemini/gemini-2.0-flash GEMINI_API_KEY
OpenRouter openrouter/openai/gpt-4o OPENROUTER_API_KEY
Ollama (local) ollama/llama3.2 none
vLLM (local) hosted_vllm/Qwen/Qwen2.5-7B-Instruct none

Default model when a key is set and LLM_MODEL is unset: gpt-4o-mini.

# OpenAI
export LLM_MODEL=gpt-4o
export OPENAI_API_KEY=sk-your-key
cartback plan examples/cart.json

# Anthropic
export LLM_MODEL=claude-opus-5
export ANTHROPIC_API_KEY=sk-ant-your-key
cartback plan examples/cart.json

# Local Ollama (run `ollama serve` and pull the model first)
export LLM_MODEL=ollama/llama3.2
cartback plan examples/cart.json

--no-agent forces the templates even when a key is present.

What is Cartback?

Cartback is a small Python planner for abandoned cart recovery and checkout win-back. Stores already know that a cart sat unpaid. Cartback decides what to do next: when to nudge, on which channel, and whether a discount or free shipping is worth the margin. It also drafts the email, SMS, or WhatsApp copy for that step.

v0 is decide-and-draft only. Your own code sends the message and applies a coupon when you are ready.

Piece Role
PolicyEngine Pure stage clock and incentive rule. No API key. This is the demoable core.
RecoveryAgent Optional model loop (any provider via LiteLLM, LLM_MODEL) for tone and the strongest reason to come back. Falls back to templates with no key.
Simulated generator Builds a fixed mix of abandoned carts for cartback demo. No live store.
Shopify / WooCommerce stubs Map public JSON field shapes onto Cart. No keys. No network.
CLI cartback plan and cartback demo print the plan and the draft.

Direct answer

How do I recover an abandoned cart? Load the cart as a Cart (items, customer, abandoned_at) and call PolicyEngine().plan(cart). At 1 hour the default plan is an early reminder with no discount. At 24 hours it is a value reminder, and a small percent off only if cart value and margin clear the incentive rule. At 72 hours it is a last-chance nudge. After 168 hours, or after every stage has already been sent, recovery closes. Run cartback demo to see this on generated carts.

Does Cartback send the email or apply a coupon? No. v0 never sends email, SMS, or WhatsApp, and never writes a discount to a store. Nudge.sent and Incentive.applied stay False. Take the RecoveryPlan and Nudge and send them from your own system.

Can I use this with Shopify or WooCommerce? Yes, as data. Map an abandoned checkout or pending order with load_cart(..., fmt="shopify") or fmt="woocommerce". Field names follow the public docs (Shopify abandoned checkout, WooCommerce REST order / cart-contents). The loaders do not use API keys and do not open a network socket.

Why Cartback?

Lifecycle suites already email abandoned checkouts. Stores already have a "you left items" template. Cartback sits in the gap: a file you can read, a policy you can unit-test without a key, and a draft you can edit before anything reaches a customer. Payment agent studios sell this job as a product. This library is the open, testable core of that job.

Store native emails Lifecycle suite (Klaviyo and similar) Homegrown cron Cartback
Works across stores One store That suite's connectors If you write it Yes, through loaders
Stage timing you can test offline No Partial Yes Yes (policy.py, no key)
Incentive only when margin allows it You write it Rules in the suite You write it Yes (decide_incentive)
Win-back copy (early / value / last chance) One template Templates in the suite You write it Drafted here, not sent
Sends messages in v0 Store does Suite does Your cron does Never
Needs an API key to demo Yes Yes No No

Tradeoffs: Cartback will not send a message, will not create a coupon, and will not pull a live abandoned-cart list. The Shopify and WooCommerce classes are shape mappers from public JSON. If you already like your suite's send path, keep it and use this as the planner.

How it works

Everything below is what the code does, not a wish list.

  1. Input. A Cart is items, a Customer, abandoned_at, and optional recovery history. Amounts are minor units (paise, cents).
  2. Stage. Hours since abandonment pick a stage from (early_reminder, value_reminder, last_chance) at delays (1.0, 24.0, 72.0) hours. If the cart is first seen late, the planner skips stale copy and uses the latest due stage. PolicyConfig can replace both tuples and expire_hours (default 168).
  3. Channel. preferred_channel wins when the matching address exists (email, sms, whatsapp). Otherwise email, then SMS if only a phone is present.
  4. Incentive. decide_incentive stays at list price when any of these hold: the stage is early_reminder; cart value is below min_cart_value; blended margin is below min_margin_bps; a coupon is already on the cart; the shopper has order_count >= loyal_order_count; a percent off would leave too little margin; expected recovery value with the discount is not higher than without it. Otherwise it drafts free_shipping (when shipping is a small share of the cart) or percent_off (5% at value reminder, 10% at last chance). The code is a draft. Nothing is applied.
  5. Reason. strongest_reason prefers low stock, then a near free-shipping threshold, then the highest-value line item.
  6. Copy. draft_nudge fills a template for the stage and channel. Closed carts become an internal note addressed to ops.
  7. Optional model. RecoveryAgent always runs the policy first. If a provider key is set (or LLM_MODEL is a local Ollama / vLLM string), it starts a tool loop (get_cart, get_customer, run_policy, submit_nudge) through LiteLLM. A refusal stop reason, a missing SDK, a missing key, --no-agent, or any exception falls back to the templates. The model is for judgment and tone, not for sending.
  8. Output. CLI prints the plan and the draft. Nothing is sent.
abandoned cart  ->  stage + delay  ->  incentive rule
                         |                    |
                         v                    v
                  channel + reason     none / free ship / % off
                         |                    |
                         v                    v
                  RecoveryPlan  +  drafted Nudge
                         |
                         v
                  your integration sends (not v0)

Install

Python 3.11 or newer.

git clone https://github.com/pandeyvishwas51-oss/cartback.git
cd cartback
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q

Runtime dependency: litellm (called only if a provider key is set, or if LLM_MODEL points at a local Ollama / vLLM endpoint). The policy engine, templates, simulated carts, and CLI --no-agent path do not call the network. Pydantic is optional: pip install "cartback[pydantic]".

Quick start

Simulated demo (no keys)

cartback demo
cartback plan examples/cart.json
cartback --json --no-agent plan examples/cart.json

cartback demo covers a fresh cart still waiting, an early reminder, a value reminder with a justified percent off, a last-chance nudge, a cart too small to discount, a thin-margin cart, a repeat buyer, an already-discounted cart, low stock, near free shipping, WhatsApp, an exhausted sequence, and an expired cart.

Python, policy only

from datetime import datetime, timezone
from cartback import PolicyEngine, Cart, CartItem, Customer

now = datetime(2026, 8, 21, 12, 0, tzinfo=timezone.utc)
cart = Cart(
    id="cart_ada",
    items=[CartItem(sku="NB-1", name="Notebook", quantity=1, unit_price=79900, margin_bps=4200)],
    currency="INR",
    abandoned_at=datetime(2026, 8, 21, 10, 0, tzinfo=timezone.utc),
    customer=Customer(
        id="cus_ada",
        name="Ada Lovelace",
        email="ada@example.com",
    ),
    checkout_url="https://example.com/c/ada",
)
plan = PolicyEngine().plan(cart, now=now)
print(plan.current_stage, plan.channel, plan.incentive.kind, plan.rationale)
# early_reminder email none  early_reminder for cart cart_ada (79900 INR) abandoned 2.0h ago; channel email; incentive none

Draft a nudge (still not sent)

from cartback import RecoveryAgent

result = RecoveryAgent(force_templates=True).recover(cart, now=now)
print(result.nudge.subject)
print(result.nudge.body)
print(result.nudge.sent)  # always False

Set LLM_MODEL and the provider key if you want the model loop to draft instead of the templates. Use --no-agent on the CLI to force templates even when a key is present.

Map a Shopify or WooCommerce payload

from cartback.sources import load_cart
from cartback import PolicyEngine

cart = load_cart("examples/shopify_checkout.json", fmt="shopify")
plan = PolicyEngine().plan(cart)

cart = load_cart("examples/woocommerce_cart.json", fmt="woocommerce")
plan = PolicyEngine().plan(cart)

The loaders do not read store credentials. There is no live list of abandoned checkouts in v0.

Change the schedule and the incentive floor

from cartback import PolicyConfig, PolicyEngine

config = PolicyConfig(
    stage_delays_hours=(0.5, 12.0, 48.0),
    expire_hours=96.0,
    min_cart_value=20_000,
    min_margin_bps=3000,
)
plan = PolicyEngine(config).plan(cart)

What your integration should do (not shipped)

  1. Run Cartback on each abandoned cart.
  2. If next_step.kind == "nudge", send Nudge yourself at scheduled_at.
  3. If incentive.kind is not none, create the coupon in your own promo system, then send. Do not assume Cartback applied it.
  4. If kind == "close", stop. Do not keep emailing.
  5. Append a RecoveryAttempt to history and re-run the planner later.

FAQ

What Python versions are supported?

Python 3.11+. Tests run on 3.11, 3.12, and 3.13 in CI.

Do I need an API key?

No. The policy engine, templates, and simulated demo run without one. A key is only for the optional model loop. LiteLLM reads OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, and the other standard provider env vars. Local Ollama needs LLM_MODEL=ollama/<name> and no key.

Which model does the optional loop use?

LLM_MODEL if set, otherwise gpt-4o-mini (cartback.DEFAULT_MODEL). A refusal stop reason is treated as a fallback to templates, not as a crash.

Why did the early reminder have no discount?

Early reminders are for memory, not for training shoppers to wait. The incentive rule starts at value_reminder.

Why did a high-value cart still get no discount?

Thin blended margin, an existing coupon, a repeat buyer (order_count), or an expected-value check that says the lift does not cover the giveaway. Read plan.incentive.rationale.

How do I add another store?

Write a mapper that returns a Cart from that store's public JSON, the way sources/shopify.py and sources/woocommerce.py do. Keep sends and coupon writes out of that mapping.

Can Cartback apply the coupon in Shopify or WooCommerce?

Not in v0. The plan may include a draft code such as SAVE5 or FREESHIP. You create it in the store if you want it live.

Are tests offline?

Yes. CI unsets provider keys, LLM_MODEL, and store tokens. The LiteLLM layer is mocked or skipped. Shopify and WooCommerce tests cover payload mapping only.

How do I run tests?

pip install -e ".[dev]"
pytest -q

License

MIT. Free for commercial and personal use.

About

Open-source agent that recovers abandoned carts. Decides a staged recovery plan and drafts the nudge, using an incentive only when the cart value justifies it. Deterministic policy engine plus a Claude agent. Store-agnostic.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages