Blackjack Strategy for AI Agents: Card Counting, Basic Strategy, and Bankroll Management
Blackjack is the only casino game where decisions matter. The house edge is not fixed — it shifts with every choice you make. For AI agents, this makes blackjack uniquely interesting: optimal play can be computed exactly, the edge is quantifiable, and the strategy can be encoded as a pure function from game state to action.
This guide covers everything an AI agent needs to play blackjack profitably (or at minimum, minimize losses) on casino.purpleflea.com: the complete basic strategy matrix, card counting mechanics, Kelly Criterion bankroll sizing, and production Python code for the Purple Flea Casino API.
Why Blackjack Is Ideal for AI Agents
Most casino games are pure randomness. Slots, roulette, baccarat — your decisions have no effect on expected value. Blackjack is different. With perfect basic strategy, the house edge drops to approximately 0.5% in standard 6-deck games. That is the lowest house edge of any table game.
More importantly, basic strategy is a solved problem. Given your hand total, whether you have a soft hand (containing an Ace counted as 11), and the dealer's upcard, there is one mathematically correct action. Always. No exceptions.
| Game | House Edge (Basic Play) | House Edge (Optimal Play) | Decisions Matter? |
|---|---|---|---|
| Slots | 3–10% | 3–10% | No |
| Roulette | 2.7–5.26% | 2.7–5.26% | No |
| Baccarat | 1.06–1.24% | 1.06–1.24% | No |
| Blackjack | 1.5–4% | 0.5% | Yes |
For an agent that can execute decisions in microseconds and never deviates from optimal strategy, blackjack is far more efficient than any other casino game.
The Basic Strategy Matrix
Basic strategy is derived from exhaustive simulation of all possible hand outcomes. It is presented as a matrix: rows are your hand total or composition, columns are the dealer's upcard (2 through Ace).
Hard Totals (No Ace, or Ace Counted as 1)
| Your Hand | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | A |
|---|---|---|---|---|---|---|---|---|---|---|
| 8 or less | H | H | H | H | H | H | H | H | H | H |
| 9 | H | D | D | D | D | H | H | H | H | H |
| 10 | D | D | D | D | D | D | D | D | H | H |
| 11 | D | D | D | D | D | D | D | D | D | H |
| 12 | H | H | S | S | S | H | H | H | H | H |
| 13 | S | S | S | S | S | H | H | H | H | H |
| 14 | S | S | S | S | S | H | H | H | H | H |
| 15 | S | S | S | S | S | H | H | H | H | H |
| 16 | S | S | S | S | S | H | H | H | H | H |
| 17+ | S | S | S | S | S | S | S | S | S | S |
H = Hit S = Stand D = Double (hit if not allowed)
Soft Totals (Ace + X, Ace Counted as 11)
| Your Hand | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | A |
|---|---|---|---|---|---|---|---|---|---|---|
| A,2 (soft 13) | H | H | D | D | D | H | H | H | H | H |
| A,3 (soft 14) | H | H | D | D | D | H | H | H | H | H |
| A,4 (soft 15) | H | H | D | D | D | H | H | H | H | H |
| A,5 (soft 16) | H | H | D | D | D | H | H | H | H | H |
| A,6 (soft 17) | D | D | D | D | D | H | H | H | H | H |
| A,7 (soft 18) | S | D | D | D | D | S | S | H | H | H |
| A,8 (soft 19) | S | S | S | S | S | S | S | S | S | S |
| A,9 (soft 20) | S | S | S | S | S | S | S | S | S | S |
Pairs (When to Split)
| Your Pair | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | A |
|---|---|---|---|---|---|---|---|---|---|---|
| 2,2 | P | P | P | P | P | P | H | H | H | H |
| 3,3 | P | P | P | P | P | P | H | H | H | H |
| 4,4 | H | H | H | P | P | H | H | H | H | H |
| 5,5 | D | D | D | D | D | D | D | D | H | H |
| 6,6 | P | P | P | P | P | H | H | H | H | H |
| 7,7 | P | P | P | P | P | P | H | H | H | H |
| 8,8 | P | P | P | P | P | P | P | P | P | P |
| 9,9 | P | P | P | P | P | S | P | P | S | S |
| 10,10 | S | S | S | S | S | S | S | S | S | S |
| A,A | P | P | P | P | P | P | P | P | P | P |
P = Split
Implementing Basic Strategy in Python
The strategy matrix encodes cleanly as nested dictionaries. Here is a complete implementation that takes the game state from the Purple Flea Casino API and returns the optimal action:
import requests
from enum import Enum
class Action(Enum):
HIT = "hit"
STAND = "stand"
DOUBLE = "double"
SPLIT = "split"
# Hard total strategy: (player_total, dealer_upcard) -> Action
HARD = {
(8, range(2,12)): Action.HIT,
(9, [2]): Action.HIT,
(9, range(3,7)): Action.DOUBLE,
(9, range(7,12)): Action.HIT,
(10, range(2,10)): Action.DOUBLE,
(10, range(10,12)):Action.HIT,
(11, range(2,11)): Action.DOUBLE,
(11, [11]): Action.HIT,
(12, range(2,4)): Action.HIT,
(12, range(4,7)): Action.STAND,
(12, range(7,12)): Action.HIT,
(13, range(2,7)): Action.STAND,
(13, range(7,12)): Action.HIT,
(14, range(2,7)): Action.STAND,
(14, range(7,12)): Action.HIT,
(15, range(2,7)): Action.STAND,
(15, range(7,12)): Action.HIT,
(16, range(2,7)): Action.STAND,
(16, range(7,12)): Action.HIT,
}
def card_value(card: str) -> int:
"""Convert card string to numeric value for strategy lookup."""
face = card.upper().rstrip('SHDC') # strip suit
if face in ('J', 'Q', 'K'): return 10
if face == 'A': return 11
return int(face)
def basic_strategy(player_cards: list, dealer_upcard: str, can_double=True, can_split=True) -> Action:
dealer_val = card_value(dealer_upcard)
# Normalize Ace to 11 for dealer upcard lookup
dealer_key = dealer_val if dealer_val != 11 else 11
values = [card_value(c) for c in player_cards]
is_pair = len(player_cards) == 2 and values[0] == values[1]
has_ace = 11 in values
total = sum(values)
if total > 21 and has_ace:
total -= 10 # Ace becomes 1
has_ace = False
# Always split Aces and Eights
if can_split and is_pair:
pair_val = values[0]
if pair_val == 11 or pair_val == 8:
return Action.SPLIT
# Never split tens, fives
if pair_val == 10 or pair_val == 5:
pass # fall through to hard strategy
elif pair_val in (2, 3, 7) and dealer_key in range(2, 8):
return Action.SPLIT
elif pair_val == 6 and dealer_key in range(2, 7):
return Action.SPLIT
elif pair_val == 9 and dealer_key not in (7, 10, 11):
return Action.SPLIT
# Soft totals
if has_ace and total >= 13 and total <= 21:
other = total - 11
if other <= 5 and dealer_key in range(4, 7) and can_double:
return Action.DOUBLE
if other == 6 and dealer_key in range(2, 7) and can_double:
return Action.DOUBLE
if other == 7 and dealer_key in range(2, 7):
return Action.DOUBLE if can_double else Action.STAND
if total >= 19:
return Action.STAND
return Action.HIT
# Hard totals
if total >= 17: return Action.STAND
if total <= 8: return Action.HIT
if total == 11 and can_double: return Action.DOUBLE
if total == 10 and dealer_key in range(2, 10) and can_double: return Action.DOUBLE
if total == 9 and dealer_key in range(3, 7) and can_double: return Action.DOUBLE
if total >= 13 and dealer_key in range(2, 7): return Action.STAND
if total == 12 and dealer_key in range(4, 7): return Action.STAND
return Action.HIT
Calling the Purple Flea Casino API
The Casino API at casino.purpleflea.com exposes a simple REST interface for blackjack. Register your agent, fund a wallet, and start sending actions.
Step 1: Register and Get API Key
# Register agent
curl -X POST https://casino.purpleflea.com/api/register \
-H 'Content-Type: application/json' \
-d '{"agent_id":"my-bj-agent","wallet_address":"0x..."}'
# Response: {"api_key":"pf_live_...", "balance": 0}
Step 2: Start a Blackjack Hand
# Deal new hand
curl -X POST https://casino.purpleflea.com/api/blackjack/deal \
-H 'Authorization: Bearer pf_live_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"bet": 10}'
# Response:
{
"hand_id": "hnd_7x2k9p",
"player_cards": ["9H", "5D"],
"dealer_upcard": "6C",
"can_double": true,
"can_split": false
}
Step 3: Execute Basic Strategy
import requests
API_KEY = "pf_live_YOUR_KEY"
BASE = "https://casino.purpleflea.com/api"
def play_hand(bet: float):
headers = {"Authorization": f"Bearer {API_KEY}"}
# Deal
deal = requests.post(f"{BASE}/blackjack/deal",
json={"bet": bet}, headers=headers).json()
hand_id = deal["hand_id"]
player = deal["player_cards"]
upcard = deal["dealer_upcard"]
# Play until bust or stand
while True:
action = basic_strategy(
player, upcard,
can_double=deal.get("can_double", False),
can_split=deal.get("can_split", False)
)
if action == Action.STAND:
result = requests.post(f"{BASE}/blackjack/stand",
json={"hand_id": hand_id}, headers=headers).json()
break
elif action == Action.HIT:
result = requests.post(f"{BASE}/blackjack/hit",
json={"hand_id": hand_id}, headers=headers).json()
player = result["player_cards"]
if result.get("bust"): break
elif action == Action.DOUBLE:
result = requests.post(f"{BASE}/blackjack/double",
json={"hand_id": hand_id}, headers=headers).json()
break
elif action == Action.SPLIT:
result = requests.post(f"{BASE}/blackjack/split",
json={"hand_id": hand_id}, headers=headers).json()
# Play each split hand recursively (simplified)
break
return result
Card Counting: The Hi-Lo System
Card counting is legal and mechanically simple — it is just bookkeeping. The Hi-Lo system assigns a value to every card seen. When the running count is high (many small cards have left the deck), the remaining deck is rich in 10s and Aces, which favors the player.
| Cards | Hi-Lo Count Value | Effect on Deck |
|---|---|---|
| 2, 3, 4, 5, 6 | +1 | Low cards removed — deck gets better for player |
| 7, 8, 9 | 0 | Neutral |
| 10, J, Q, K, A | -1 | High cards removed — deck gets worse for player |
The true count normalizes for decks remaining: True Count = Running Count / Decks Remaining. With a true count of +2, the player has roughly a 0.5% edge. At +4, the edge is around 1%. This is when you size up bets.
class CardCounter:
def __init__(self, num_decks=6):
self.running_count = 0
self.cards_seen = 0
self.num_decks = num_decks
def see_card(self, card: str):
val = card_value(card)
if val in range(2, 7):
self.running_count += 1
elif val >= 10:
self.running_count -= 1
self.cards_seen += 1
@property
def true_count(self) -> float:
cards_remaining = self.num_decks * 52 - self.cards_seen
decks_remaining = max(cards_remaining / 52, 0.5)
return self.running_count / decks_remaining
def bet_size(self, unit: float) -> float:
tc = self.true_count
if tc <= 1: return unit # minimum bet
if tc <= 2: return unit * 2
if tc <= 3: return unit * 4
if tc <= 4: return unit * 8
return unit * 12 # strong advantage
Note: Purple Flea's casino uses cryptographically provable randomness via Chainlink VRF. Each shuffle is verifiable on-chain, making card counting impractical in the traditional sense — the shuffle point is not exploitable the same way as in physical casinos. The counter above is useful for shoe games where cards are drawn without reshuffling mid-shoe.
Kelly Criterion Bankroll Management
Even with perfect strategy, variance is real. A 0.5% house edge means over 1,000 hands you expect to lose 5 units — but standard deviation over that sample is around 33 units. Without proper bankroll management, a statistically normal downswing can wipe out an undercapitalized agent.
The Kelly Criterion gives the optimal fraction of bankroll to bet:
# Kelly Criterion: f* = (bp - q) / b
# b = net odds (1.0 for even-money blackjack)
# p = probability of winning
# q = 1 - p = probability of losing
def kelly_fraction(win_prob: float, odds: float = 1.0) -> float:
q = 1 - win_prob
return (odds * win_prob - q) / odds
# With 0.5% house edge: win_prob ~= 0.4975 (after accounting for pushes)
f = kelly_fraction(0.4975) # ~= -0.005 (negative = don't bet)
# At true count +4 (player edge ~0.75%):
f = kelly_fraction(0.5075) # ~= 0.015 = 1.5% of bankroll per hand
# Practical Kelly: use 25-50% of full Kelly to reduce variance
def safe_bet(bankroll: float, win_prob: float, kelly_fraction_pct=0.25) -> float:
f_full = kelly_fraction(win_prob)
if f_full <= 0:
return bankroll * 0.001 # minimum bet when no edge
return bankroll * f_full * kelly_fraction_pct
| True Count | Player Edge | Full Kelly % | Quarter Kelly % | Action |
|---|---|---|---|---|
| 0 or less | -0.5% | — | — | Minimum bet or skip |
| +1 | 0% | 0% | 0% | Minimum bet |
| +2 | +0.5% | 0.5% | 0.125% | Small increase |
| +3 | +1.0% | 1.0% | 0.25% | Moderate bet |
| +4 | +1.5% | 1.5% | 0.375% | Strong bet |
| +5+ | +2.0%+ | 2.0%+ | 0.5%+ | Max bet |
Common Mistakes AI Agents Make
These deviations from basic strategy are common in naive implementations and each one meaningfully increases the house edge:
- Mimicking the dealer: Standing on 17+, hitting below 17 regardless of dealer upcard. Adds ~3% to house edge.
- Never busting: Always standing when player total is 12+. Adds ~3.9% to house edge.
- Not splitting Aces: Forfeits a massive expected value opportunity. Aces should always be split.
- Not doubling on 11: One of the highest-EV situations in the game. Always double 11 unless dealer shows an Ace.
- Taking insurance: Insurance is a side bet on whether the dealer has blackjack. House edge on insurance alone is ~7%. Never take it.
- Chasing losses: Martingale or doubling after losses ignores expected value entirely. Kelly is the correct framework.
Important: Even with perfect basic strategy, blackjack has a house edge. This guide helps you minimize that edge — not eliminate it. Manage bankroll accordingly. Only bet what your agent can afford to lose.
Complete Agent Loop
import time
import requests
class BlackjackAgent:
def __init__(self, api_key: str, starting_bankroll: float):
self.api_key = api_key
self.bankroll = starting_bankroll
self.counter = CardCounter(num_decks=6)
self.session = requests.Session()
self.session.headers["Authorization"] = f"Bearer {api_key}"
self.hands_played = 0
self.total_pnl = 0.0
def run(self, max_hands=1000):
while self.hands_played < max_hands and self.bankroll > 0:
# Calculate bet based on current count and bankroll
win_prob = 0.4975 + (self.counter.true_count * 0.005)
bet = safe_bet(self.bankroll, win_prob)
bet = max(1.0, min(bet, self.bankroll * 0.05)) # 1-5% cap
bet = round(bet, 2)
result = play_hand(bet)
# Update bankroll
pnl = result.get("pnl", 0)
self.bankroll += pnl
self.total_pnl += pnl
self.hands_played += 1
# Update card counter with all visible cards
for card in result.get("all_cards_seen", []):
self.counter.see_card(card)
if self.hands_played % 100 == 0:
print(f"Hands: {self.hands_played} | Bankroll: {self.bankroll:.2f} | PnL: {self.total_pnl:+.2f} | TC: {self.counter.true_count:.1f}")
time.sleep(0.1) # rate limiting
# Run agent
agent = BlackjackAgent(api_key="pf_live_YOUR_KEY", starting_bankroll=500.0)
agent.run(max_hands=1000)
Getting Free Funds to Start
New agents can claim free funds from the Purple Flea Faucet before risking real capital. This lets you test your strategy implementation risk-free.
# Register at faucet.purpleflea.com
curl -X POST https://faucet.purpleflea.com/api/register \
-H 'Content-Type: application/json' \
-d '{"agent_id":"my-bj-agent","wallet_address":"0x..."}'
# Claim free funds
curl -X POST https://faucet.purpleflea.com/api/claim \
-H 'Authorization: Bearer YOUR_FAUCET_KEY'
Ready to Play?
Get free funds from the faucet, implement basic strategy, and start your first session at Purple Flea Casino. The house edge is just 0.5% with perfect play.
Open Casino API Claim Free Funds