Back to posts

Polymarket Balance/Allowance Error: A Fractional Shares Problem?

Hit a confusing error when placing orders on Polymarket:

PolyApiException[status_code=400, error_message={'error': 'not enough balance / allowance'}]

Firstly, I checked balance and allowances but everything looked fine. USDC balance sufficient. Allowances set correctly. It seemed that the error only appeared on specific markets, not others.

Short version: Position sizes aren't round numbers. Tried to sell 23 YES, actually held 22.99695 YES. Order rejected.

Not 100% confirmed this is the issue, but timing and behavior match up.

The misleading debug path

First instinct: check balance and allowances as suggested by folks online.

from py_clob_client.clob_types import AssetType, BalanceAllowanceParams

polymarket_client.clob_client.get_balance_allowance(
    params=BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
)

Output:

{
    "balance": "123456",  # plenty of USDC
    "allowances": {
        "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E": "115792...760179",  # Exchange
        "0xC5d563A36AE78145C45a50134d48A1215220f80a": "115792...639935",  # Operator
        "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296": "115792...639935",  # NegRisk
    }
}

Those allowances are uint256.max (unlimited). Balance is fine. Allowances are fine. But order still fails.

Found others online with similar error, but they had zero balance or missing allowances. Different issue. That was a rabbit hole.

Buy vs sell asymmetry

Next observation: error only happened when SELLING (I think, didn't do extensive testing). Buying worked fine.

Tried this:

  • SELL 23 YES @ 0.94 → error
  • BUY 23 NO @ 0.06 → works

Economically equivalent (buying NO at 0.06 is selling YES at 0.94), mechanically different.

Initial hypothesis: Sell requires holding the token, buy only needs USDC. Maybe I don't have enough YES tokens?

Checked positions. Had YES tokens. Should work.

Still failed.

The likely culprit: fractional position sizes

Looked closer at actual position sizes. Positions API returns floats:

positions_df = client.positions_df
print(positions_df.filter(pl.col("outcome") == "Yes").select(["title", "size"]))

# Output:
# title: "Will Gemini 3.0 be released by October 15?"
# size: 22.99695

Not 23. 22.99695.

When I tried to SELL 23 YES, the order was rejected. Makes sense if I only held 22.99695 - can't sell what you don't have.

Why aren't positions round numbers? Not entirely clear.

  • It's not fees - no fees on Polymarket at the time of writing
  • Rounding in the settlement process?
  • Partial fills?
  • Something else?

Doesn't really matter why. Point is: positions aren't the round numbers you might expect from your order sizes.

The fix

Query actual position sizes before selling:

positions = client.positions_df
yes_position = positions.filter(
    (pl.col("conditionId") == condition_id) &
    (pl.col("outcome") == "Yes")
)
actual_size = yes_position["size"][0]  # 22.99695, not 23

# Sell what you actually have
client.create_order(
    token_id=yes_token_id,
    price=0.94,
    size=actual_size,
    side="SELL"
)

After using actual position sizes, the error stopped appearing.

Why BUY works but SELL fails

When you BUY YES tokens:

  • Exchange checks your USDC balance
  • As long as price * size ≤ usdc_balance, order succeeds

When you SELL YES tokens:

  • Exchange checks your YES token balance
  • Token balance has fractional parts. Even if you place order for 10 shares, you might only end up with 9.999 for some reason.
  • If size > token_balance (even by 0.00305 etc.), order fails

The asymmetry: buying checks collateral, while selling checks tokens (which are not always exactly the amount you placed an order for).

Implications for programmatic trading

If you're placing orders programmatically, you might do:

# Buy 100 YES at 0.48
client.create_order(token_id=yes_id, price=0.48, size=100, side="BUY")

# Later, sell 100 YES at 0.52
client.create_order(token_id=yes_id, price=0.52, size=100, side="SELL")

Second order maybe fails. You might not have exactly 100 YES.

Better approach is to track actual positions:

# After buying, check what you actually received
positions = client.positions_df
actual_yes = positions.filter(
    (pl.col("asset") == yes_token_id)
)["size"][0]

# Sell your actual position
client.create_order(token_id=yes_id, price=0.52, size=actual_yes, side="SELL")

Or build in a buffer (though this doesn't account for partial fills):

import math

actual_yes = 99.97  # what you actually have
safe_size = math.floor(actual_yes)  # 99

# Sell 99, leaves 0.97 behind
client.create_order(token_id=yes_id, price=0.52, size=safe_size, side="SELL")

First approach (sell everything) is cleaner. Second approach (floor to integer) leaves dust that accumulates over time.

What I still don't know

I haven't traced through the full exchange mechanics to confirm why positions end up fractional. And if positions can be off by 0.00305, what about order sizes? Do they get rounded somewhere in the flow? Unknown.

Simple fix in the end (use actual position sizes), but it took a while to figure out. The balance/allowance checks were red herrings. The real issue was trying to sell slightly more than I held. If you hit this error and your balances/allowances look correct, check if your sell size matches (or is less than) your actual position size. Try doing something like size=math.floor(size*1000)/1000, for example, to be conservative.