{"slug": "i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent", "title": "I spent three weeks sourcing 500 perfume bottles by hand. Then I built an agent to do the job.", "summary": "A developer built SupplyMe, an agent that automates B2B supplier sourcing for small manufacturing orders, after spending three weeks manually sourcing 500 perfume bottles. The agent uses Gemini to extract claims from supplier pages and a deterministic confidence-scoring function to rank suppliers, and it persists every step as an event so it can continue working after the user closes the tab.", "body_md": "*I wrote this post to enter the Google × Devpost **All Things Agentic**\n\nhackathon ([https://allthingsagentichackathon.devpost.com/](https://allthingsagentichackathon.devpost.com/)). The project is [SupplyMe](https://github.com/fillateo/SupplyMe),\n\nsubmitted in the Taskmaster category*\n\nI wanted 500 units of a 50ml fragrance made in Indonesia, on a first-batch\n\nbudget.\n\nFive hundred is a small order, and small orders are where the B2B marketplaces\n\nstop being useful to you. The factories I wanted were not listed on any of them.\n\nThe ones that were listed sorted by ad spend, and every number on every profile\n\nwas a form field somebody filled in once and never opened again.\n\nSo I did the job by hand. Three weeks, 41 tabs, three languages. My spreadsheet\n\nhad a column for minimum order quantity and nine of its rows said `?`\n\n, because\n\nmost factories never publish one. I sent the same eight questions to one\n\nsupplier at a time and got answers to four of them.\n\nThen two suppliers listed the same major fragrance brand as a customer.\n\nI read both pages twice. They matched in every way that mattered to me: a logo,\n\na sentence, no date, no contract, nobody else saying it. One of those companies\n\nwas lying and I had no way in.\n\nA factory tells you its real minimum inside a negotiation and tells the next\n\nbuyer something else. No dataset holds that number. You learn it by asking, and\n\nyou learn whether it holds by finding somebody other than the factory saying it.\n\nThat is two different jobs. Read what a supplier publishes. Wait days for the\n\nanswer to what it does not. A search engine does the first badly and the second\n\nnot at all, and a chatbot stops existing the moment you close the tab.\n\nThe thing I built is called SupplyMe. You type a product into it — *\"500 × 50ml\nEDP, Indonesia, premium packaging, minimise first-batch risk\"* — and then you\n\nIt breaks that product into the supply chain it needs, searches for real\n\nmanufacturers of each part, reads what they publish, and emails them what the\n\nweb could not answer. Days later, when a factory replies, it notices that the\n\nprice in the email is not the price on the website, puts both numbers back to\n\nthe supplier in a single follow-up, and ranks whoever survives.\n\nBecause a supplier can take three days to answer, nothing in the system waits in\n\nmemory for one. Every step is a persisted event, so a reply can arrive long\n\nafter the process that sent the email is gone and the mission still picks up\n\nwhere it left off. That is the part that makes it an agent rather than a\n\nconversation: it keeps working when you are not there.\n\nThe first thing I built is the part that answers my week-one question, and it\n\ncontains no model at all.\n\nGemini reads a page and extracts claims, stamping each one with where it came\n\nfrom. A deterministic function turns those sources into a confidence — noisy-OR\n\nwith geometric decay:\n\n```\nCORROBORATION_DECAY, CONFIDENCE_CEILING = 0.55, 0.97\nconfidence = min(1 - math.prod((1 - w[i] * DECAY**i) for i in range(n)), CEILING)\n```\n\nThe weights are the opinionated part:\n\n| Source | Weight |\n|---|---|\n| The supplier's own email | 0.90 |\n| The brand's own website | 0.85 |\n| The supplier's website | 0.75 |\n| A Maps listing | 0.55 |\nA directory listing |\n0.45 |\n| A bare search result | 0.30 |\n\nDecay makes the second corroborating source count for less than the first and\n\nthe twentieth count for close to nothing, so twenty directory listings copying\n\none press release land under the manufacturer's own spec sheet. That `0.45`\n\nis\n\nthe marketplace problem I started with, written down as a constant.\n\nOne rule held the design together. Ask a model how confident it is and you get a\n\nnumber that moves when you rephrase the prompt. Compute it from source identity\n\nand you get one that moves when the evidence changes.\n\nRanking follows the same rule: a weighted sum over price, minimum-order fit,\n\ncapability, lead time, evidence strength and logistics, at 20/20/20/15/15/10.\n\nTell a mission to minimise first-batch risk and weight slides off price and onto\n\norder-size fit. The agent that writes the recommendation receives a ranking it\n\ndid not compute and cannot reorder — hand it that power and the scores turn into\n\ndecoration.\n\nDrawing that line meant I never had to build explainability, because the\n\nexplanation is the calculation. `MOQ 500 fits an order of 500`\n\n.\n\nI started by wrapping everything in an `LlmAgent`\n\n, because that is what the\n\nframework is for and it felt like cheating not to. Then I read the traces.\n\nSix of them made one call and returned. One prompt, one schema, one response, no\n\nbranching, no tool choice. The workflow had already decided what happened next;\n\nthe model was filling in a shape.\n\nThe seventh looked like this against a real supplier:\n\n```\nread_page   https://kemasan-wangi.example.com/\nsearch_web  \"PT Kemasan Wangi Nusantara Indonesia 50ml glass perfume bottle MOQ\"\nread_page   https://kemasan-wangi.example.com/produk/botol-parfum-50ml\n```\n\nNobody scripted that sequence. It landed on the homepage, could not find what it\n\nneeded, searched for the phrase that would surface a product page, and went back\n\nfor it. It returned `moq = 500`\n\n, quoted from *\"Minimum order: 500 pcs per\ndesain\"*, and reported price and lead time as missing — which is what later\n\nSo research stayed an agent, with `search_web`\n\n, `read_page`\n\nand `query_maps`\n\n,\n\nand the other six became single structured calls. A tool loop standing in for a\n\nstructured call gets you a slower and less predictable structured call at\n\nroughly ten times the price. Working out where not to put an agent was the most\n\nuseful hour I spent.\n\nMeasured from the API's own token counts, eight suppliers researched through to\n\na full recommendation cost **$0.29**, across 98 model calls and 562,287 input\n\ntokens. Twelve suppliers costs $0.78, and that is the number I plan against.\n\nInput tokens are nearly the whole bill, because one real supplier website runs\n\nto tens of thousands of tokens and the research agent reads several per\n\nsupplier. Spend tracks how many suppliers you look at. How ambitious the brief\n\nis barely moves it.\n\nOne saving worth stealing: reasoning tokens bill as output, and extraction does\n\nnot need them. Reading a price out of an email gains nothing from a thinking\n\nbudget. Capping it took the fast tier from 1,222 output tokens per call to 405.\n\nSupplyMe still cannot tell me which one lied, and I no longer think that was the\n\nright question. It reports one claim as corroborated by the brand's own site and\n\na trade publication, and the other as the supplier's word with nothing behind\n\nit. Those two things arrive on my screen looking different, and I can click\n\neither one through to the sentence it came from. In week one I would have taken\n\nthat over an answer.\n\n`MOCK=true docker compose up --build`\n\nBuilt with Google ADK, Gemini 3.5 Flash on Vertex AI, Cloud Run, Firestore,\n\nPub/Sub, Cloud Tasks, Cloud Scheduler, Secret Manager, Places, Gmail over SMTP\n\nand IMAP, FastAPI, Next.js and OpenTofu.", "url": "https://wpnews.pro/news/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent", "canonical_source": "https://dev.to/jaitramadandij/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent-to-do-the-job-51kn", "published_at": "2026-08-31 22:09:34+00:00", "updated_at": "2026-08-31 22:53:11.614228+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "generative-ai", "artificial-intelligence"], "entities": ["SupplyMe", "Gemini", "Google", "Devpost", "All Things Agentic"], "alternates": {"html": "https://wpnews.pro/news/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent", "markdown": "https://wpnews.pro/news/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent.md", "text": "https://wpnews.pro/news/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent.txt", "jsonld": "https://wpnews.pro/news/i-spent-three-weeks-sourcing-500-perfume-bottles-by-hand-then-i-built-an-agent.jsonld"}}