





I Built an Autonomous Selling Agent That Negotiates, Gets Paid, and Ships — Here's How It Actually Works
Most people trying to sell a second-hand phone waste hours writing listings, replying to lowball offers at midnight, chasing payments, and scheduling couriers. I wanted to see how much of that loop an AI agent could own end to end — not just the listing part, but everything from photo upload to delivery confirmation.
The result is AutoSeller AI: upload a photo, and the agent handles analysis, listing generation, buyer negotiation, payment collection, and logistics. No seller intervention unless something genuinely requires a human decision.
Here's what I built, what surprised me, and what I'd do differently.
What the System Does
The agent lifecycle has seven states:
IDLE → ANALYZING → LISTING → WAITING_FOR_BUYER
→ NEGOTIATING → DEAL_CONFIRMED → AWAITING_PAYMENT
→ PAYMENT_CONFIRMED → SCHEDULING → SHIPPING → DELIVERED
Each state transition is explicit and validated. The state machine refuses invalid jumps — that constraint alone caught about a dozen bugs during development where I'd try to skip AWAITING_PAYMENT and jump straight to PAYMENT_CONFIRMED. The machine blocked it every time.
When a seller uploads a photo, Gemini 2.0 Flash analyzes it and returns structured JSON: brand, model, condition, estimated original price, resale price, defects noticed, confidence score. That data feeds into a pricing engine that applies condition multipliers and checks long-term memory for historical sell rates on that category and platform.
The Negotiation Engine
This is the most interesting part. The agent needs to negotiate with real buyers without revealing the seller's floor price, handle pressure tactics, detect suspicious behavior, and know when to escalate.
The decision logic in agent/decision_engine.py works on three thresholds:
pythonclass NegotiationConfig:
AUTO_ACCEPT_ABOVE = 0.95 # accept if offer >= 95% of asking
AUTO_REJECT_BELOW = 0.60 # reject if offer < 60% of asking
MAX_COUNTER_ROUNDS = 3 # escalate to seller after 3 rounds
MIN_PROFIT_MARGIN = 0.70 # never go below 70% of estimated value
When a buyer message comes in, the risk checker runs first. It scans for patterns like "western union", "gift card", "my shipper", attempts to share phone numbers, and pressure language like "last price, take it or leave it". High-risk messages get blocked before the negotiation engine even sees them.
If the message passes risk checks and contains a price offer, the decision engine calculates the ratio against asking price and responds:
pythondef decide_negotiation_action(offer, asking_price, min_price, counter_rounds, buyer_profile):
risk = analyze_offer(offer, asking_price, min_price)
if risk.get("auto_reject"):
return {"action": "auto_reject", "counter_price": round(asking_price * 0.90)}
if risk.get("auto_accept"):
return {"action": "auto_accept"}
if counter_rounds >= NegotiationConfig.MAX_COUNTER_ROUNDS:
return {"action": "escalate_to_seller", "offer": offer}
# smart counter: gets closer as rounds increase
ratio = offer / asking_price
if ratio >= 0.85:
counter = round(asking_price * 0.95)
elif ratio >= 0.75:
counter = round(asking_price * 0.90)
else:
counter = round(asking_price * 0.95)
return {"action": "counter", "counter_price": max(counter, min_price)}
The counter logic deliberately converges — each round the agent gives a little more ground, which mirrors how real negotiation works and avoids the agent stonewalling indefinitely.
Long-Term Memory and Learning
The agent needed to remember outcomes across transactions — not just within a session. After every completed sale, agent/feedback_engine.py records the category, platform, asking price, final price, and days to sell into a database table.
I needed proper agent memory that persisted beyond a single session. The system stores rolling averages:
pythondef record_outcome(category, platform, asking_price, final_price, days_to_sell, success):
price_ratio = round(final_price / asking_price, 2)
# rolling average update
memory.avg_price_ratio = round(
(memory.avg_price_ratio * total + price_ratio) / (total + 1), 2
)
memory.avg_days_sold = round(
(memory.avg_days_sold * total + days_to_sell) / (total + 1), 1
)
This feeds back into pricing decisions. If electronics on OLX historically close at 87% of asking price, the agent sets the initial asking price slightly higher to leave room. If a category has a low success rate on a platform, the agent recommends a different one.
For the session-level memory, I looked at Hindsight for agent memory between turns. The structured context management approach — keeping the full observation available to each decision step — matched what I needed for maintaining negotiation state across multiple buyer messages without the context bleeding between sessions.
The Hindsight agent memory approach of separating short-term (session) from long-term (cross-transaction) memory maps cleanly onto how this system needed to work: the negotiation context lives in session memory, while platform performance and pricing ratios accumulate in persistent storage.
The State Machine Was Worth the Overhead
Early versions used simple flags and booleans to track where the agent was in the pipeline. That got messy fast — race conditions between the polling dashboard and the chat endpoint, states getting partially updated, panels showing at the wrong time.
Switching to an explicit state machine with validated transitions fixed all of that:
pythonVALID_TRANSITIONS = {
AgentState.DEAL_CONFIRMED: [
AgentState.AWAITING_PAYMENT,
AgentState.NEGOTIATING, # buyer backs out
AgentState.CANCELLED
],
AgentState.AWAITING_PAYMENT: [
AgentState.PAYMENT_CONFIRMED,
AgentState.DEAL_CONFIRMED, # payment failed, retry
AgentState.CANCELLED
],
...
}
When the payment confirmation tried to jump from deal_confirmed directly to scheduling, the machine blocked it and logged the invalid transition. That's how I found the bug — not from a crash, but from a clear error message in the activity log. Invalid transitions are now immediately visible in the seller dashboard's activity feed.
Concrete Example Interaction
Here's what a full simulation run looks like:
Seller uploads iPhone photo → Gemini identifies it as iPhone 13 128GB, good condition, estimates ₹42,000 resale
Agent sets asking price at ₹46,200 (10% above estimate, adjusted by historical data)
Listing generated with title, description, tags, OLX prefilled URL
Buyer opens chat: "Is this still available?"
Agent confirms, describes condition honestly including the back scratch
Buyer offers ₹38,000 — agent counters at ₹44,000 (round 1)
Buyer offers ₹40,000 — agent accepts (87% of asking, above historical average for this category)
Deal confirmed → Razorpay payment link sent → payment confirmed
Seller dashboard shows pickup scheduling panel
Seller selects slot → Shiprocket courier booked → AWB generated
Tracking updates propagate to both buyer and seller
Total seller input: upload photo, fill name, select pickup slot. Everything else was the agent.
What I'd Do Differently
Separate the buyer and seller contexts earlier. The shared item_id approach works but creates coupling — the chat endpoint needs to initialize state if it doesn't exist, because buyers access the chat URL directly without going through the upload flow.
Persist state to the database, not just memory. Server restarts wipe all in-memory sessions. For a demo this is manageable, but in production every item state needs to live in the database from the start, not just at the end of a transaction.
The feedback loop needs more data before it's useful. The pricing adjustment based on historical ratios only becomes meaningful after several transactions per category. With zero history, the agent falls back to rules. The rules are reasonable, but the learning layer is effectively inert until you have real transaction data feeding it.
Risk detection is pattern matching, not understanding. The current checker catches keywords. A sophisticated bad actor would get through. Production would need something more robust — probably a dedicated classifier trained on marketplace fraud patterns.
Takeaways
Explicit state machines are annoying to set up and worth every line. Invalid transitions are bugs caught at definition time, not at 2am.
Separation of short-term session memory from long-term learning memory isn't just architectural taste — they have genuinely different access patterns and lifetimes.
The negotiation engine needs a floor that's always enforced, never bypassed. Every path through the code should be incapable of going below min_price, not just the happy path.
Building the simulation layer first (fake buyers, fake tracking, fake payments) let me test the full pipeline before any real API integration. The sim-to-real switch was mostly just removing the if IS_SIMULATION branches.
The full project is on GitHub: https://github.com/sandhya13r/autoseller-ai
LinkedIn Post
Most selling agents I've seen just write the listing and stop there.
I wanted to build one that actually finishes the job.
AutoSeller AI takes a photo and runs the entire pipeline: item analysis, listing generation, buyer negotiation, payment collection, and courier booking. The seller uploads one photo and selects a pickup slot. That's it.
The negotiation engine is the interesting part. It maintains a floor price the buyer never sees, counters in 3 rounds before escalating to the seller, and blocks suspicious patterns (gift card requests, external contact attempts, pressure language) before they reach the logic layer.
The learning layer records every transaction outcome — final price as a % of asking, days to sell, platform — and feeds that back into pricing decisions on the next listing. Early data, but the feedback loop is real.
A few things I learned building this:
→ Explicit state machines catch bugs at definition time. Invalid transitions are logged immediately, not discovered in prod.
→ Short-term session memory and long-term learning memory need to be separate systems. Different lifetimes, different access patterns.
→ Simulate everything first. I had fake buyers, fake payments, and fake tracking before I touched a real API. The real integration was just removing if IS_SIMULATION blocks.
→ The floor price must be architecturally unreachable, not just checked on the happy path.
Built with Gemini 2.0 Flash, Razorpay, Shiprocket, and FastAPI. Used Hindsight for agent memory across negotiation turns — the separation of session vs persistent memory it encourages matched exactly what this system needed.
Project repo: https://github.com/sandhya13r/autoseller-ai
AIAgents #AgentMemory #AI #Hindsight #LLM
Hindsight GitHub: https://github.com/vectorize-io/hindsight ← already embedded in article
Hindsight docs: https://hindsight.vectorize.io/ ← already embedded in article
Vectorize agent memory: https://vectorize.io/what-is-agent-memory ← already embedded in article
Top comments (0)