Saltar al contenido principal

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.

EP-0EP-7

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".

step1_fetch.py
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 ads

Step 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.

step2_analyze.py
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 None

Step 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.

step3_price.py
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 None

Step 4: Update Ad (EP-7 Price-Only Pattern)

Critical: The EP-7 body must contain only 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.
step4_update.py
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&timestamp={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:

step5_errors.py
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

View Pricing