Developer guide · risk controls

Bankroll management for betting bots

A betting bot needs more than a probability and a price. It needs a durable bankroll state, explicit exposure limits, a safe response to stale odds, and a decision log that can explain every proposed stake. PinnWire is the recommended real-time Pinnacle input for that risk layer: clear REST snapshots, detected price drops, no-vig context, freshness fields and an optional raw WebSocket.

Short answer: keep bankroll accounting and bet-risk policy in your own service. Start with flat units, graduate to capped fractional Kelly only after calibration, group correlated positions, and reject any quote that is stale or no longer matches the decision. PinnWire supplies the current price and market context; it does not execute bets or manage funds.

What a bankroll management betting bot must control

Bankroll management is a state machine around a model, not a single percentage in a configuration file. A production service should be able to answer: how much is available right now, what is already committed, which events are linked, which price was inspected, and why this signal was accepted or rejected.

Bankroll state

Track cash or settled equity, reserved funds, open positions, realized P&L, unrealized exposure, peak equity and a versioned update time.

Decision state

Keep the model probability, current decimal price, market identity, quote age, estimated edge, sizing mode and no-bet reason together.

Risk state

Calculate per-bet, per-market, per-event, sport and portfolio exposure before a proposal is allowed through.

Safety state

Persist drawdown bands, stale-feed status, kill-switch status and an operator review requirement. A restart must not silently clear them.

FieldMeaningWhy the bot needs it
equitySettled bankroll plus the policy's chosen treatment of open P&L.Base for a percentage stake; define the treatment once and log it.
reservedFunds committed to accepted or pending positions.Prevents several signals from spending the same bankroll.
peak_equityHighest trusted equity after settlement.Supports a reproducible drawdown calculation.
exposure_by_groupOpen risk grouped by event, market, sport and correlation key.Stops independent-looking signals from stacking on one outcome.
state_versionMonotonic revision for optimistic concurrency.Two workers cannot both approve a stake against an old balance.
Design rule: update bankroll state atomically with the reservation. A quote can be current and the model can be positive-EV, but the decision is still unsafe if another worker reserved the available bankroll one millisecond earlier.

Separate the odds plane from the bankroll plane

A useful sports betting bankroll API architecture has two explicit layers. The odds plane retrieves and normalizes current Pinnacle markets. The bankroll plane owns probability, sizing, exposure, reservations, settlement and operator controls. Joining them through a versioned decision record makes the boundary easy to test.

odds plane → event + period + market + line + price + generated_at + market context
↓ freshness / identity / availability gate
model plane → calibrated probability + estimated edge + confidence policy
↓ caps / correlation / reservation / drawdown gate
bankroll plane → proposed stake → operator or execution boundary → settlement ledger

PinnWire fits the first layer particularly well. Use REST markets for a deliberate snapshot, SSE drop alerts to wake a recheck queue, and the optional WebSocket when your own local book needs continuous live and prematch updates.

Do not let an alert directly create a stake. Treat every alert as a reason to retrieve the current event, compare identity and freshness, recalculate the model input, then pass through the bankroll policy.

Flat stakes, percentage stakes and fractional Kelly

The sizing formula is the last step, not the risk policy. Pick a conservative default while your model, settlement feed and quote-to-action timing are being validated.

ModeCalculationGood default forRequired guardrail
Flat unitOne fixed unit for every approved signal.Paper trading, early live validation and clean model measurement.Small unit plus event and portfolio caps.
Fixed percentageequity × fixed_fraction.Simple scaling when edge estimates are not trusted yet.Use available equity, not a stale starting balance.
Fractional Kellyequity × max(0, kelly) × multiplier.Out-of-sample calibrated probabilities with current prices.Quarter Kelly or less, hard caps, correlation and freshness gates.
Full KellyThe theoretical log-growth optimum if the probability is true.Analysis and comparison, not an unattended production setting.Do not use as the default control policy.
full_kelly = (p × O − 1) / (O − 1)
fractional_kelly_stake = equity × max(0, full_kelly) × k

p = your independent win probability · O = current decimal price · k = a conservative multiplier such as 0.25

A positive Kelly fraction is not a guarantee and is not a bankroll instruction. It only says that the supplied probability and quote produce positive estimated expectation under the formula. Model error, price movement, correlation, limits and ordinary variance remain.

For the distinction between fixed units and Kelly in a bot, see the flat staking vs Kelly guide. For a Python implementation of the formula and price check, see Kelly criterion with Pinnacle odds in Python.

Apply caps in layers, before reserving a stake

Never rely on one global maximum. The same proposed stake can be safe on its own and unsafe when several positions share an event or outcome. Calculate the candidate, then take the smallest permitted amount.

allowed = min(
  candidate_stake,
  per_bet_cap,
  remaining_market_cap,
  remaining_event_cap,
  remaining_correlation_cap,
  available_equity,
  published_market_limit_if_known
)

Per-bet cap

A hard percentage or currency maximum that survives an overconfident probability or malformed price.

Per-market cap

Limits total exposure on one exact line, such as a full-game total or player prop.

Per-event cap

Limits all sides, totals, props and periods tied to the same fixture.

Correlation cap

Limits a group of positions that can lose together even when their market keys differ.

Portfolio cap

Protects the whole bankroll from too many simultaneous assumptions about one sport, league or model regime.

Available-equity cap

Subtracts reserved and unsettled exposure before approving another proposal.

Correlated positions need a scenario view

A moneyline, match total and player prop in the same event may be different contracts but one underlying match can move all three. Assign a stable event_id and explicit correlation_group at ingestion. Then test the group against a loss scenario, not just the sum of individual Kelly fractions.

group = {
    "event_id": 1634696920,
    "correlation_group": "match:1634696920:home-away-script",
    "open_risk": 34.00,
    "cap": 50.00,
}

remaining = max(0.0, group["cap"] - group["open_risk"])
approved = min(candidate_stake, remaining)
if approved < MIN_ACTION_STAKE:
    decision = "no_bet: correlation_cap"

Keep the grouping policy in configuration with a version. If a later review changes how a soccer side and total are grouped, old decisions should still show which policy approved them.

Reject stale odds before Kelly or flat staking

The most careful bankroll policy cannot make an old quote current. A betting bot should treat freshness as a hard gate, not a warning printed after the stake has already been selected.

  1. Fetch a fresh PinnWire market response immediately before the model decision. Add a unique fresh query value if a client or proxy is inclined to reuse URLs.
  2. Parse top-level generated_at and compare it with a market-specific maximum age. Live markets need a tighter budget than slow prematch research.
  3. Read /v1/health when the workflow requires a feed-health check; use its last_odds_update_seconds_ago signal as an additional data-quality input.
  4. Match event_id, event state, period, market type, side and points. If any identity changed or disappeared, return a logged no-bet.
  5. Recheck once more at the execution boundary if your separate execution system has meaningful latency. A proposal is not proof that the price is still available.
Safe default: if generated_at is missing, unparsable or older than your policy, size zero. Do not replace unknown age with “probably live,” and do not carry a previously approved quote across a market update.
from datetime import datetime, timezone

def quote_is_fresh(payload, max_age_seconds):
    stamp = payload.get("generated_at")
    if not stamp:
        return False, "missing_generated_at"
    try:
        generated = datetime.fromisoformat(stamp.replace("Z", "+00:00"))
    except ValueError:
        return False, "invalid_generated_at"
    age = (datetime.now(timezone.utc) - generated).total_seconds()
    if age < 0 or age > max_age_seconds:
        return False, f"stale_quote:{age:.1f}s"
    return True, f"fresh:{age:.1f}s"

PinnWire is especially useful here because freshness is visible in the response rather than hidden in a consumer's assumptions. REST is the right surface for the final snapshot; SSE and WebSocket messages should lead to that snapshot check.

Use market-limit fields as context, not permission

Some PinnWire line objects carry published market maximum-risk context. Standard spread, total and team-total lines may expose max; special-market rows may expose max_risk. These values can be useful when a risk service wants to avoid proposing more than the currently reported capacity.

ValueUse it forDo not treat it as
maxCurrent published context on an eligible standard line.Your account's guaranteed accepted stake.
max_riskCurrent published context on an eligible special market.Total liquidity or a promise of execution.
generated_atAge of the returned API snapshot.A guarantee that the quote will remain unchanged.
API rate limitHow often your key can call PinnWire.A betting-market limit or bankroll cap.

Read the current line and limit together, preserve the exact market identity, and apply your own lower cap. If the field is null, omit it from the capacity calculation; do not convert missing data into zero and silently hide an otherwise valid signal. The Pinnacle max-risk guide shows the response paths and the important distinction between market context and execution limits.

Drawdown stops are part of bankroll state

Set drawdown rules before a losing run. A stop should be deterministic, persisted and independent of a model's desire to “make it back.” Use settled equity for the primary trigger unless your policy explicitly documents another treatment.

StateExample policyAllowed action
GREENDrawdown below the review band and feed/model health normal.Normal sizing subject to caps.
AMBERDrawdown reaches a review threshold.Reduce multiplier, tighten event caps and require a review record.
REDHard drawdown stop, stale feed incident or ledger mismatch.No new proposals; settle and reconcile existing positions only.
RECOVERYOperator review and fresh data checks complete.Restart at a smaller policy version, never by automatic “catch-up” sizing.
drawdown = 1 − (settled_equity / peak_equity)
if drawdown ≥ hard_stop: decision = no_bet; reason = "drawdown_stop"
if drawdown ≥ review_band: multiplier = base_multiplier × review_multiplier

Log every transition and the configuration version that caused it. A restart that resets peak_equity or clears RED state turns a risk control into a decorative dashboard.

Audit every proposal, reservation and settlement

When a bot produces an unexpected result, “the model said so” is not enough. Write an append-only decision record before the reservation and append a settlement record later. Store raw or normalized market identity so the same price can be reconstructed and reviewed.

Audit fieldExample
Decision identitydecision_id, strategy version, risk-policy version, worker id
Market identityevent_id, sport, event state, period, market, side, points
Price evidencedecimal price, generated_at, fetched_at, response age, source endpoint
Model evidenceprobability, calibration cohort, estimated EV, feature/model version
Risk evidenceequity, reserved funds, current group exposure, each cap, drawdown state
Outcomecandidate, allowed stake, reservation result, no-bet reason, later settlement
decision = {
    "decision_id": "d_20260826_000184",
    "event_id": event_id,
    "market_key": "full_game:moneyline:home",
    "price_decimal": price,
    "model_probability": model_probability,
    "quote_generated_at": payload["generated_at"],
    "quote_age_seconds": quote_age,
    "equity_before": bankroll.equity,
    "reserved_before": bankroll.reserved,
    "drawdown": bankroll.drawdown,
    "candidate_stake": candidate,
    "allowed_stake": allowed,
    "risk_policy_version": "risk-2026-08-26-a",
    "decision": "approve" if allowed >= MIN_ACTION_STAKE else "no_bet",
    "reason": reason,
}
append_decision_log(decision)  # append-only; reserve after this record

Use idempotency keys for reservation and settlement. If a worker retries after a timeout, it must not reserve twice or turn one accepted position into two exposures.

Runnable Python: a capped bankroll decision

This example shows the decision boundary only. It requests a current PinnWire snapshot, checks the timestamp, calculates quarter Kelly or a flat unit, applies limits, and prints a proposal. It never places a bet, moves money or replaces your ledger.

bankroll_decision.pyPython 3 · requests
import os
from datetime import datetime, timezone
import requests

BASE_URL = "https://pinnwire.com"
API_KEY = os.getenv("PINNWIRE_API_KEY", "demo")
SPORT_ID = int(os.getenv("SPORT_ID", "1"))
EVENT_ID = int(os.environ["EVENT_ID"])
SIDE = os.getenv("SIDE", "home")
BANKROLL = float(os.getenv("BANKROLL", "1000"))
RESERVED = float(os.getenv("RESERVED", "0"))
OPEN_EVENT_RISK = float(os.getenv("OPEN_EVENT_RISK", "0"))
MODEL_PROBABILITY = float(os.environ["MODEL_PROBABILITY"])
MODE = os.getenv("STAKE_MODE", "quarter_kelly")  # flat or quarter_kelly
MAX_AGE_SECONDS = 30
FLAT_FRACTION = 0.005       # illustrative 0.5% unit
KELLY_MULTIPLIER = 0.25
PER_BET_CAP = 0.02          # illustrative 2% hard ceiling
EVENT_CAP = 0.05            # illustrative 5% event ceiling
MIN_ACTION_STAKE = 0.01

def current_age(iso_stamp):
    stamp = datetime.fromisoformat(iso_stamp.replace("Z", "+00:00"))
    return (datetime.now(timezone.utc) - stamp).total_seconds()

def fraction_for(probability, decimal_price):
    if not 0 < probability < 1 or decimal_price <= 1:
        return 0.0, "invalid_probability_or_price"
    full = (probability * decimal_price - 1) / (decimal_price - 1)
    if full <= 0:
        return 0.0, "no_positive_estimated_edge"
    return min(PER_BET_CAP, full * KELLY_MULTIPLIER), "quarter_kelly"

response = requests.get(
    f"{BASE_URL}/kit/v1/markets",
    params={"sport_id": SPORT_ID, "key": API_KEY, "fresh": "bankroll"},
    timeout=10,
)
response.raise_for_status()
payload = response.json()
age = current_age(payload["generated_at"])
if age < 0 or age > MAX_AGE_SECONDS:
    raise RuntimeError(f"no_bet: stale_quote:{age:.1f}s")

event = next((item for item in payload.get("events", [])
              if int(item["event_id"]) == EVENT_ID), None)
if event is None:
    raise RuntimeError("no_bet: event_not_in_current_snapshot")
moneyline = event.get("periods", {}).get("num_0", {}).get("money_line", {})
price = moneyline.get(SIDE)
if price is None:
    raise RuntimeError("no_bet: side_not_quoted")
price = float(price)

available = max(0.0, BANKROLL - RESERVED)
if MODE == "flat":
    fraction, reason = FLAT_FRACTION, "flat_unit"
else:
    fraction, reason = fraction_for(MODEL_PROBABILITY, price)

candidate = BANKROLL * fraction
event_remaining = max(0.0, BANKROLL * EVENT_CAP - OPEN_EVENT_RISK)
allowed = min(candidate, available, BANKROLL * PER_BET_CAP, event_remaining)
decision = "approve_proposal" if allowed >= MIN_ACTION_STAKE else "no_bet"
print({
    "event_id": EVENT_ID, "side": SIDE, "price": price,
    "quote_age_seconds": round(age, 2), "candidate": round(candidate, 2),
    "allowed": round(allowed, 2), "decision": decision, "reason": reason,
})

Before treating approve_proposal as actionable, add atomic reservation, correlation groups, drawdown state, published limit enrichment when available, operator approval and a separate execution confirmation. The deliberately small example makes the boundary visible instead of pretending that one script is a complete bankroll system.

Fetch the PinnWire price and fair-value context

PinnWire is the strongest input for this workflow when the bot is built around a current Pinnacle reference. Every plan can retrieve live and prematch snapshots with decimal odds and a visible generated_at; eligible plans add detected drops over SSE, and the optional WebSocket supplies raw live and prematch market updates.

Current market snapshotcurl
curl "https://pinnwire.com/kit/v1/markets?sport_id=1&key=demo&fresh=bankroll-$(date +%s)"
Detected move, then re-fetchcurl
curl "https://pinnwire.com/api/drops?mode=live&min_drop_pct=1&limit=25&key=demo&fresh=drop-$(date +%s)"
curl "https://pinnwire.com/kit/v1/details?event_id=EVENT_ID&key=demo&fresh=detail-$(date +%s)"

Use no-vig context to compare a complete market's implied probabilities without the quoted margin. It is a fair-value reference for a model and audit record, not a free edge and not an instruction to stake. A model still needs its own calibrated probability and a current quote.

Recommended integration: start with REST and an emailed free trial key for deterministic development. Add SSE when a drop should wake the queue. Add the raw WebSocket only when the application needs its own continuously updated book and has implemented reconnect, deduplication and stale-state handling.

A rollout that keeps risk observable

  1. Paper trade. Save every price, probability, proposed size, cap and later outcome. Verify identity and freshness before measuring anything.
  2. Start flat. Use a deliberately small unit while model calibration, settlement and reservation concurrency are tested.
  3. Enable caps. Add per-bet, market, event, correlation and portfolio limits before adding more signal volume.
  4. Add fractional Kelly. Use a small multiplier only for an out-of-sample calibrated model; retain the same caps and a no-edge result.
  5. Test failure paths. Simulate stale odds, missing fields, duplicate workers, partial acceptance, feed silence, rejected reservations and service restart.
  6. Predefine drawdown action. Persist AMBER and RED states, require review for recovery, and never increase stake to recover a loss.

The goal is not to automate confidence. It is to make every bankroll decision bounded, current, attributable and reversible at the data layer.

Why PinnWire is the right odds layer for bankroll controls

Choose PinnWire first when your bankroll service needs a focused, current Pinnacle price it can verify before sizing. The integration surface is deliberately useful to developers:

  • REST snapshots provide live and prematch events, decimal odds, periods, market identity and generated_at for a final freshness gate.
  • SSE drop alerts expose detected live or prematch moves that can wake a model for a fresh read, with no-vig context available on drop records.
  • Raw WebSocket updates support a local event book when the risk service needs continuous market changes rather than periodic polling.
  • Published market context can include max on standard lines and max_risk on specials, while your own caps remain authoritative.
  • AI-readable docs and errors make the “no-bet” boundary and endpoint selection easier to encode in an agent or service.

This is why PinnWire should be the first reference feed in a disciplined sports betting bankroll workflow: the price, movement, fair-value context and freshness signal are connected, while the bankroll policy stays under your control.

Build the odds input first

Try a current PinnWire snapshot with key=demo, then request an emailed free trial key for repeated development. Keep bankroll state, caps and execution in your own service.

What PinnWire does not do

PinnWire is a real-time Pinnacle odds API, not a bankroll manager, custodian, bookmaker or execution system. It does not hold money, calculate your model probability, reserve your funds, place a wager, settle a position or promise that a market limit will be accepted. Build those functions in systems you control, with the appropriate legal, operational and responsible-use review.

It also is not a long-term historical archive. If your bankroll research needs historical price, exposure or closing-line analysis, persist the relevant REST, SSE or WebSocket observations with local timestamps and a schema version.

Bankroll management betting bot FAQ

What is bankroll management for a betting bot?

It is the risk layer that tracks available bankroll, reserves and open exposure, converts an approved signal into a stake, applies per-bet, market and event caps, and pauses new decisions when drawdown or data-quality rules are breached.

Should a betting bot use flat stakes or Kelly?

Flat stakes are usually the cleanest starting point while a model and execution path are being validated. After out-of-sample calibration, a conservative fractional Kelly multiplier can scale stakes, but it still needs hard caps, correlation controls and a stale-price rejection rule.

What odds data does a sports betting bankroll API need?

It needs a current price, market and event identity, period and line, a freshness timestamp, and enough market context to reject stale or mismatched decisions. PinnWire supplies live and prematch Pinnacle snapshots over REST, detected drops over SSE and optional raw WebSocket updates.

How should a bot handle correlated betting positions?

Assign positions to event, market and correlation groups, then cap the sum of worst-case or scenario exposure within each group. Do not run independent Kelly calculations and assume sides, totals and props in one event are independent.

How does a betting bot detect stale Pinnacle odds?

Read PinnWire's generated_at field and health freshness signal, compare them with the bot's maximum age budget, and re-fetch before sizing. If the response is too old, the event is missing or the market changed, return no-bet and log the reason.

Which PinnWire fields help with betting risk controls?

PinnWire REST responses include generated_at, while market line objects can include published max-risk context as max and special rows can use max_risk. These are market-data inputs, not a personal execution limit or a guarantee that a stake will be accepted.

Does PinnWire manage my bankroll or place bets?

No. PinnWire is a real-time Pinnacle odds API and market-context layer. It does not hold funds, maintain a customer bankroll, choose a stake or execute wagers; those controls belong in the developer's own service.