The shop owner who keeps everything in her head
Walk into any market in Lagos and you'll meet her: the woman who runs a provisions store, sells rice by the bag, gives regulars credit "till month end," and runs her entire business from memory and a worn exercise book. She knows Amaka owes her ₦52,000. She knows Tunde always pays on time. She knows garri is her fastest mover and she must never run out. She is, quietly, a brilliant accountant.
Until the day she forgets. And forgetting costs money.
There are over 20 million of these micro-businesses in Nigeria alone, and the overwhelming majority keep no digital books at all. Not because they can't count — they count better than most of us — but because every app on the market asks them to fill forms, learn columns, and speak spreadsheet. The software demands they meet it halfway. They never do.
So I flipped it. I built Kredex — an AI bookkeeper you run entirely by talking to it, in plain English or Nigerian Pidgin. No forms. No columns. Just conversation:
You: Amaka took 3 bags of rice on credit, she'll pay next Friday.
Kredex: Recorded — Amaka owes ₦102,000, due next Friday. Your rice stock is now 13 bags.
And crucially, it remembers — across every conversation, forever. That persistent, structured memory is the entire point, and it's why Kredex was built for the MemoryAgent track of the Qwen Cloud Global AI Hackathon. Every drop of intelligence in it is powered by Qwen.
This post is the whole story: why I built it, how Qwen made it possible, the two-tier memory engine at its heart, how I exposed it over MCP, how I deployed it on Alibaba Cloud — and the four hard engineering problems I had to solve to make it real.
Everything Kredex can do
Before the how, here's the what — the full surface of the product, all driven from one chat box in plain English or Pidgin:
📒 Bookkeeping by conversation
- Log stock in a sentence — with cost price, selling price, and a per-item reorder level ("warn me when rice drops below 5 bags").
- Record cash sales — automatically decrements the right stock and counts the revenue.
- Record credit sales — tracks the debtor, the amount, and a due date ("Amaka took 3 bags, she'll pay Friday").
- Record payments — applied against a customer's outstanding debt, balance updated instantly.
- Record running-cost expenses — rent, transport, fuel, and the rest, so profit is real profit.
❓ Ask it anything about the business
- "What's in stock?" — the whole shelf with live counts.
- "What's low and needs restocking?" — exactly what's crossed its reorder line.
- "Who owes me money?" — every open debt with due dates, in one answer.
- "Give me today's summary" and "Am I making money this month?" — sales, costs, debts, and a plain-English profit & loss.
📸 Beyond typing
-
Receipt OCR — snap a photo of a supplier receipt and Kredex reads the line items and amounts, ready to log to stock (powered by
qwen-vl-max). -
Voice logging & talk-back — speak your books and have Kredex reply out loud (
qwen3-asr-flash+qwen3-tts-flash), for owners who'd rather talk than type.
🧾 Get paid & stay on top
- Invoices — "invoice Mr. Tunde for 2 bags of rice" produces a numbered, downloadable PDF.
- Reminders & alerts — set reminders ("remind me to call my supplier Monday") and get automatic low-stock notifications.
🧠 A memory that spans every conversation
- Cross-session recall — start a brand-new chat with zero history and Kredex still knows your prices, customers, and rules, because memory belongs to the business, not the thread.
- Correct it once, everywhere — change a price and the old value is overwritten in place (with history kept); every future answer reflects the new truth.
- A visible Memory tab — see both tiers of what Kredex knows: exact structured facts and narrative habits, with how often each has been recalled.
🧭 Look outward
- Opportunity Scout — tell Kredex where you trade and it searches the live web for grants, loans, and programs your business could actually apply for.
📊 A living dashboard
- Revenue this month, total owed to you, a business-health score, a revenue-over-time chart, and a "needs attention" feed — the whole business at a glance.
🔌 And it's reusable
- An MCP server exposes all of the above — memory and bookkeeping — to any MCP client (Claude Desktop, IDE agents), so Kredex can be the memory backend for other tools too.
Now, the how.
Do you need to be an AI expert to build this? No.
Here's the honest truth that took me a while to believe: if you can call a REST API, you can build an AI agent. There is no machine learning, no GPUs, no model training anywhere in Kredex. The "AI" is a series of well-structured HTTP calls to Qwen, and the engineering is everything around those calls — routing, memory, tools, and glue.
The only prerequisites to follow along:
- Comfort with JavaScript/TypeScript (Kredex is Node.js + Express) — though everything here maps 1:1 to Python, which I'll show.
- A Qwen Cloud account (free — more on the generous free tier below).
- Basic familiarity with Docker for deployment.
That's it. Let me show you how approachable Qwen makes this.
Why Qwen — and why it's the easiest AI platform to start with
I evaluated a few providers. Qwen won for three concrete reasons.
1. A genuinely generous free tier
New Qwen Cloud accounts get 70M+ free tokens across models. For context: I built, debugged, seeded, benchmarked, and demoed an entire multi-model agent — vision, reasoning, embeddings, the works — and stayed comfortably inside the free allowance for most of development. For a hackathon, an indie hacker, or a student testing an idea, this removes the single biggest barrier to starting.
2. It speaks OpenAI and Anthropic — so there's nothing new to learn
This is the part that made me smile. Qwen Cloud is wire-compatible with both the OpenAI SDK and the Anthropic Messages API. You don't learn a bespoke SDK. You point the tool you already know at Qwen's endpoint and keep moving.
Getting a key takes two minutes (per the official Quickstart): sign up at qwencloud.com, mint a key at home.qwencloud.com/api-keys, and export it — never hardcode secrets:
export DASHSCOPE_API_KEY="sk-your-key-here"
The hackathon's base code is in Python, and it's beautifully short:
# Python — the official Quickstart
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
)
completion = client.chat.completions.create(
model="qwen3.7-max",
messages=[{"role": "user", "content": "Summarize the benefits of solar energy."}],
)
print(completion.choices[0].message.content)
Kredex is Node.js, so here's the exact same idea in the language I actually shipped — literally the OpenAI SDK with one changed line:
// Node — what Kredex actually uses (src/lib/qwen.ts)
import OpenAI from "openai";
export const qwen = new OpenAI({
apiKey: env.QWEN_API_KEY,
baseURL: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", // ← the only Qwen-specific line
maxRetries: 2,
timeout: 30_000,
});
Same SDK. Same method names. Same streaming, same function-calling. Whatever language your idea lives in — Python, Node, or a raw curl — Qwen meets you there.
3. The right model for every job, under one client
A real product isn't one model call. Kredex uses seven Qwen models, each chosen for a specific job, all reachable through that single client by just changing the model string:
| Model | Job in Kredex | Why this one |
|---|---|---|
qwen3.5-flash |
Chat tool-calling, fact & memory extraction, Opportunity Scout | Fast and cheap — critical in an agent loop where latency compounds across rounds |
qwen3.7-max |
Deep P&L reasoning ("am I making money?") | Strong multi-step reasoning for the hard questions |
qwen-vl-max |
Receipt photo OCR → structured items | Vision-language: reads a crumpled receipt and returns clean JSON |
text-embedding-v4 |
Semantic memory (1024-d) + fuzzy item matching | High-quality embeddings power Tier-2 recall |
qwen3-asr-flash |
Speech-to-text (voice logging) | Lets owners talk their books |
qwen3-tts-flash |
Text-to-speech (Kredex talks back) | Accessibility for low-literacy users |
qwen3.5-omni-flash |
Omni voice path | End-to-end voice |
export const MODELS = {
brain: "qwen3.7-max", // reasoning
agent: "qwen3.5-flash", // fast tool-calling — the workhorse
agentFallback: "qwen3.7-max", // resilience (see "Challenges" below)
vision: "qwen-vl-max",
embedding: "text-embedding-v4",
asr: "qwen3-asr-flash",
tts: "qwen3-tts-flash",
} as const;
qwen3.5-flash deserves a special mention: in a tool-calling agent, a single user message can trigger multiple model round-trips (decide tool → run it → decide again → finally reply). If each hop is slow, the whole thing feels sluggish. Flash's low latency is what makes Kredex feel instant despite doing a surprising amount of work per message.
The tech stack
Before we go deeper, here's the whole picture:
- Frontend: React + Vite + TypeScript + Tailwind, in a custom editorial "paper & ink" design language (Fraunces + JetBrains Mono).
- Backend: Node.js + Express + TypeScript.
- Database: MongoDB (Mongoose).
- AI: Qwen Cloud via DashScope (OpenAI-compatible) — 7 models across chat, reasoning, vision, ASR, TTS, and embeddings.
-
Interop:
@modelcontextprotocol/sdk(Model Context Protocol server). - Infra: Docker + Docker Compose, Caddy (auto-HTTPS), nginx — on Alibaba Cloud Simple Application Server.
Here's the architecture in text, which doubles as the diagram spec:
┌──────────────────────────────┐
Shop owner ─────▶ │ React client (chat UI) │
(English / Pidgin) └───────────────┬──────────────┘
│ HTTPS (SSE stream)
┌───────────────▼──────────────┐
│ Express API — the Agent Loop │
│ 1. local classifier (0ms) │
│ 2. Qwen tool-calling ────────┼──▶ Qwen: qwen3.5-flash
│ 3. run tools vs MongoDB │
│ 4. Qwen streams the reply ───┼──▶ Qwen: qwen3.5-flash
└───┬───────────────┬───────────┘
two-tier │ │ ┌──▶ Qwen: qwen-vl-max (receipts)
memory ┌─────▼─────┐ ┌──────▼─────┐ ├──▶ Qwen: qwen3.7-max (P&L)
│ Tier 1 │ │ Tier 2 │ └──▶ Qwen: text-embedding-v4
│ trie │ │ vectors │
│ (facts) │ │ (memories) │ also exposed over ▼
└─────┬─────┘ └──────┬─────┘ MCP → Claude Desktop,
└────── MongoDB ─┘ IDE agents, any client
The agent loop
A message from the owner flows through three stages, cheapest first:
message
│
├─ 1. local keyword classifier (0ms, no LLM) → intent guess
│
├─ 2. Qwen (qwen3.5-flash) sees the message + tool schemas, decides
│ which tools to call with what args → we run each against MongoDB,
│ feed the results back → repeat until it stops asking for tools
│
└─ 3. Qwen streams the final natural-language reply, token by token
Stage 1 is a 1-millisecond keyword pass — no LLM, no cost — that gives a first guess at intent (and shows the owner "⚡ understood locally as a sale"). Stage 2 is where Qwen shines: it reads a messy human sentence and extracts item names, quantities, prices, and customer names, then calls the correct tool.
const res = await completeWithFallback({
model: MODELS.agent, // qwen3.5-flash
messages,
tools: toolDefs,
// Force a tool call for action intents so the model can't fabricate a
// confirmation ("I've recorded the sale…") without actually doing the work.
tool_choice: round === 0 && ACTION_INTENTS.has(intent) ? "required" : "auto",
temperature: 0.2,
enable_thinking: false,
}, MODELS.agentFallback);
There are 11 bookkeeping tools — record_sale, record_credit_sale, record_payment, log_stock, query_debts, query_stock, daily_summary, record_expense, create_invoice, save_customer_phone, set_reminder — each a plain function that mutates MongoDB and returns a result the model narrates back to the owner.
The heart of it: a two-tier memory engine
The MemoryAgent track asks for persistent, structured memory that learns and recalls across sessions. The naive approach — dump everything into a vector database — is both slow and wrong for a business, because a shop's knowledge is actually two different kinds of thing:
- Exact, correctable values — the price of rice, a phone number, a reorder level. There's one current truth, and it changes.
- Fuzzy, associative knowledge — "Amaka gets a 5% bulk discount," "never sell below cost," "I close early on Fridays for prayers."
Forcing both into one store means either your prices drift (vectors are approximate) or your habits become rigid. So I built two tiers and route each fact to the one that fits. This isn't just cleaner — it's faster, and here's exactly why.
Tier 1 — structured facts, in a canonical-key trie
Prices and terms get canonicalized into dotted keys — category.subject.attribute — and stored in a trie:
product.rice.sell_price = 40000 (overwritten 1×)
product.rice.cost_price = 28000
customer.tunde.pays_on = "end of month"
customer.tunde.phone = 08035551212
product.garri.reorder_level = 5
shop.min_margin_percent = 15
A small qwen3.5-flash call (the "canonicalizer") turns each owner message into these keyed facts. Writing is an upsert: a new value overwrites the old one in place and pushes the previous value into history ("used to be ₦34,000"). Recall is deterministic — detect which subjects a query mentions, prefix-walk the trie, return the hits. No embeddings, no similarity threshold, no model call at read time.
Why this makes Kredex faster: the questions owners ask most — "what do I sell rice for?", "what's Tunde's number?" — are answered by an in-memory trie lookup in microseconds, with zero API latency and zero hallucination surface. The authoritative stuff is never left to a probabilistic guess.
Tier 2 — narrative memory, in vectors
Habits and rules have no single value, so they go into a vector store (text-embedding-v4), where recall is scored:
score = cosine_similarity × importance × recency
then diversified with MMR (so you don't get five near-duplicate memories), and reinforced on use — memories recalled often stay strong; the rest gently decay, like human forgetting. This is associative recall for the questions a trie can't answer, like "which of my customers are unreliable?"
On every turn, the agent gets both: authoritative facts injected as ground truth, plus the most relevant narrative memories as context. That combination is why a brand-new chat with zero history still knows your shop — because memory belongs to the business, not the thread.
Reusable over MCP: one registry, two front-ends
Kredex ships a Model Context Protocol (MCP) server that exposes the shop's two-tier memory and all its bookkeeping tools to any MCP client — Claude Desktop, an IDE agent, another app. Kredex isn't just "an app with memory." It's a memory backend anything can plug into.
The elegant part: the 11 bookkeeping tools are not re-declared for MCP. An OpenAI function schema and an MCP inputSchema are both just JSON Schema, so I map the exact same registry that powers the Qwen agent straight onto MCP — one source of truth, two front-ends:
// One registry → Qwen function-calling AND MCP, for free
const bookkeepingTools = toolDefs.map((t) => ({
name: t.function.name,
description: t.function.description,
inputSchema: t.function.parameters, // JSON Schema maps 1:1
}));
Add 6 memory tools (recall_facts, remember_fact, list_facts, recall_memories, remember, list_memories) and you get 17 tools over the protocol. I drove it with a real MCP client — an actual session, no Kredex UI involved:
→ recall_facts { query: "rice price" } // Tier 1 read
{ "facts": [{ "key": "product.rice.sell_price", "value": 34000, "unit": "NGN" }] }
→ remember_fact { key: "product.rice.sell_price", value: 40000 } // external write
{ "ok": true, "stored": "product.rice.sell_price" }
→ recall_facts { query: "rice price" } // overwrite persisted
{ "facts": [{ "key": "product.rice.sell_price", "value": 40000, "unit": "NGN" }] }
An external agent, with zero knowledge of my codebase, just read and overwrote a real shop's memory over a standard protocol. That's the MemoryAgent thesis made literal.
Deploying on Alibaba Cloud: SAS vs ECS (and why I chose SAS)
The hackathon requires proof of Alibaba Cloud deployment, and Alibaba gives you two very different front doors. Picking the right one matters, so let me save you the research.
ECS (Elastic Compute Service) is the full, flexible VM — you get VPC control, auto-scaling, GPU instances, the lot. SAS (Simple Application Server) bundles compute, storage, and networking into a fixed monthly plan. It trades flexibility for radical simplicity.
Here's the decision, distilled:
| Choose SAS if… | Choose ECS if… |
|---|---|
| A single server for dev/test or low-traffic production | You need GPU instances for local model inference |
| You want fixed, predictable pricing | You need auto-scaling or load balancing |
| You want to deploy in under 5 minutes | You need full VPC control or peering |
| Your agent calls external LLM APIs (no local GPU) | Your workload is high-concurrency / mission-critical |
| You're an individual, student, or small team | You need multiple data disks or specialized storage |
Look at that fourth row. Kredex calls Qwen's APIs over HTTP and runs no models locally — there is no GPU to justify. That single fact makes SAS the correct, cheaper, faster choice. Alibaba even lists "LLM API wrapper agents" as the best-fit workload for SAS. Decision made.
Even better: when you create the SAS instance, you can pick a Docker application image that ships with Docker Engine and Compose pre-installed — so the box is ready for a containerized app the moment it boots.
How I dockerized and shipped it
Kredex runs as a four-service Docker Compose stack on one SAS box:
# docker-compose.yml — the whole production stack on a single SAS instance
services:
mongo: # MongoDB with a persistent named volume (private to the network)
server: # Express API — the agent + the two-tier memory engine
web: # nginx serving the React build + proxying /api
caddy: # HTTPS termination + auto Let's Encrypt certs (ports 80/443)
Both apps use multi-stage Docker builds to keep images small — compile in a node:20-alpine build stage, then copy only the artifacts into a lean runtime stage:
# server/Dockerfile — build TypeScript, ship only dist/ + prod deps
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./ && RUN npm ci
COPY . . && RUN npm run build
FROM node:20-alpine AS runtime
ENV NODE_ENV=production
COPY package*.json ./ && RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
Deploying is then almost anticlimactic — the reward for doing the containerization work up front:
# on the SAS instance
cp .env.example .env # add QWEN_API_KEY + JWT_SECRET
docker compose up -d --build
From a raw public IP to kredex.xyz, with automatic HTTPS
SAS auto-assigns a public IP (mine was 8.222.241.247). Two jobs remained: get a real domain onto it, and get HTTPS — without becoming a certificate administrator. Caddy handles both. I pointed kredex.xyz's DNS at the SAS IP, and Caddy did the rest: it automatically obtains and renews free. Let's Encrypt certificates and reverse-proxies traffic to the internal nginx container.
# Caddyfile — auto-HTTPS + routing, in 6 lines
kredex.xyz, www.kredex.xyz {
reverse_proxy web:80 {
flush_interval -1 # keep the SSE chat stream flowing token-by-token
}
}
That flush_interval -1 is a small but vital detail: Kredex streams replies token-by-token over Server-Sent Events, and without it the proxy would buffer the whole reply and kill the "typing" feel. One line preserves the magic.
The result: DNS → Caddy (HTTPS) → nginx (static app + /api proxy) → Express (the agent) → Qwen. A domain, a padlock, and a live agent, on a single predictable-cost server.
Four challenges (and how I solved them)
The demo is smooth now. Getting there was a series of reiterations. Here are the four that taught me the most.
1. Model exhaustion — and routing around it
Different Qwen models exhaust their free quota at different rates, and the flash tier (my workhorse) empties first. Early on, a quota wall on one model would take the whole app down with a 403.
There's also a subtle platform nuance worth flagging, because it bit me: Qwen offers a Token Plan (a prepaid credit pool, keys prefixed sk-sp-, on a token-plan.*.maas.aliyuncs.com base URL) and Pay-as-you-go (keys sk-, on dashscope-intl.aliyuncs.com). They are not interchangeable — mixing a key with the wrong endpoint returns 401 InvalidApiKey. And critically, the Token Plan is documented as being for interactive dev tools, not backend scripts. Kredex is a backend, so it must run on the pay-as-you-go endpoint. Knowing this distinction up front saves hours of confusing auth errors.
The resilience fix was a quota-aware fallback wrapper around every model call. If qwen3.5-flash returns a quota error, it transparently retries on qwen3.7-max, which still has budget — so the app never dies on a wall:
export async function completeWithFallback(params, fallbackModel) {
try {
return await qwen.chat.completions.create(params);
} catch (err) {
if (isQuotaError(err)) {
console.warn(`quota hit on ${params.model} → falling back to ${fallbackModel}`);
}
return await qwen.chat.completions.create({ ...params, model: fallbackModel });
}
}
One helper, wrapped around both the streaming and non-streaming paths, and quota exhaustion became a graceful degradation instead of an outage.
2. The model that "reasoned its way to lying"
qwen3.5-flash runs in thinking mode by default. With rich context, it would sometimes reason its way to simply answering — replying "I've recorded the sale ✅" without ever calling record_sale. Silent book corruption: the worst kind of bug, because everything looks fine.
Two fixes, belt-and-suspenders. First, enable_thinking: false on the agent path (faster too). Second, tool_choice: "required" on the first round for action intents, so the model must perform the action before it's allowed to talk about it. (Gotcha within the gotcha: tool_choice: "required" returns a 400 while thinking mode is on — you have to disable thinking first. And stray <think>…</think> tags leaked into the streamed reply until I both disabled thinking at the source and wrote a stream filter that strips the tags across chunk boundaries.)
3. Fact laundering
My canonicalizer originally read both the owner's message and Kredex's reply. Seemed reasonable. It was a disaster. When the model once hallucinated "Amaka pays on Wednesdays," that hallucination got laundered into a stored fact — because I was feeding the AI's own guess back into the fact extractor. Worse, giving it two inputs destabilized its JSON output: it emitted malformed keys that silently dropped real updates, so a genuine price change never persisted.
The fix was one line with a big principle behind it: canonicalize the owner's words only — never the AI's. Facts are the human's ground truth. The model gets to read memory; it does not get to invent it.
4. The Opportunity Scout that always returned nothing
Kredex has an Opportunity Scout that uses qwen3.5-flash with live web search (enable_search: true) to find grants and loans a shop can actually apply for. It kept returning empty, and I nearly blamed the model. It was innocent: a forced web search plus a full JSON response legitimately takes ~50 seconds, but my shared client capped every call at 30s with 2 retries. It was timing out, retrying, and dying — three times over.
The fix was a per-request override — a longer timeout and no retries for this one slow, expensive call — with results cached for 10 minutes so the wait is paid once:
const completion = await qwen.chat.completions.create(
{ model: MODELS.agent, enable_search: true, /* … */ },
{ timeout: 90_000, maxRetries: 0 }, // this call earns its long leash
);
(Bonus fifth bug, free of charge: my first MCP run hung forever because a console.log("MongoDB connected") was writing to **stdout* — which MCP reserves for its JSON-RPC protocol stream. One stray log line corrupts the handshake. All diagnostics now go to stderr. Humbling.)*
What's next for Kredex
- WhatsApp as the front door — most of these owners already live in WhatsApp; the agent loop is channel-agnostic, so this is a connector, not a rewrite.
- Proactive nudges — "you're low on garri, and Alhaji Musa delivers Tuesdays — want me to draft the order?"
-
Voice-first mode end-to-end, leaning on
qwen3-asr-flashandqwen3-tts-flash, for owners who'd rather talk than type. - Shared memory for cooperatives — multiple shops, one knowledge base.
Try it yourself
- 🌐 Live app: kredex.xyz — create a shop and just start talking to it.
- 🎬 Docs & live walkthrough: kredex.xyz/docs — a screenshot-by-screenshot tour from sign-up to a living picture of a business, plus the MCP walkthrough.
- 💻 Code: github.com/shegz101/kredex
- 🐦 Follow along: @getkredex
Closing thought
Kredex started from one belief: the woman in the market is already a great accountant — she just deserves software that speaks her language, not the other way around. Making software meet a human where she is takes real AI: fast tool-calling, vision, reasoning, embeddings, and memory that persists across every conversation.
Qwen gave me all of that behind a single, familiar, OpenAI-compatible client — with a free tier generous enough that the only limit was my own imagination. If you've been putting off building an AI agent because it felt like it needed a research team, take this as your sign. Get a key, change one line of baseURL, and go build the thing.
Thanks for reading. 🧡

Top comments (0)