> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto-add a free gift at a threshold

> A Cart SDK use case that adds a free gift when the cart total crosses a threshold, and removes it if the total drops below.

Watch the cart total and keep a free gift in sync with it: add the gift once the shopper crosses a spend threshold, remove it if they drop back below.

<Note>
  For a merchant-configured version with tiers, per-market rules, and a progress bar, use the [Rewards](/aftersell/cart/rewards-block) block, with no code. This is for when your rule is something the block can't express.
</Note>

## The snippet

Paste into **Cart settings → Custom script → Initialization**, and set the two constants:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const FREE_GIFT_VARIANT_ID = 1234567890; // the gift product's variant id
const THRESHOLD_CENTS = 5000;            // $50.00, since totals are in cents

function reconcileGift(state) {
  const gift = state.items.find((line) => line.variantId === FREE_GIFT_VARIANT_ID);
  const qualifies = state.totalPrice >= THRESHOLD_CENTS;

  if (qualifies && !gift) {
    window.aftersell.cart.actions.addItem(FREE_GIFT_VARIANT_ID, 1);
  } else if (!qualifies && gift) {
    window.aftersell.cart.actions.removeItem(gift.key);
  }
}

window.aftersell.cart.events.on('cart_loaded', reconcileGift);
window.aftersell.cart.events.on('cart_updated', reconcileGift);
```

## How it works

**`reconcileGift`** describes the state the cart *should* be in, then makes at most one change to get there:

* Qualifies and the gift isn't there → [`addItem`](/aftersell/cart/sdk-actions#additemvariantid-quantity) adds it.
* Doesn't qualify and the gift is there → [`removeItem`](/aftersell/cart/sdk-actions#removeitemkey) removes it, by the line's `key`.
* Otherwise → do nothing.

It runs on both [`cart_loaded`](/aftersell/cart/sdk-events#cart_loaded) (so a cart that already qualifies on page load gets the gift) and [`cart_updated`](/aftersell/cart/sdk-events#cart_updated) (so it reacts to every change afterward). Because `cart_loaded` replays to late subscribers, this works no matter when your script runs.

<Warning>
  **The `!gift` / `gift` guards are what stop this looping.** Adding the gift fires `cart_updated`, which runs `reconcileGift` again, and the second pass finds the gift already present, so it does nothing. Strip the guards and you get an infinite loop. See [the two rules](/aftersell/cart/sdk-events#the-two-rules).
</Warning>

Note that adding the gift **raises the cart total**, so a threshold near a product's price can oscillate: adding a \$0 gift is safe, but a gift with a price could push the cart above the threshold on its own. Keep the gift free, or compare against a total that excludes it.

## Adapting it

**Gate on item count instead of spend:**

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const qualifies = state.itemCount >= 3;
```

**Gate on a specific product being in the cart:**

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const qualifies = state.items.some((line) => line.productId === TRIGGER_PRODUCT_ID);
```

**Only in certain countries.** Read [`context`](/aftersell/cart/sdk-overview#context), which is available before the cart loads:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const ELIGIBLE = ['US', 'CA'];

// Only register the handlers for eligible countries.
if (ELIGIBLE.includes(window.aftersell.cart.context.customer_country)) {
  window.aftersell.cart.events.on('cart_loaded', reconcileGift);
  window.aftersell.cart.events.on('cart_updated', reconcileGift);
}
```

**Exclude the gift from the threshold** so it can't hold itself up:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const subtotalWithoutGift = state.items.reduce(
  (sum, line) => (line.variantId === FREE_GIFT_VARIANT_ID ? sum : sum + line.finalLinePrice),
  0
);
const qualifies = subtotalWithoutGift >= THRESHOLD_CENTS;
```

**Mark the gift so it reads as a gift.** A [line transform](/aftersell/cart/sdk-hooks#registerlinetransform) can change a line's title, variant title, hidden state, and internal properties — so relabel it:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.hooks.registerLineTransform((line) => {
  if (line.variantId === FREE_GIFT_VARIANT_ID) {
    line.setTitle('Free gift 🎁');
  }
});
```

<Note>
  A line transform **cannot hide a line's quantity controls** — its only setters are `setTitle`, `setVariantTitle`, `setHidden`, and `setInternalProperties`. If a shopper changes the gift's quantity, correct it from a `cart_updated` handler instead.
</Note>

## Where to go next

* **[Rewards block](/aftersell/cart/rewards-block)**: the no-code version, with tiers.
* **[Actions](/aftersell/cart/sdk-actions)**: `addItem`, `removeItem`, and the rest.
* **[Events](/aftersell/cart/sdk-events)**: why the guards matter.
