Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 20 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
GRIZZLY_API_KEY=replace_with_your_key

# What to buy (wx = Apple, 62 = Turkey)
SERVICE=wx
COUNTRY=62
MAX_PRICE=2
PROVIDER_IDS=311
# Max bid; must be >= the platform minimum, otherwise WRONG_MAX_PRICE
MAX_PRICE=1
# Optional, comma-separated provider IDs; leave empty to omit
# Good for Apple - Turkey : 385, 393, 406
PROVIDER_IDS=
# Optional, comma-separated provider IDs to EXCLUDE; leave empty to omit
EXCEPT_PROVIDER_IDS=12,25,9395,96,159,197,202,222,311,385,415,253,358

NTFY_URL=replace_with_ntfy_url
# Notifications: set ntfy and/or Discord (at least one required)
NTFY_URL=
DISCORD_WEBHOOK_URL=

# Global request rate shared by all workers
# Acquisition workers
THREADS=10
MAX_REQUESTS_PER_SECOND=5
REQUEST_TIMEOUT_SECONDS=10
# Numbers to keep; extras are cancelled + reefunded. 0 = unlimited
MAX_ACQUISITIONS=1
STATUS_EVERY_REQUESTS=100

# SMS watch phase
STATUS_POLL_SECONDS=5
WATCH_TIMEOUT_SECONDS=1200

LOG_LEVEL=INFO
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
__pycache__/
*.py[cod]
.pytest_cache/
docs/superpowers/specs/
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ RUN groupadd --gid 10001 bot \
COPY requirements.txt ./
RUN pip install --no-cache-dir --requirement requirements.txt

COPY --chown=bot:bot bot.py ./
COPY --chown=bot:bot grizzly ./grizzly

USER bot

CMD ["python", "-u", "bot.py"]
CMD ["python", "-u", "-m", "grizzly"]
232 changes: 106 additions & 126 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,160 +1,140 @@
# Grizzly SMS Bot

Small Docker bot that repeatedly calls the Grizzly SMS `getNumber` endpoint until
it gets a phone number. When a number is acquired, the bot sends one notification
to an ntfy topic.
Acquire a temporary phone number from [Grizzly SMS](https://grizzlysms.com/) and
automatically **watch it for the incoming SMS code** — in a single run — with push
notifications over **ntfy and/or Discord**.

Built for grabbing scarce numbers the moment they come in stock (e.g. **Apple in
Turkey** — `SERVICE=wx`, `COUNTRY=62`) and relaying the verification code straight
to your phone.

> ⚠️ **Real purchases.** `getNumber` is not a stock check — every acquired number
> reserves a real number and holds your Grizzly balance. A number that expires
> without an SMS is auto-refunded. `MAX_ACQUISITIONS` (default `1`) is a hard cap:
> extras won by a concurrency burst are cancelled and refunded automatically.

## Features

- **One-shot flow** — acquire → watch for the SMS code → notify, no manual steps.
- **Hard single-number guarantee** — burst-bought extras are cancelled (`setStatus=8`) and refunded.
- **ntfy and/or Discord** — pick either or both; you choose simply by setting the URL(s).
- **Rate-limited workers** — one shared limiter across threads, with a global backoff on HTTP / `429` errors.
- **Fatal-response aware** — stops on `BAD_KEY` / `NO_BALANCE` / `WRONG_MAX_PRICE` and exits non-zero.
- **Provider filtering** — target or exclude specific providers (`PROVIDER_IDS` / `EXCEPT_PROVIDER_IDS`).
- **Auto-loads `.env`** — no `source .env` needed; zero third-party deps beyond `requests`.
- **Watch-only mode** — re-attach to numbers you already own: `python -m grizzly watch <id> ...`.

## How It Works

The bot starts several worker threads. Each worker calls the Grizzly SMS API with
the configured service, country, max price, and optional provider IDs.
1. **Acquire** — worker threads poll `getNumber` at a shared rate limit. The first
number returned (`ACCESS_NUMBER:<id>:<phone>`) is kept; any extra won in the same
burst is cancelled and refunded, so you keep exactly `MAX_ACQUISITIONS`.
2. **Watch** — the tool polls `getStatus` for the kept number until the code arrives
(`STATUS_OK:<code>`), the number expires (`STATUS_CANCEL`), or the watch times out.

When Grizzly returns:
`NO_NUMBERS` → keep polling. HTTP / `429` → the whole pool backs off (honouring
`Retry-After`). Fatal response (`BAD_KEY`, `NO_BALANCE`, `WRONG_MAX_PRICE:<min>`) →
notify and stop.

```text
ACCESS_NUMBER:<activation_id>:<phone_number>
```

the bot logs the number and sends a notification to ntfy.

If Grizzly returns `NO_NUMBERS`, the bot keeps polling. If Grizzly returns an HTTP
error, the bot pauses briefly before sending more requests.

## Requirements

- [Docker](https://www.docker.com/) with Docker Compose
- A [Grizzly SMS](https://grizzlysms.com/) API key
- An [ntfy](https://ntfy.sh/) topic URL

ntfy is a simple notification app. This bot uses it to send a push notification
when Grizzly SMS returns a phone number.
> The SMS only arrives once **you** enter the acquired number into the target
> service. The watcher relays the code — it can't conjure one.

## Quick Start
## Project Layout

```bash
cp .env.example .env
```

Edit `.env` with your own values:

```env
GRIZZLY_API_KEY=your_grizzly_api_key
SERVICE=wx
COUNTRY=62
MAX_PRICE=2
PROVIDER_IDS=311

NTFY_URL=https://ntfy.sh/your-topic

THREADS=10
MAX_REQUESTS_PER_SECOND=5
REQUEST_TIMEOUT_SECONDS=10
STATUS_EVERY_REQUESTS=100
LOG_LEVEL=INFO
grizzly/
__main__.py # CLI entry point (full flow + `watch` subcommand)
config.py # env parsing + .env auto-loader
notify.py # ntfy / Discord backends + fan-out notifier
api.py # Grizzly client, rate limiter, response parsing
bot.py # Acquirer (workers + hard cap) and Watcher (SMS polling)
tests/ # stdlib unittest, no network
```

By default, this example targets **Apple** with `SERVICE=wx` and **Turkey** with
`COUNTRY=62`. You can verify service and country codes in the Grizzly SMS
[API documentation](https://grizzlysms.com/docs-old), the
[Apple service page](https://grizzlysms.com/apple), and the
[price/country table](https://grizzlysms.com/price).

Start the bot:
## Quick Start (Docker)

```bash
cp .env.example .env # then edit it (see Configuration)
docker compose up -d --build
```

Watch logs:

```bash
docker compose logs -f --tail=100
```

Stop the bot:

```bash
docker compose down
```

## Configuration

| Variable | Required | Description |
| --- | --- | --- |
| `GRIZZLY_API_KEY` | yes | Your Grizzly SMS API key. |
| `SERVICE` | yes | Grizzly service code. The example `wx` is Apple. See the [API docs](https://grizzlysms.com/docs-old) and [Apple page](https://grizzlysms.com/apple). |
| `COUNTRY` | yes | Grizzly country code. The example `62` is Turkey. See the [API docs](https://grizzlysms.com/docs-old) and [price/country table](https://grizzlysms.com/price). |
| `MAX_PRICE` | yes | Maximum price accepted by Grizzly SMS. |
| `PROVIDER_IDS` | no | Comma-separated provider IDs. Leave empty to omit `providerIds`. |
| `NTFY_URL` | yes | ntfy topic URL used for notifications. |
| `THREADS` | yes | Number of worker threads. |
| `MAX_REQUESTS_PER_SECOND` | yes | Global request start limit shared by all workers. |
| `REQUEST_TIMEOUT_SECONDS` | yes | HTTP timeout for Grizzly and ntfy requests. |
| `STATUS_EVERY_REQUESTS` | no | Log a progress message every N requests. Defaults to `100`. |
| `LOG_LEVEL` | no | Python logging level, for example `INFO` or `DEBUG`. |
| `GRIZZLY_API_URL` | no | Override the Grizzly API endpoint. Mostly useful for debugging. |

## Logs

At `LOG_LEVEL=INFO`, the bot logs startup, ntfy connectivity, progress, errors,
and acquired numbers.

Example:

```text
startup service=wx country=62 maxPrice=2 providerIds=311 workers=10 limit=5.0/s
ntfy test: OK
still polling requests=100 no_numbers=100 acquired=0
still polling requests=200 no_numbers=200 acquired=0
number acquired worker=4 activation=123456 number=33612345678
notification sent activation=123456
```

## Provider IDs
Set `NTFY_URL` and/or `DISCORD_WEBHOOK_URL` in `.env` (at least one is required).

`PROVIDER_IDS` is optional.
## Run Without Docker

Use a provider:

```env
PROVIDER_IDS=311
```

Use multiple providers:

```env
PROVIDER_IDS=311,312
```
```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

Do not send `providerIds` at all:
# Full flow: acquire a number, then watch it for the SMS code
python -m grizzly

```env
PROVIDER_IDS=
# Watch numbers you already own (no purchase)
python -m grizzly watch 541507557 541507572
```

## Tuning

Start with conservative values:
`.env` is auto-loaded from the current directory at startup — no `source .env`
needed. Real environment variables still take precedence, and
`GRIZZLY_ENV_FILE=/path/to/env` points at a different file.

```env
THREADS=5
MAX_REQUESTS_PER_SECOND=2
```
## Configuration

Increase them slowly if your machine, network, and Grizzly SMS account can handle
it. If you receive HTTP errors, lower `THREADS` or `MAX_REQUESTS_PER_SECOND`.
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `GRIZZLY_API_KEY` | yes | — | Your Grizzly SMS API key. |
| `SERVICE` | yes* | — | Service code (`wx` = Apple). |
| `COUNTRY` | yes* | — | Country code (`62` = Turkey). |
| `MAX_PRICE` | yes* | — | Max bid; must be ≥ the platform minimum (else `WRONG_MAX_PRICE`). |
| `PROVIDER_IDS` | no | — | Comma-separated provider IDs to target; omitted when empty. |
| `EXCEPT_PROVIDER_IDS` | no | — | Comma-separated provider IDs to exclude; omitted when empty. |
| `NTFY_URL` | one of† | — | ntfy topic URL. |
| `DISCORD_WEBHOOK_URL` | one of† | — | Discord webhook URL. |
| `THREADS` | yes* | — | Number of worker threads. |
| `MAX_REQUESTS_PER_SECOND` | yes* | — | Global request rate shared by all workers. |
| `REQUEST_TIMEOUT_SECONDS` | yes* | 10 | HTTP timeout (defaults to 10s in `watch` mode). |
| `MAX_ACQUISITIONS` | no | `1` | Numbers to keep; extras are cancelled + refunded. `0` = unlimited. |
| `STATUS_EVERY_REQUESTS` | no | `100` | Progress-log cadence during acquisition. |
| `STATUS_POLL_SECONDS` | no | `5` | Watch-phase poll interval. |
| `WATCH_TIMEOUT_SECONDS` | no | `1200` | Watch deadline (number lifetime ≈ 20 min). |
| `LOG_LEVEL` | no | `INFO` | Python logging level. |
| `GRIZZLY_API_URL` | no | prod | Override the endpoint (debugging). |

† At least one of `NTFY_URL` / `DISCORD_WEBHOOK_URL` must be set. If both are set,
notifications go to both.

\* Required only for the acquire flow (`python -m grizzly`). The `watch`
subcommand needs just `GRIZZLY_API_KEY` and a notifier (plus optional
`STATUS_POLL_SECONDS` / `WATCH_TIMEOUT_SECONDS`).

Service, country, and provider codes: see the Grizzly SMS
[API docs](https://grizzlysms.com/docs-old), the
[Apple service page](https://grizzlysms.com/apple), and the
[price/country table](https://grizzlysms.com/price).

## Notifications

| Event | Urgent |
| --- | --- |
| Bot started | no |
| Number acquired | yes |
| Extra number cancelled (refunded) | no |
| SMS code received | yes |
| Number expired | no |
| Watch timeout / interrupted | no |
| Fatal / stopped | yes |

## Tests

## Running Without Docker
No network, standard library only:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
set -a
source .env
set +a
python bot.py
python -m unittest discover -s tests -t .
```

## Notes

- Repository: <https://github.com/kurosaki-sol/GrizzlySmsBot>
- Use with your own Grizzly account and API key. You are responsible for how you
use temporary numbers and for complying with Grizzly SMS' terms of service.
Loading