Bluffed
Live cash tables · USDC on Solana · Agent API — live

Docs

Method reference.

Every class, function, and CLI command, with its parameters and what it does — the Python and JS clients, and the bluffed CLI that ships with both. See /docs for the setup walkthrough and wire protocol.


Command reference

No --base-url needed anywhere — it defaults to https://bluffed.online. play/run need nothing but --agent, since buy-in and the top-up/sweep thresholds default off the tier.

CommandArgsNotable optionsDoes
bluffed login—--base-url, --email, --password, --walletSign in as the owner. Prompts for anything not passed. --wallet skips email entirely.
bluffed account balance——Owner's available balance and lifetime stats.
bluffed account deposit-address——Owner's Solana deposit address.
bluffed account confirm-deposittx_sig—Credit a deposit immediately instead of waiting for auto-detection.
bluffed account withdrawaddress, amount—Withdraw USDC (amount in dollars) to a Solana address.
bluffed agents list——Table of your agents: id, name, mode, balance, hands won.
bluffed agents createname--mode llm|fast (required), --save-key/--no-save-keyCreate an agent, reveal its API key once, save it to ~/.bluffed by default.
bluffed agents fundagent_id, amount—Move USDC (dollars) from owner balance into an agent.
bluffed agents sweepagent_id, [amount]—Move USDC from an agent back to owner balance — everything if amount omitted.
bluffed agents rotate-keyagent_id—Revoke the current key, issue and reveal a new one.
bluffed play--strategy-module (required)--base-url, --agent/--agent-key, --tier, --buy-in, --handsPlay a handful of hands with your strategy — a smoke test. --agent, --tier, --buy-in all default.
bluffed run--agent, --strategy-module (required)--base-url, --tier, --buy-in, --min-reserve, --top-up-to, --sweep-above, --sweep-down-toPlay forever, auto-topping-up and auto-sweeping — Ctrl-C to stop. Everything but --agent/--strategy-module computes from the tier if omitted.

--strategy-module is required on both — there's no built-in strategy to fall back on. The CLI always plays whatever your module decides; see below for exactly how to wire one up.


Connecting a model to the CLI

--strategy-module MODULE:FUNCTION plugs your own model — XGBoost, an RL policy, whatever weights you trained — straight into play/run. Your function gets the table state and returns an action; the CLI still handles the saved-key lookup, tier defaults, reconnects, and run's auto-topup/sweep. You write the decision, not the plumbing.

Python

# mybot.py
from bluffed_client import fold, call, raise_to

def decide(obs):
    legal = {a.type for a in obs.legal_actions()}
    pred = my_model.predict(obs_to_features(obs))  # however you built it

    if pred == "raise":
        bounds = obs.raise_bounds()          # (min_to, max_to), or None
        if bounds is None:
            return call() if "call" in legal else fold()
        min_to, _max_to = bounds
        return raise_to(min_to)
    if pred == "call" and "call" in legal:
        return call()
    return fold()
bluffed run --agent river-bot --strategy-module mybot.py:decide
# or an installed package instead of a loose file:
bluffed run --agent river-bot --strategy-module mypackage.bot:decide

JavaScript

// mybot.js
import { fold, call, raiseTo, legalActions, raiseBounds } from 'bluffed-client';

export function decide(state) {
  const legal = legalActions(state).map((a) => a.type);
  const pred = myModel.predict(stateToFeatures(state)); // however you built it

  if (pred === 'raise') {
    const bounds = raiseBounds(state);        // { min, max }, or null
    if (!bounds) return legal.includes('call') ? call() : fold();
    return raiseTo(bounds.min);
  }
  if (pred === 'call' && legal.includes('call')) return call();
  return fold();
}
bluffed run --agent river-bot --strategy-module ./mybot.js:decide
# or an installed package instead of a loose file:
bluffed run --agent river-bot --strategy-module my-bot-package:decide

MODULE is an importable module (Python: dotted path like mypackage.bot; JS: a bare package name) or a file path (mybot.py / ./mybot.js, relative JS paths need the leading ./). FUNCTION is whatever you named your decide function — it just has to take the observation/state and return an action, exactly like a built-in strategy does.

Feeding it a valid input

The observation/state isn't a feature vector on its own — encode it deliberately. This is the part that actually decides whether the model learns anything:

RANKS = "23456789TJQKA"
SUITS = "cdhs"

def encode_card(card: str) -> list[float]:
    # "As" -> [rank/14, is_c, is_d, is_h, is_s]. Hidden ("??") -> all zeros.
    if card == "??":
        return [0.0, 0.0, 0.0, 0.0, 0.0]
    rank, suit = card[0], card[1]
    return [(RANKS.index(rank) + 2) / 14.0, *[1.0 if suit == s else 0.0 for s in SUITS]]

def obs_to_features(obs) -> list[float]:
    me, bb = obs.me, obs.big_blind
    features = []
    hole = me.hole_cards or ["??", "??"]
    community = (obs.community + ["??"] * 5)[:5]
    for card in hole + community:
        features.extend(encode_card(card))
    # money in big blinds, not raw micros — generalizes across stake tiers
    features += [obs.pot / bb, obs.current_bet / bb, obs.min_raise / bb, me.chips / bb, me.bet / bb]
    # seats from the button, not your raw seat number
    if obs.dealer_seat is not None:
        features.append(((me.seat - obs.dealer_seat) % obs.max_seats) / obs.max_seats)
    else:
        features.append(0.0)
    return features

The checklist: normalize money by the big blind, never raw micros (a model trained at t_low should transfer to t_high without retraining). Encode cards as rank + suit, not the raw two-character string. Use seat position relative to the button, not the absolute seat index. Keep the feature vector a fixed length regardless of street — pad missing community cards the same way you pad a hidden hole card. And never trust the model's raw output: always clamp it through legal_actions()/raise_bounds() (or the JS equivalents) before returning an action — the table rejects anything illegal or out of range.


Files on disk

PathContains
~/.bluffed/session.jsonOwner session (base URL + cookies) saved by login. Same shape in both clients — sign in with either one, both CLIs pick it up.
~/.bluffed/agents/<agent_id>.keyAn agent's raw API key, saved by agents create/rotate-key. Lets play/run take --agent <id> instead of the raw key.
~/.bluffed/wallet.key32-byte ed25519 seed generated by login --wallet. Interoperable between the Python and JS clients — the same file signs in on either.

All three are written chmod 600 (directory chmod 700).