DEV Community

prodbymarcu
prodbymarcu

Posted on

I built an auction engine that refuses to bid for me, and the platform terms say why

I started an auction house for a selfish reason, the goods I make have no channel. One-off prints, ESP32 devices, speaker prototypes, the surplus in a homelab that gets upgraded every year. None of it has a storefront that fits, so I built the machinery before I built the inventory.

The engine is stdlib Python plus SQLite, and the interesting part isn't the scheduling, it's the one rule I refused to encode as prose.

The trap is older than the code

Every auction platform carries the same rule, and an autonomous agent breaks it the moment nobody tells it not to. HiBid's Seller Terms, item 7, "Buyback and Price Manipulation":

You shall not bid or make an offer, directly or indirectly, nor allow any other person to bid or make an offer on your behalf, by agency or otherwise, on your Listed Item(s) or any part of your auctions... we may ban you from future use.

eBay's shill-bidding rule says the same thing in different words, and both threaten the ban, a refund is not on the table. So the guard lives in the engine, at the identity layer, a prompt telling a model to behave would be a suggestion the model can ignore. An agent that "helps the price along" is the fastest route to a banned account, and a banned account on your real name is worse than a bad sale.

def place_bid(self, lot_no, bidder_id, amount, at):
    """Returns (accepted, reason). Every rejection is a real auction rule."""
    if bidder_id == self.seller_id:
        return False, "SELF_BID: a seller may not bid on their own lot, directly or by agency"
    lot = self.lot(lot_no)
    if lot is None:
        return False, "UNKNOWN_LOT"
    if at > self._dt(lot["closes_at"]):
        return False, "AFTER_CLOSE"
    cur = lot["current_bid"]
    # a reserve means the first legal bid is the reserve itself, not
    # reserve-minus-an-increment; after that, increments off the live price
    if cur == 0 and lot["reserve"] > 0:
        min_next = lot["reserve"]
    else:
        min_next = max(cur, lot["reserve"]) + increment_for(max(cur, lot["reserve"]))
    if amount < min_next:
        return False, f"UNDER_MINIMUM: next valid bid is {min_next}"
    if lot["high_bidder"] == bidder_id:
        return False, "SAME_BIDDER: raising against yourself is not a contest"
Enter fullscreen mode Exit fullscreen mode

Every rejection returns a reason string, so the suite can assert against the rules as written.

What the rules buy you

A reserve is the first legal bid, off-by-one here and a lot opens below reserve. An off-by-one here lets a lot open below reserve and reads as a rules violation to the platform, and that reads as a rules violation to the platform, which is how a listing gets suspended.

Increments follow price bands, the standard practice:

BANDS = [
    (0, 25, 1), (25, 50, 2), (50, 100, 5), (100, 250, 10),
    (250, 500, 25), (500, 1000, 50), (1000, 5000, 100),
    (5000, 10 ** 12, 500),
]
Enter fullscreen mode Exit fullscreen mode

Closes are staggered, never simultaneous. A single close invites sniping on every lot at once and makes the sale unwatchable, and lot order is the reverse of desirability so the best pieces close last.

A lot cannot exist without a condition report and a disclosure line. Those fields are non-optional in the engine, because a sloppy catalog has to fail loudly, a quiet lie about the goods is the thing that gets you in trouble. Seller terms also require that items are not stolen or counterfeit and that faults are disclosed plainly, the "sold AS IS, WHERE IS, with all its faults" convention.

Buyer's premium is the house's own fee and it's non-refundable by convention, so I set it explicitly with a minimum floor, a provider default here silently deletes the margin. HiBid charges bidders nothing and does not handle payments at all, the house sets its own premium and collects on its own card. If you don't set the premium, your margin doesn't exist.

The fee forensics, from the primary pages

venue house fee other
HiBid 2% of online hammer $0.25 per unique bid, capped $150 per auction, $75 webcast setup
AuctionFlex 360 subscription $95/mo Solo, $145 Team, $195 Business, $295 Enterprise
LiveAuctioneers 5% of hammer live event fees $525-895/day, payments 3.99% + 25c, plus a separate buyer-side fee
Proxibid 5% arts/antiques/firearms, 3% vehicles, 2% farm equipment
Invaluable 5% down to 1% by hammer size acquired by Liquidity Services 2026-10-01
eBay auction-style ~13% all-in automatable through his own API
Own platform zero commission Circuit, BidJS (two script tags), Bidsquare, BidWrangler, AuctionWorx

The break-even rule from Circuit's worked example: below about $250k a year of online hammer, the marketplace-versus-own-platform choice is about brand and ownership, not cost. Above it, run both. The mistake most first-time sellers make is sizing the channel by its fee, a 2% house with no bidders earns less than a 13% marketplace with traffic. Model the bidder count before the fee.

One Florida note for anyone building on it, an online-only sale is not license-regulated there, in-person floor sales often are, and the online exemption doesn't carry, so re-verify before any floor sale.

The test sale

Synthetic bidders, labeled as such. The suite asserts: undocumented lot blocked, seller bid refused, under-reserve refused, under-increment refused, same-bidder raise refused, post-close refused, close order staggered, premium math correct including the minimum floor, and an unsold lot producing exactly zero revenue.

Gross on the block came out to 215.50 across 2 sold of 3 lots, house commission 2.72, and the unsold lot generated nothing. Every check passes.

The honest verdict

The machinery is done, the money is the open question. Cataloging labor is the cost line in a HiBid sale, fees are a minor line item, and that is exactly the job a 24/7 local model is good at, intake in, description plus condition report plus disclosure out, lot scheduling, invoicing, consignor settlement. What the model can't manufacture is bidders. The bottleneck is inventory plus audience, and when the channel stalls, that's the report, bidders missing, code fine.

Top comments (0)