# How a Tech Lead Built a Procurement System in Telegram Without Writing Code: An Agentic Development Case Study
When internal procurement in the team turned into chaos with lost requests and uncoordinated queries, the tech lead decided to create a system based on Telegram. Without writing a single line of code, using agentic systems and a clear architecture, he built a solution with OCR, moderation, and secure deployment. Here's how to do it without losing control over quality.
Problem: Chaos Instead of a Procurement Process
In small teams, procurement often starts with informal agreements. First come scraps of paper, then notes, then a dedicated Telegram thread. A process emerges: drop the link, price, quantity, and justification. But in practice, some requests get lost, others slip through, some lack price or justification, and people forget their own requests. As a result, the tech lead spends time fighting chaos instead of managing procurement. Key takeaway: if a process involves money, it shouldn't exist as free-form chat.
Infrastructure Requirements: From "Little Bot" to Full System
The initial idea—a simple Telegram bot—quickly ran into infrastructure challenges. Due to Telegram's instability without VPN, it required:
- Foreign VPS
- Custom VPN setup
- Reliable auto-restart
- Convenient deployment
- Rollback mechanism for failed releases
This is no longer a "quick evening script," but a full engineering pipeline. Experience shows: "simple automation" often hides complex infrastructure underneath.
Agentic Development: Accelerator, Not Replacement for Thinking
The project was implemented using ChatGPT and an agentic system, but the tech lead's active involvement was essential. The process included:
- Formulating the task and assigning roles
- Clarifying business logic
- Manual testing of scenarios
- Identifying contradictions and bugs
- Iterating on targeted fixes
- Retesting
The agent accelerated the cycle: shortening the time between spotting a problem and verifying a new implementation. Key takeaway: you can delegate code syntax, but systems thinking and architecture remain critical. Without a clear process model, the agent just speeds up implementing flawed logic.
Ditching "Magic": Switching to a Wizard Flow
The first hypothesis—automatic data extraction from links—proved unviable due to:
- Site diversity
- Unstable page structures
- Ambiguous prices
- Complex data rendering
- Hostile marketplace logic
Instead, a wizard flow was implemented:
- Attach screenshot
- Provide link
- Write justification
- Specify quantity
- Manually enter price if needed (if OCR failed)
- Review final card before confirming
This approach is less "wow," but ensures controllability—the key factor for operational processes.
Why LLM Wasn't the System's Core
Though tempting to use LLM for request processing, a simple stack was chosen:
- Python
- Telegram bot framework
- SQLite
- Local OCR
- HTML reports on templates
- systemd for services and timers
Rationale:
- Runs on low-spec VPS
- No runtime dependency on external APIs
- No paid inference per request
- Easy debugging
- Predictable behavior
This choice highlights a key principle: not every task benefits from AI. Sometimes strict FSM and validation deliver more value than complex models.
State Machine: Foundation of Reliability
A request is an entity with a lifecycle including statuses:
- Draft
- Awaiting image
- Awaiting link
- Awaiting justification
- Awaiting confirmation
- Pending moderation
- Returned for revision
- Deferred to next period
- Urgent
- Sent
FSM was used to manage this cycle. Code example:
class PurchaseWizard(StatesGroup):
waiting_for_image = State()
waiting_for_link = State()
waiting_for_reason = State()
waiting_for_qty_lots = State()
waiting_for_lot_size = State()
waiting_for_manual_price = State()
waiting_for_confirmation = State()
and domain statuses:
class PurchaseStatus(str, Enum):
DRAFT = "draft"
AWAITING_IMAGE = "awaiting_image"
AWAITING_LINK = "awaiting_link"
AWAITING_REASON = "awaiting_reason"
AWAITING_QTY = "awaiting_qty_lots"
AWAITING_CONFIRMATION = "awaiting_confirmation"
COMPLETE = "complete"
PENDING_MODERATION = "pending_moderation"
RETURNED_FOR_REVISION = "returned_for_revision"
APPROVED = "approved"
CANCELLED = "cancelled"
SENT = "sent"
When roles, money, and deadlines are involved, shift from message handling to state management.
Architecture: Simplicity as a Sign of Maturity
System diagram:
Telegram user
↓
Bot handlers / FSM
↓
Purchase service
├── SQLite
├── OCR service
├── Moderation logic
├── HTML report builder
└── Reminder / maintenance jobs
↓
systemd service + timers
Project structure:
purchase_bot/
├── app/
│ ├── bot/
│ ├── db/
│ ├── services/
│ ├── templates/
│ └── main.py
├── data/
│ ├── uploads/
│ ├── reports/
│ └── bot.db
├── scripts/
├── deploy/
└── README.md
Skipping Docker and heavy DBs in the first version allowed focus on functionality. For internal tools, simple architecture is a sign of maturity, not weakness.
Telegram UX: Interface Over Message Stream
Working with Telegram revealed challenges:
- Persistent old buttons
- Overlapping cards
- Parallel old and new request versions
- Notification delays
- Inconsistent "Back" button behavior
- Screen looping
- Chat clutter
Solution: design the bot as a screen-based state system. Core principles:
- Single active screen
- Deleting outdated messages
- Safe handling of old callback buttons
- Separate rules for menus, notifications, and work cards
This approach, while not flashy, ensures daily usability.
Real Processes Break Symmetry
Implementing moderation, urgent requests, and period deferrals showed real processes don't match neat diagrams. For example, a second moderation stage can add unnecessary friction if the first fully handled the request. Logic had to be rebuilt for real needs, not diagrams. Tech leads know this pattern: if the scheme hinders flow, break the scheme.
Sneakiest Bugs at Layer Boundaries
Critical errors arose not in core features, but at layer intersections. For example:
- "Confirm" button hung due to timezone-aware vs. timezone-naive object comparison
- Data format mismatches between modules
- State sync issues on updates
These bugs underscore the importance of careful component interface design.
Key Takeaways
- Architecture over agents: Even with agentic systems, clear architecture and systems thinking are mandatory. Agents speed up implementation but don't replace design.
- Controllability over "smartness": For operational processes, reliability and simplicity trump complex automation. Wizard flows often outperform full automation attempts.
- States, not messages: With roles, money, and deadlines, design as a managed lifecycle with explicit statuses.
- Simplicity as strategy: For internal tools, avoid architectural bloat. Minimal stack on modest hardware ensures reliability and easy maintenance.
- Telegram UX demands discipline: Bots should be designed as screen systems, not message streams. Deleting outdated elements and a single screen are critical for user experience.
— Editorial Team
No comments yet.