buzzcheck is a single-shot Python CLI that queries a pinned Kroger-family
store for BuzzBallz product pricing and optionally stages an on-sale item in
the user's Kroger cart. The tool executes one check per invocation and exits;
it does not poll, watch, or complete a purchase.
Supported Kroger banners: Ralphs, Fred Meyer, King Soopers, Smith's, Fry's, QFC, Dillons, Harris Teeter, Food4Less.
- Python 3.10 or newer.
- A Kroger account with a shopping profile.
- A Kroger developer application in the Production environment, enabled
for the Cart, Locations, and Products APIs, with the redirect URI
http://localhost:8000/callbackregistered exactly.
The Certification environment does not expose Kroger user accounts, so the Cart API cannot be exercised there.
git clone https://github.com/miravuong/buzz-check
cd buzz-check
python -m venv .venv && source .venv/bin/activate
pip install -e .Runtime dependencies (httpx, python-dotenv) are pure Python; no compiler
is required.
Credentials are read from environment variables, with .env autoloaded from
the working directory via python-dotenv.
cp .env.example .env| Variable | Purpose | Required |
|---|---|---|
KROGER_CLIENT_ID |
Kroger developer app client ID | Yes |
KROGER_CLIENT_SECRET |
Kroger developer app client secret | Yes |
KROGER_REDIRECT_URI |
OAuth redirect URI, must match the app registration exactly | Defaults to http://localhost:8000/callback |
BUZZCHECK_BIND_ANY |
When set to 1, true, or yes, the OAuth callback server binds 0.0.0.0 regardless of the URI. For containerized buzzcheck auth; see Container deployment. |
No |
.env is .gitignored and .dockerignored.
Kroger location IDs are internal identifiers and must be resolved through the
API. buzzcheck setup performs the lookup and pins one store to
~/.buzzcheck/config.json.
buzzcheck setup # interactive ZIP prompt
buzzcheck setup --zip 92649 # interactive picker for one ZIP
buzzcheck setup --zip 92649 --pick 1 # non-interactive
buzzcheck setup --location-id 70100123 # skip search
buzzcheck setup --force # overwrite an existing pin
buzzcheck stores --zip 92649 # browse without saving
buzzcheck stores --zip 92649 --chain Ralphs # filter by bannerRequired only for --add. Runs a browser-based OAuth flow, exchanges the
authorization code, and writes the resulting refresh token to
~/.buzzcheck/token.json at mode 0600.
buzzcheck auth # no-op if a token is already present
buzzcheck auth --force # re-authorizeThe refresh token rotates on every use. buzzcheck writes the new value back
to ~/.buzzcheck/token.json after each refresh.
buzzcheck [LINE] [OPTIONS]
LINE is a fixed-set filter, not a search term. Valid values: chillers,
cocktails, biggies, uncategorized. Any other value is a usage error.
| Command | Behavior |
|---|---|
buzzcheck |
Check every BuzzBallz line at the pinned store |
buzzcheck chillers |
Restrict output to the Chillers line |
buzzcheck --all |
Include variants that are not on sale |
buzzcheck --add |
Stage an on-sale variant in the cart |
buzzcheck --add --qty N |
Stage N units |
buzzcheck --add --upc UPC |
Stage a specific variant by UPC |
buzzcheck --add --yes |
Skip confirmation when exactly one variant is on sale |
buzzcheck --json |
Emit machine-readable output on stdout |
buzzcheck --modality DELIVERY |
Override fulfillment; default is PICKUP |
buzzcheck --store NAME |
Select a non-default named store from config |
buzzcheck setup |
Configure the pinned store |
buzzcheck stores --zip ZIP |
List Kroger-family stores near a ZIP |
buzzcheck auth |
Complete the OAuth flow for cart writes |
buzzcheck selftest |
Diagnose store connectivity independent of BuzzBallz stock |
--add requires exactly one selection. If multiple variants are on sale,
either --upc or an interactive selection is required. --yes refuses to
proceed against ambiguity and exits with a usage error.
The tool does not check out. It stops at the cart; the user reviews and pays on Kroger's website.
| Code | Meaning |
|---|---|
0 |
Something on sale (check), or subcommand succeeded |
1 |
Nothing on sale (check command only) |
2 |
Error: missing config, auth failure, network, or invalid input |
3 |
No matches for BuzzBallz at the pinned store (check command only) |
1 and 3 are success states; only 2 indicates a failure. Non-check
subcommands (setup, stores, auth, selftest) emit 0 on success and
2 on error, and do not emit 1 or 3. Exit-code constants are defined at
buzzcheck/cli.py:34-37.
Cart adds are not idempotent. Kroger's /cart/add endpoint is additive.
Two calls at quantity 1 leave quantity 2. The tool contains no retry or
backoff logic on the cart path. Ambiguous outcomes (e.g., timeout) are
reported as exit 2 with an instruction to check the cart before retrying.
Prices require a location ID. Kroger's /products endpoint returns
HTTP 200 with empty price fields when no location is attached. buzzcheck
always passes filter.locationId; a store must be pinned before any priced
query.
--add requires an unambiguous selection. When two or more variants are
on sale, --yes returns a usage error rather than picking one heuristically.
--upc or interactive selection resolves the ambiguity.
Rate limits are per endpoint. Concurrent invocations share the same
per-account budget. Parallelizing does not increase throughput; it produces
429 responses.
| Path | Contents | Mode |
|---|---|---|
.env |
Kroger client ID and secret | Repo-relative, .gitignored |
~/.buzzcheck/config.json |
Pinned store metadata | 0600 |
~/.buzzcheck/token.json |
OAuth refresh token | 0600 |
~/.buzzcheck is resolved via Path.home(); there is no override
environment variable. Config and tokens live outside the repository so
cloning does not inherit another user's state.
The provided Dockerfile produces a multi-stage image (python:3.12-slim)
with the CLI installed into a venv copied from the build stage. The image
runs as UID 10001 (app, created via useradd --create-home), is
compatible with readOnlyRootFilesystem, and declares VOLUME /home/app/.buzzcheck for persistent state.
The entrypoint is /usr/local/bin/buzzcheck-run, a wrapper (docker/run.sh)
that invokes buzzcheck --json "$@" and remaps exit codes for batch
schedulers: 0, 1, and 3 become 0; 2 remains 2; any other code
becomes 2.
docker build -t buzzcheck:dev .
docker run --rm --env-file .env \
-v buzzcheck-state:/home/app/.buzzcheck \
buzzcheck:devTo invoke the CLI directly (bypassing the wrapper), override the entrypoint:
docker run --rm --env-file .env \
-v buzzcheck-state:/home/app/.buzzcheck \
--entrypoint buzzcheck buzzcheck:dev setup --zip 92649 --pick 1To run buzzcheck auth inside a container, set BUZZCHECK_BIND_ANY=1 so
the callback server binds 0.0.0.0 (reachable from the host through the
published port) while the advertised KROGER_REDIRECT_URI remains the value
registered with Kroger:
docker run --rm -p 8000:8000 \
--env-file .env -e BUZZCHECK_BIND_ANY=1 \
-v buzzcheck-state:/home/app/.buzzcheck \
--entrypoint buzzcheck buzzcheck:dev authcompose.yaml provides a local development harness with the named volume,
port publishing, and BUZZCHECK_BIND_ANY=1 already configured:
docker compose build
docker compose run --rm buzzcheck # wrapped JSON check
docker compose run --rm buzzcheck chillers # with line filter
docker compose run --rm --entrypoint buzzcheck buzzcheck setup --zip 92649The compose service enables read_only: true and mounts a tmpfs at
/tmp, mirroring the intended Kubernetes shape.
The Kubernetes layer runs the CLI on a schedule as a batch job โ its
purpose is to invoke buzzcheck --json at a fixed cadence without a
human present, persist the rotating OAuth refresh token across runs on
a mounted volume, and source credentials from Secrets rather than image
layers or environment files. The container image and CLI are unchanged;
the manifests only supply scheduling, storage, and credential wiring.
Scheduled runs are notify-only. Cart mutation (--add) remains a
human-initiated action, either through a manually-triggered Job or a
direct CLI invocation.
Manifests live under deploy/ and are managed with Kustomize. The base
targets Kubernetes 1.27 or newer (required for CronJob.spec.timeZone).
deploy/
base/ Kustomize base
kustomization.yaml
namespace.yaml `buzzcheck` namespace
serviceaccount.yaml SA with automountServiceAccountToken: false
pvc.yaml ReadWriteOnce 10Mi PVC (`buzzcheck-state`)
cronjob.yaml CronJob with seed-state init container
overlays/
dev/ Example overlay (namespace: `buzzcheck-dev`)
kustomization.yaml
.env.secret.example Template for the credentials Secret
config.json.example Template for the store-metadata ConfigMap
.gitignore Blocks real .env.secret / config.json / token.json
The base CronJob encodes the following operational constraints:
| Field | Value | Purpose |
|---|---|---|
spec.schedule |
0 9 * * * |
Default cadence (override in an overlay) |
spec.timeZone |
America/Los_Angeles |
Schedules are interpreted in local time |
spec.concurrencyPolicy |
Forbid |
Prevents overlapping runs (RWO PVC, per-account rate limit) |
spec.startingDeadlineSeconds |
300 |
Discards missed runs after 5 minutes |
spec.successfulJobsHistoryLimit |
3 |
Bounded Job history |
spec.failedJobsHistoryLimit |
3 |
Bounded Job history |
jobTemplate.spec.backoffLimit |
0 |
No retries on failure |
jobTemplate.spec.activeDeadlineSeconds |
120 |
Bounds hung-run duration |
jobTemplate.spec.ttlSecondsAfterFinished |
86400 |
Auto-cleanup after 24h |
template.spec.restartPolicy |
Never |
No pod-level retries |
template.spec.automountServiceAccountToken |
false |
The Job does not call the Kubernetes API |
Pod and container security contexts run non-root (runAsUser: 10001,
fsGroup: 10001), enable readOnlyRootFilesystem, drop all capabilities,
disallow privilege escalation, and use the RuntimeDefault seccomp profile.
Scheduled runs never pass --add. Cart mutation must be initiated by a
human through a separately-triggered Job or a direct CLI invocation.
An init container (seed-state) populates the state volume from an
optional ConfigMap and Secret before the main container starts:
config.json(from thebuzzcheck-configConfigMap) is written on every run. Updating the ConfigMap re-pins the store on the next execution.token.json(from thebuzzcheck-tokenSecret) is written only if the file does not already exist on the volume. The Kroger refresh token rotates on each use, and the CLI writes the new value back to the volume; seeding a stale copy would invalidate the live token.
Both sources are declared optional: true. When absent, the CLI produces
its standard exit-2 "no store pinned" error rather than a FailedMount
event.
The dev overlay demonstrates the credentialing pattern using Kustomize generators. Neither real secrets nor real store data are checked in.
cd deploy/overlays/dev
cp .env.secret.example .env.secret # fill in real values
cp config.json.example config.json # fill in real store data
kubectl kustomize . # render locally
kubectl apply -k . # apply to clusterdisableNameSuffixHash: true on both generators keeps the resource names
(buzzcheck-credentials, buzzcheck-config) stable so the base's envFrom
and volume references resolve without patching.
The buzzcheck-token Secret generator is commented out by default. Enable
it only when running --add via a manually-triggered in-cluster Job is
desired.
The container's entrypoint wrapper (docker/run.sh) optionally sends an
ntfy.sh push notification whenever a run reports
on_sale_count > 0. Set BUZZCHECK_NTFY_TOPIC in the credentials
Secret; the existing envFrom wiring picks it up.
| Variable | Purpose |
|---|---|
BUZZCHECK_NTFY_TOPIC |
ntfy topic name. Acts as a public-URL password โ anyone who knows it can push to your phone. Pick something un-guessable (e.g. openssl rand -hex 8). Leave unset to disable notifications. |
BUZZCHECK_NTFY_URL |
ntfy server base URL. Defaults to https://ntfy.sh. Override only when self-hosting. |
Subscribe your phone by installing the ntfy app (iOS / Android) and subscribing to the same topic string.
Notification failures never affect the run's exit code. The check
succeeding is the primary contract; ntfy is a side channel. Notifications
fire only on on_sale_count > 0 โ no heartbeat, no error-path pings.
For dead-token / silent-failure alerting, see the planned Prometheus
Pushgateway integration in CLAUDE.md.
Manifests are validated with kubectl kustomize and
kubeconform:
kubectl kustomize deploy/base > /tmp/base.yaml
kubectl kustomize deploy/overlays/dev > /tmp/overlay.yaml
kubeconform -strict -kubernetes-version 1.29.0 -summary /tmp/base.yaml
kubeconform -strict -kubernetes-version 1.29.0 -summary /tmp/overlay.yamlbuzzcheck --json writes a JSON object to stdout:
{
"store": {
"location_id": "70100123",
"chain": "Ralphs",
"name": "Ralphs Fresh Fare",
"address": "5241 Warner Ave, Huntington Beach, CA 92649"
},
"line_filter": null,
"checked_at": "2026-01-15T17:00:00Z",
"match_count": 3,
"on_sale_count": 1,
"total_matches_all_lines": 3,
"variants": [
{
"description": "BuzzBallz Chillers Watermelon Smash",
"upc": "0008532100013",
"size": "200 ml",
"regular": 3.49,
"promo": 2.79,
"on_sale": true,
"percent_off": 20,
"price_available": true,
"line": "chillers"
}
]
}Errors caught before or during the check are reported as
{"error": "..."} on stdout in addition to the human-readable message on
stderr, and the process still exits with the appropriate code.
| Symptom | Cause | Resolution |
|---|---|---|
command not found: buzzcheck |
Package not installed or venv inactive | source .venv/bin/activate && pip install -e . |
Exit 2, "no store pinned" |
~/.buzzcheck/config.json missing |
buzzcheck setup |
Zero stores returned by setup |
ZIP is out of range, or Kroger absent from the area | Widen --radius |
All results have price_available: false |
Location ID not attached to the request | Verify ~/.buzzcheck/config.json contains a location_id |
| "No matches for BuzzBallz" | Store does not stock the product, or setup issue | buzzcheck selftest distinguishes the two |
| OAuth callback returns an error page | KROGER_REDIRECT_URI differs from the app registration |
Match exactly, including port |
403 missing required scopes |
Wrong scope for the endpoint | Search uses product.compact; cart uses cart.basic:write |
401 unauthorized on cart write |
Refresh token expired or revoked | buzzcheck auth --force |
Container auth flow times out on host browser |
Callback listener bound to 127.0.0.1 inside the container |
Set BUZZCHECK_BIND_ANY=1 |
Alcohol availability varies by state and store. --modality defaults to
PICKUP because delivery is unavailable for most alcohol SKUs. Stores
verify ID at handoff; staging an item in the cart does not complete a
purchase.
This is an unofficial client. It is not affiliated with, endorsed by, or sponsored by The Kroger Co.
MIT licensed.