Pricing Automation
How to build an automated repricing system for Binance C2C P2P ads. This guide covers the full repricing loop: fetching the order book, analyzing competition, calculating a target price, updating the ad, and handling every error case.
The Repricing Loop
┌────────────────────────────────────────────────────────────────┐ │ Repricing Loop (per ad) │ └────────────────────────────────────────────────────────────────┘ Every N seconds: ───────────────────────────────────────────── 1. Fetch order book EP-0 BAPI search 2. Filter competitors in-memory 3. Sort by price in-memory 4. Calculate target strategy logic 5. Update ad price EP-7 /ads/update ───────────────────────────────────────────── Error handling on EP-7: 187049 ──▶ Remove surplusAmount, retry 187055 ──▶ Binary search valid price, retry -1021 ──▶ Sync time (UTIL-1), rebuild, retry 83229 ──▶ Mark ad paused, skip
Step 1: Fetch the Order Book (EP-0)
The order book is the raw competitive data. For a SELL ad (you're selling USDT), fetch tradeType=BUY — those are the sellers the buyer sees, which is the list your ad competes in. Always pass publisherType: "merchant".
import httpx
async def fetch_competitors(asset: str, fiat: str) -> list[dict]:
"""Fetch page 1-2 of sellers (= competitors for your SELL ad)."""
url = "https://p2p.binance.com/bapi/c2c/v2/friendly/c2c/adv/search"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Content-Type": "application/json",
}
ads: list[dict] = []
async with httpx.AsyncClient() as client:
for page in range(1, 3): # 40 ads total
resp = await client.post(url, headers=headers, json={
"asset": asset,
"fiat": fiat,
"tradeType": "BUY", # returns SELL ads = competitors
"page": page,
"rows": 20,
"publisherType": "merchant",
})
data = resp.json().get("data", [])
if not data:
break
ads.extend(data)
return adsStep 2: Analyze Competition
Filter and sort the raw order book data to extract useful competitive signals. Key filters: exclude your own ads, exclude ads with incompatible payment methods, exclude very small or very large surplus amounts.
def analyze_order_book(
ads: list[dict],
my_adv_nos: set[str],
payment_filter: list[str] | None = None,
min_surplus: float = 100.0,
) -> list[dict]:
"""Filter and sort competitors from raw order book."""
result = []
for ad in ads:
adv_no = ad.get("advNo") or ad.get("adv", {}).get("advNo")
# Exclude own ads
if adv_no in my_adv_nos:
continue
price = float(ad.get("adv", {}).get("price") or ad.get("price", 0))
surplus = float(
ad.get("adv", {}).get("tradableQuantity")
or ad.get("tradableQuantity", 0)
)
# Exclude thin ads
if surplus < min_surplus:
continue
# Optional: payment method filter
if payment_filter:
methods = [
m.get("identifier")
for m in (ad.get("adv", {}).get("tradeMethods") or [])
]
if not any(m in payment_filter for m in methods):
continue
result.append({
"advNo": adv_no,
"price": price,
"surplus": surplus,
})
# Sort ascending by price (cheapest seller first)
return sorted(result, key=lambda x: x["price"])
def best_ask(competitors: list[dict]) -> float | None:
"""Return the lowest competitor price."""
return competitors[0]["price"] if competitors else NoneStep 3: Calculate Target Price
Target price depends on your strategy. Common approaches: undercut the best ask by a fixed spread, position yourself at rank N in the order book, or float above a reference floor. Always quantize to the correct decimal precision for the fiat currency.
from decimal import Decimal, ROUND_DOWN
# Per-currency quantization (examples)
CURRENCY_PRECISION: dict[str, str] = {
"USD": "0.01",
"ARS": "0.01",
"COP": "1",
"CLP": "1",
"UYU": "0.01",
"CRC": "1",
}
def quantize(price: float, fiat: str) -> str:
"""Round to correct decimal places for fiat currency."""
precision = CURRENCY_PRECISION.get(fiat, "0.01")
return str(Decimal(str(price)).quantize(Decimal(precision), rounding=ROUND_DOWN))
def calculate_target_price(
competitors: list[dict],
my_current_price: float,
strategy: str = "undercut",
spread: float = 0.01,
target_rank: int = 1,
fiat: str = "USD",
) -> str | None:
"""Calculate new target price.
strategy="undercut": match best ask minus spread
strategy="position": price to achieve target_rank in book
"""
if not competitors:
return None # No data — don't update
if strategy == "undercut":
best = competitors[0]["price"]
target = best - spread
return quantize(target, fiat)
if strategy == "position":
# Price just below the competitor at target_rank position
if len(competitors) >= target_rank:
anchor = competitors[target_rank - 1]["price"]
target = anchor - spread
return quantize(target, fiat)
# Not enough competitors — undercut best
return quantize(competitors[0]["price"] - spread, fiat)
return NoneStep 4: Update Ad (EP-7 Price-Only Pattern)
advNo + price. Adding any other field (especially surplusAmount) causes error 187049 because Binance validates surplus against a cached value that may differ from your current balance.import hmac
import hashlib
import time
import httpx
async def update_price(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
price: str, # already quantized string
) -> dict:
"""EP-7: Price-only update."""
timestamp = int(time.time() * 1000)
query = f"recvWindow=60000×tamp={timestamp}"
signature = hmac.new(
api_secret.encode(), query.encode(), hashlib.sha256
).hexdigest()
resp = await client.post(
f"https://api.binance.com/sapi/v1/c2c/ads/update?{query}&signature={signature}",
# CRITICAL: ONLY advNo + price. Nothing else.
json={"advNo": adv_no, "price": price},
headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
)
return resp.json()Step 5: Handle Errors
Three errors are specific to the repricing path. Each has a deterministic recovery strategy:
import asyncio
import httpx
async def reprice_with_recovery(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
price: str,
fiat: str,
) -> bool:
"""Full repricing with error recovery."""
for attempt in range(5):
result = await update_price(client, api_key, api_secret, adv_no, price)
code = str(result.get("code", ""))
if result.get("success"):
return True
if code in ("-1021", "-1022"):
# Clock drift — sync time and rebuild timestamp
server_time = await get_server_time(client)
# Use server_time to generate fresh timestamp on next loop
await asyncio.sleep(0.1)
continue
if code == "187049":
# surplusAmount was in body — this should never happen
# if following the price-only pattern, but handle defensively
raise ValueError(
"187049: Remove surplusAmount from EP-7 body"
)
if code == "187055":
# Price outside allowed range
price = await find_valid_price(
client, api_key, api_secret, adv_no, float(price), fiat
)
continue
if code in ("83229", "83230"):
# Ad taken offline by Binance
print(f"Ad {adv_no} is offline — skipping reprice")
return False
if code in ("-1000", "429", "-9000"):
# Backoff and retry
backoff = min(2 ** attempt * 0.5, 30.0)
await asyncio.sleep(backoff)
continue
# Unknown error
print(f"Reprice error {code}: {result.get('message')}")
return False
return False
async def find_valid_price(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
current_price: float,
fiat: str,
) -> str:
"""Binary search for a valid price when 187055 is returned.
Walk price up in steps until a non-overlapping price is found.
In production, fetch the dynamic range from Binance first.
"""
step = {"USD": 0.01, "ARS": 1.0, "COP": 100.0, "CLP": 10.0}.get(fiat, 0.01)
for i in range(50): # max 50 steps
candidate = current_price + (step * i)
result = await update_price(
client, api_key, api_secret, adv_no, quantize(candidate, fiat)
)
if result.get("success"):
return quantize(candidate, fiat)
if str(result.get("code")) != "187055":
break # Different error — stop searching
raise RuntimeError(f"Could not find valid price near {current_price} {fiat}")
async def get_server_time(client: httpx.AsyncClient) -> int:
resp = await client.get("https://api.binance.com/api/v3/time")
return resp.json()["serverTime"]AutoP2P does all this in <1 second
The repricing loop described in this guide — order book fetch, competition analysis, price quantization, EP-7 price-only update, and all error recovery — runs in AutoP2P in under 1 second per ad cycle. It handles multiple ads in parallel across multiple exchanges and currencies, with a built-in rate limiter and circuit breaker.
<1s
Reprice latency
Automatic
Error recovery
3
Rate limit layers