# How to Build a CS2 Skin Price Alert Bot

> A CS2 price alert bot should poll cs2.sh's `POST /v1/prices/latest` for a bounded watchlist, evaluate one explicit field such as a marketplace `ask` or `bid`, and notify only when the value crosses a stored threshold with acceptable freshness. Persist the last accepted observation and alert state so restarts, null data, and temporary API failures do not create duplicate messages.

**Scope:** This is a broad, high-level v0 guide, not a complete application. cs2.sh is designed as a flexible market-data layer for whatever you want to build on top of it, so adapt the implementation to your own product, strategy, and integrations.

The delivery channel can be Discord, Telegram, email, push, or a webhook. The market-data loop stays the same because notification delivery is separate from deciding whether a price event occurred.

## Define alerts precisely

Every rule needs enough information to answer one question without guessing:

```text
market_hash_name
variant_version, if any
source
field: ask or bid
condition: below, above, or percentage move
threshold
freshness_limit
cooldown
```

An ask-below alert means a sell listing is available at or below the target. A bid-above alert means a buy order is available at or above the target. Do not label a bid as a sale price or fire an ask alert from a completed-sale series.

Source is part of the rule. A user asking for a CSFloat listing below $100 should not be alerted because BUFF crossed $100. A separate "any cash marketplace" rule can evaluate all eligible sources and include the winning source in the notification.

## Poll the watchlist in batches

Deduplicate subscribed names, split them into batches of at most 100, and send them to `POST /v1/prices/latest`. One returned item can satisfy many user subscriptions, so fetch once per polling cycle and evaluate all matching rules locally.

For each requested item:

1. Select the base item or exact nested variant.
2. Read the configured source and field.
3. Reject null or missing values. A missing price is not zero.
4. Reject the observation when `collected_at` is older than the rule's tolerance. If the marketplace's own update time matters, check `updated_at` too.
5. Compare the accepted value with the previous accepted value and the threshold.
6. Commit state and enqueue the notification atomically so a worker retry cannot send the same crossing twice.

POST endpoints can return successful `items` and per-item `errors[]` together. Record failures for only the affected names and continue evaluating successful items. A malformed name should disable or flag that subscription; it should not fail the whole watchlist.

## Alert on crossings, not every matching poll

For a below-target rule, send when the state changes from above the target to at or below it:

```text
crossed_below = previous_price > target and current_price <= target
```

Keep the rule in a triggered state until the price rises above a re-arm level. A small hysteresis band prevents a price moving between $99.99 and $100.01 from generating a message every cycle.

Cooldown controls delivery frequency, while state controls correctness. A cooldown by itself can resend the same unchanged condition after time expires. Store a deterministic event key such as rule id, source `collected_at`, field, and observed value to make notification retries idempotent.

## Percentage-move alerts need a real baseline

Define both the window and the price field. "Down 10%" could mean the current ask versus the last accepted ask, the close ask one hour ago, or a daily completed-sale price. Those are different alerts.

For fixed historical windows, `POST /v1/prices/history` returns ask and bid OHLC at `5m`, `30m`, `1h`, and `1d` intervals. Compare the current ask with a prior ask close from the same source. Use `close_time` to confirm the baseline observation actually occurred near the end of its bucket, and retain `sample_count` so a poorly observed bucket can be excluded.

OHLC history begins December 24, 2025. The maximum range per request is 14 days at `5m`, 90 days at `30m`, 365 days at `1h`, and unlimited at `1d`.

## Variants and special attributes

Variant objects are sparse. A Phase 2 rule should remain unknown when the configured source has no Phase 2 object; it must not fall back to the base Doppler. Steam cannot drive phase-specific alerts because Steam does not distinguish those variants.

The current-price endpoint does not identify individual asset listings by float, paint seed, or stickers. For an exact-asset alert, use a marketplace listing interface that exposes those assets and use cs2.sh as the reference-price layer.

## Failure behavior

- On `429`, delay and retry within the documented request rate rather than increasing concurrency.
- On temporary `5xx`, retain the last accepted observation but do not evaluate it as new.
- On `401` or `403`, stop the worker and fix the key or plan. Repeated polling cannot correct authorization.
- On a stale source, show the rule as delayed instead of sending a price alert.
- On delivery failure, retry the existing notification event. Do not recalculate the market crossing during the delivery retry.

The result is a small state machine: fetch, validate, compare, persist, and deliver. Keeping those stages separate makes it possible to prove why each message was sent.

References: [current prices](/docs/prices-latest), [price history](/docs/prices-history), [API errors and limits](/docs/using-the-api), and [data coverage](/docs/data-coverage).
