Home / Blog / Track Real SellerMagnet API Credit Spend: A SQLite Usage Dashboard

Track Real SellerMagnet API Credit Spend: A SQLite Usage Dashboard

Build a local credit-usage ledger for the SellerMagnet API: log every retry attempt's billed or free outcome to SQLite as it happens, then print a per-day, per-endpoint report of what an account actually spent. Distinct from a pre-call pricing estimate: this reads the real outcome of every attempt, including the billed retries a simple try/except would never see.

November 11, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Terminal window showing a SellerMagnet API daily credit-spend report grouped by endpoint

To track real SellerMagnet API credit spend, log the billed-or-free outcome of every attempt, not just the final result of a call, to a local SQLite table as it happens, then group by day and endpoint for a report. A retry helper that only returns the final result or raises the final error hides how many billed attempts happened along the way, so the ledger has to sit inside the retry loop itself, not around it.

Key Takeaways

  • get_with_retry only exposes the final outcome of a call: a success after two billed 502 retries looks the same from outside as a success on the first try, but costs 3 credits instead of 1.
  • Most 400 responses are a free validation gate (including an unknown category ID, which moved from billed to free on 2026-11-01), but a specific, documented set (count over 50, a non-integer count/offset, a malformed ASIN on statistics/estimated-sales, a bad converter input) is billed because it is raised after the credit deduction.
  • A 500 is refunded unless it carries a Retry-After header, in which case it stays charged; the ledger reads the same signal the retry logic already reads.
  • An unrecognised status code is logged as billed by default, so the ledger never silently under-counts real spend when the API's behaviour changes.
  • The report groups by endpoint and day from one SQLite table; no external dashboard or service is needed to see where credits went.

Why can't get_with_retry answer how many credits a call cost?

get_with_retry from the Python quickstart exists to make retries transparent to the caller: it returns the data on success or raises ApiError on a final failure, and the backoff between attempts happens inside the loop. That is exactly why it cannot answer a billing question by itself - the caller never sees the 502 that got retried and billed before the third attempt finally succeeded. Logging has to happen on every attempt, inside the same loop, before the loop decides whether to retry.

Status codes, whether SellerMagnet API charges a credit, and how the ledger logs it.
ResponseChargedLedger behaviour
200 successYes1 credit, logged as success
404 not foundYesa real answer, still 1 credit, not retried
500 internal errorNo, refunded (unless it carries Retry-After)0 credits, 1 retry
502 / 503 upstreamYes, per attempteach attempt logged separately, capped at 4
401 / 403 / 429Norejected before the credit deduction
400 (most endpoints, incl. an unknown category ID since 2026-11-01)Noa free validation gate, e.g. an unsupported marketplace
400 (count over 50, a non-integer count/offset, bad ASIN on statistics/estimated-sells, bad converter input)Yesraised inside the view, AFTER the credit is already gone

How do I build a credit-usage ledger in Python?

tracked_call() runs the same policy as retry.py's get_with_retry (same RETRYABLE set, same backoff), but calls _classify() on every attempt's status, message and Retry-After before deciding whether to retry, and writes the outcome to a calls table keyed by day and endpoint. get and ApiError are the unmodified functions from sellermagnet.py; nothing about the actual HTTP call is reimplemented, only the logging that wraps it.

usage_tracker.py - classify and log every attempt, then report per day

"""Log every SellerMagnet API attempt to a local SQLite ledger and print a per-day cost report.

`get_with_retry` (retry.py) hides how many billed attempts happened inside its own backoff loop: a
caller that only checks the final return value or the final exception has no idea a 502 was retried
twice (2 credits) before it finally raised. This module runs the SAME retry policy (same RETRYABLE
set, same backoff) but logs the billing outcome of every attempt BEFORE deciding whether to retry, so
the ledger matches what the account was actually charged, not what the caller happened to see.

Classification rules (SellerMagnet API's documented status codes and billing behaviour):
  200                      -> billed (1 credit)
  404                      -> billed (a real "not found" answer, not an outage)
  500 WITHOUT Retry-After  -> refunded (free); WITH Retry-After it stays charged
  502 / 503                -> billed on every attempt
  401 / 403 / 429          -> free (rejected before the credit deduction)
  400                      -> billed ONLY for the known endpoint-specific cases raised after the
                              deduction (count over 50, a non-integer count/offset, a malformed ASIN
                              on statistics/estimated-sells, a bad converter input); every other 400
                              is a free validation gate (e.g. an unsupported marketplace, or an
                              unknown category ID, which moved from billed to free on 2026-11-01)
  anything else            -> assumed billed; an unrecognised status is new API behaviour, and a
                              ledger that silently calls it free would under-count real spend
"""
import sqlite3
import time
import random

from sellermagnet import ApiError, get
from retry import RETRYABLE

# endpoint -> substrings of the billed 400's message (these 400s are raised inside the view, AFTER
# the credit is deducted, so they are charged unlike most 400s). "Invalid CategoryId" on
# amazon-bestsellers is deliberately NOT here: since commit c2e6bf3 (2026-11-01) that check runs
# BEFORE the credit deduction, so it is a free 400 like any other gate - listing it here would make
# the ledger OVER-count, the opposite failure from the one this tool exists to avoid.
BILLED_400_HINTS = {
    "amazon-search": ("Count Max", "must be a whole number"),
    "amazon-bestsellers": ("Count Max", "must be a whole number", "offset + count must be at most"),
    "amazon-deals": ("Count Max", "must be a whole number"),
    "amazon-product-statistics": "Invalid product ASIN",
    "amazon-product-search-estimated-sells": "Invalid product ASIN",
    "amazon-asin-converter": "Invalid",
}


def _classify(endpoint: str, status: int, message: str, retry_after: float) -> tuple[bool, str]:
    """Return (billed, note) for one HTTP attempt. Never guesses from memory: every branch cites the
    documented billing rule it encodes, so a future billing change is one line to fix, not a rewrite."""
    if status == 200:
        return True, "success"
    if status == 404:
        return True, "not found (real answer)"
    if status in (502, 503):
        return True, "upstream error, billed per attempt"
    if status == 500:
        return (True, "500 kept Retry-After, stays charged") if retry_after else (False, "refunded")
    if status in (401, 403, 429):
        return False, "free gate"
    if status == 400:
        hints = BILLED_400_HINTS.get(endpoint, ())
        hints = (hints,) if isinstance(hints, str) else hints
        if any(h.lower() in (message or "").lower() for h in hints):
            return True, f"billed 400 ({endpoint})"
        return False, "free gate 400"
    return True, f"unrecognised status {status}, assumed billed"


def init_db(path: str) -> sqlite3.Connection:
    db = sqlite3.connect(path)
    db.execute(
        "CREATE TABLE IF NOT EXISTS calls ("
        "id INTEGER PRIMARY KEY, day TEXT, endpoint TEXT, status INTEGER, billed INTEGER, note TEXT)"
    )
    db.commit()
    return db


def _log(db: sqlite3.Connection, day: str, endpoint: str, status: int, billed: bool, note: str) -> None:
    db.execute(
        "INSERT INTO calls (day, endpoint, status, billed, note) VALUES (?,?,?,?,?)",
        (day, endpoint, status, int(billed), note),
    )
    db.commit()


def tracked_call(session, db: sqlite3.Connection, endpoint: str, day: str, attempts: int = 4, **params):
    """Same policy as retry.py's get_with_retry, logging every attempt's billing outcome first."""
    internal_errors = 0
    for attempt in range(1, attempts + 1):
        try:
            data = get(session, endpoint, **params)
            _log(db, day, endpoint, 200, True, "success")
            return data
        except ApiError as err:
            message = str(err).partition(": ")[2]
            billed, note = _classify(endpoint, err.status, message, err.retry_after)
            _log(db, day, endpoint, err.status, billed, note)
            internal_errors += err.status == 500
            if err.status not in RETRYABLE or attempt == attempts or internal_errors > 1:
                raise
            backoff = min(2 ** attempt, 30) + random.uniform(0, 1)
            time.sleep(max(backoff, err.retry_after))


def report(db: sqlite3.Connection, day: str) -> str:
    """One ASCII table: billed / free attempts and total credits spent per endpoint, for one day."""
    rows = db.execute(
        "SELECT endpoint, SUM(billed), COUNT(*) - SUM(billed), COUNT(*) FROM calls "
        "WHERE day = ? GROUP BY endpoint ORDER BY endpoint",
        (day,),
    ).fetchall()
    if not rows:
        return f"No calls logged for {day}."
    lines = [f"{'ENDPOINT':<40}{'BILLED':>8}{'FREE':>8}{'ATTEMPTS':>10}"]
    total = 0
    for endpoint, billed, free, attempts in rows:
        billed = billed or 0
        total += billed
        lines.append(f"{endpoint:<40}{billed:>8}{free:>8}{attempts:>10}")
    lines.append("-" * 66)
    lines.append(f"{'TOTAL CREDITS SPENT ON ' + day:<56}{total:>10}")
    return "\n".join(lines)

Illustrative run output: 3 lookups, a search call billed for exceeding count=50 then retried by the caller with a valid count, an offers 404, a refunded-then-retried statistics call

ENDPOINT                                  BILLED    FREE  ATTEMPTS
amazon-product-lookup                          3       0         3
amazon-product-offers                          1       0         1
amazon-product-statistics                      1       1         2
amazon-search                                  2       0         2
------------------------------------------------------------------
TOTAL CREDITS SPENT ON 2026-11-11                                7
Blueprint flow: one attempt is classified and logged before the retry loop decides to retry
Every attempt is logged before the loop decides whether to retry it.

How is this different from the API cost calculator?

The pricing calculator estimates cost BEFORE calling anything: it answers "what would 10,000 lookups a month cost on this plan". This ledger answers a different question AFTER the fact: "what did this account actually spend today, and on which endpoint", including the retries a pre-call estimate has no way to predict because it depends on how often upstream actually returns a 502 or 503 on a given day.

Blueprint table of SellerMagnet API status codes and whether the usage ledger logs them as billed
Only a specific set of 400s is billed; every other 400 is free, same as 401/403/429.

What does the daily report look like?

report() groups the day's rows by endpoint and sums the billed column, so the last line is always the total credits actually spent that day. Nothing here calls the API: the report reads only the local SQLite table that tracked_call() already wrote to, so running it costs zero credits and works offline.

The query behind report() (SQLite)

SELECT endpoint, SUM(billed), COUNT(*) - SUM(billed), COUNT(*)
FROM calls WHERE day = ?
GROUP BY endpoint ORDER BY endpoint;

Frequently Asked Questions

Why not just count how many times I called get_with_retry?

Because get_with_retry only returns the final result or raises the final exception. A single call that retried twice through a 502 before succeeding looks identical from the outside to one that succeeded first try, but the first one spent 3 credits and the second spent 1.

Is every 400 response free?

No. Most 400s are a free validation gate (missing marketplaceId, unsupported marketplace for that endpoint, and since a 2026-11-01 fix an unknown category ID too), but a handful are still raised inside the view after the credit is already deducted: a count over 50 or a non-integer count/offset on search/bestsellers/deals, a malformed ASIN on statistics or estimated sales, and a bad converter input. The ledger checks the endpoint and message, not just the status code, before marking a 400 as billed or free, because that split has moved before and can move again.

Does a 500 always get refunded?

Only when it does not carry a Retry-After header. A 500 that does carry one stays charged. The ledger reads the same signal the client's own retry logic already reads for that specific case.

Will the ledger ever under-report what I actually spent?

Any status code the classifier does not recognise is logged as billed by default, not free, so a brand-new response code errs toward overstating spend rather than hiding it. Within the known codes it is only as accurate as the endpoint/message table above, which is why that table is re-checked against the API's own documented behaviour rather than assumed to still be correct.

Does deduping my input ASIN list belong in this tool?

No. The ledger logs exactly what was sent to the API; removing duplicate ASINs before the call is the caller's job, same as the malformed-ASIN filter, because both save a credit before it is ever spent, not after.

Can I track spend across more than one day?

Yes, every row keeps its own day column, so report() for a single day is one query; summing across days or endpoints is a second query against the same table, not a separate tool.

Bottom line: log the billed-or-free outcome of every attempt inside the retry loop, not around it, classify a 400 by endpoint and message rather than trusting the status code alone, and default an unrecognised status to billed so the ledger never quietly under-counts real spend. For planning a budget before making a single call, use the pricing calculator; a free account includes 150 credits to try both.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime