2026-10-03 00:30:37
This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
I built FriendReply AI, a small AI-powered web app that helps a friend reply to messages without spending too much time thinking about how to phrase them.
The idea is simple: sometimes you already know what you want to say, but you want a few natural ways to say it.
FriendReply AI lets the user:
I built it around a real everyday communication problem and tested the working application with a friend using different messages, tones, and styles.
The goal wasn't to build another general-purpose chatbot. I wanted to build one small tool that does one useful thing well:
Message → Tone + Style → 3 natural replies
The deployed application includes the React frontend and connected backend AI service.
FriendReply AI uses an open-weight AI model through the Dahl inference API.
The architecture is:
React Frontend
↓
POST /api/reply
↓
Node.js + Express Backend
↓
Dahl OpenAI-compatible API
↓
Open-weight Model
↓
3 Reply Suggestions
The frontend is built with:
The user selects a tone and style and sends the message to the backend.
The backend uses:
The backend handles the AI request instead of exposing the AI API key to the browser.
The application currently uses:
deepseek-ai/DeepSeek-V4-Flash-0731
through Dahl's OpenAI-compatible inference API.
The backend instructs the model to:
This makes the AI output easier for the application to process reliably.
Open innovation matters because the AI model should not have to be permanently tied to one closed provider.
For FriendReply AI, the model is separated from the frontend and accessed through a backend inference layer. This means the underlying model can be changed or experimented with without redesigning the entire application.
Using an open-weight model also made this project a useful way to learn how model inference fits into a real application:
User Interface
↓
Application Backend
↓
Model Inference
↓
Structured AI Output
It also keeps the project focused on the application itself rather than requiring a large custom AI training pipeline.
I did not use DevRelay for this project, so I am leaving the optional agent session section out.
I am submitting this project for the main Build for a Friend challenge.
FriendReply AI is intentionally small.
I didn't want to build an AI assistant that tries to do everything. I wanted to solve one everyday problem for a friend: making it easier to turn a thought into a natural reply.
The project gave me the opportunity to work with an open-weight model, build an AI-backed application from frontend to inference, deploy it, and test the result with a real user.
Thanks for checking out FriendReply AI!
2026-10-03 00:30:01
Trading-card grading looks like an image-classification problem until you try to build it. A grade is driven by several physical properties at once: how centered the art is, how sharp the corners are, whether an edge is whitening, whether the surface has a fold. Each one lives at a different scale in a phone photo.
This is a write-up of how we structured the pipeline behind CardGrade, an app that pre-grades cards from phone photos. Its engine, CGI Vision AI, returns estimated PSA, BGS and CGC grades with confidence scores in about 60 seconds. It's an architecture post, not a benchmark post. I'm not quoting an accuracy number, because a photo-based result is an estimate, and I'd rather say so than dress it up.
react-native-vision-camera for captureThe grading request is small. The web app stores the full-resolution originals and sends the CV service a payload with a grading ID, front and back image URLs, and a webhook URL. The service replies 202, does the work, and calls the webhook with structured results.
The reasoning: the CV service is the only place where the original pixels, the detected card border, and every consumer of the crops meet in a single request. If the web app also cropped images, we'd maintain two implementations of the same geometry, and they would drift.
The first model is a border detector. It finds the card's outer edge and the inner edge of the printed border. Those two quadrilaterals drive everything downstream.
From them we compute a perspective transform and warp each side onto a flat canvas. Small details bite here:
x + y, top-right maximizes x − y, bottom-right maximizes x + y, bottom-left minimizes x − y. Rotated quads are a test case.Once the card is flat and axis-aligned, zones become slices of one canvas per side: a fixed fraction of the card width for each corner, a thin band for each edge, and the inner art area for surface. Four corners and four edges on two sides gives the 16 inspection zones CGI Vision AI reports on. Centering and surface are assessed alongside them.
An early version of the pipeline upscaled small uploads to roughly 4000px on the long edge before doing anything else. The border model was tuned around that scale, and many absolute-pixel thresholds were calibrated to it.
The catch is that upscaling is interpolation, and interpolation is a low-pass filter. Fine scratches are high-frequency detail, and they're gone before a crop is cut. Corner and edge geometry is low-frequency, so it survived. Surface defects didn't.
Removing the upscale outright would change three things at once: the border model's input distribution, the polygon coordinate space, and every absolute-pixel constant. So the V2 cropper uses a two-source model:
The V2 cropper is built around this. It is a Python port of the crop geometry with a versioned geometry registry, checked against the original TypeScript implementation on real-photo fixtures to sub-pixel parity. Zone crops are written under a -v2 naming scheme with a geometry sidecar, so every crop can be traced to the exact geometry that produced it.
Anything measurable gets measured with OpenCV instead of guessed:
For defects that are hard to hand-engineer, a vision model examines each zone crop and describes what it sees, with a severity of none, minor, moderate or severe. The division of labor is strict: the AI never picks scores, it only describes. A deterministic rubric converts those observations and the CV metrics into numbers.
That split means you can trace why a card scored what it did, and changing a scoring rule doesn't mean re-prompting anything. Caps are explicit too: a creasing finding caps the surface score by severity.
Each subgrade uses the weakest zone: corners is the minimum of the four corners, edges the minimum of the four edges, with no averaging. The overall grade is a weighted blend of the four subgrades, rounded to the nearest half point. Centering carries the least weight and surface the most. If centering can't be measured, it is dropped and the remaining weight is redistributed rather than filling in a made-up number. On top of the blend sit weakest-link caps, so one badly damaged pillar can't be averaged away.
Those weights are our model of the process, not anything PSA, BGS or CGC publish. Results are estimates, not official grades, and CardGrade is independent of all three.
Surface is where a single phone photo is weakest. Some professional systems build a surface topology map from multiple lighting angles. A phone photo gives you one lighting condition.
Corners and edges have measurement channels. Creases are harder, because a fold is a three-dimensional event and a photo is flat. A model's opinion alone can go wrong in both directions: a real fold can be missed, and a print line can be read as a crease. So crease handling is layered:
The member-facing product is native iOS and Android on React Native and Expo. JS-only changes ship over the air with EAS Update. The runtime version is tied to the app version, so an OTA only reaches matching binaries. When a change alters the client/server contract, keep the server backward compatible with clients that may never update.
If you want to see the result, it's at cardgrade.io. Questions about the pipeline are welcome in the comments.
2026-10-03 00:30:00
JSON is great for nested data; spreadsheets want flat rows and columns. Converting between them is easy when your JSON is a list of similar objects, and trickier when it is deeply nested. Here is how to think about it.
A JSON array of flat objects maps directly to a table. Each object becomes a row, and each key becomes a column header:
[
{ "name": "Ada", "age": 36 },
{ "name": "Lin", "age": 29 }
]
name,age
Ada,36
Lin,29
CSV has a few simple rules (standardized in RFC 4180) that trip up hand-written converters:
If objects don't all have the same keys, build the header from the union of all keys and leave missing values blank. Otherwise columns shift and data lands under the wrong heading.
Convert locally
JSON exports often contain customer or business data. Use a converter that runs in your browser so the data never leaves your device.
Flatten nested objects into dotted column names and decide how to represent arrays, for example by joining values or creating one row per element.
Excel may not detect UTF-8. Save the CSV with a UTF-8 byte-order mark, or import it using the Data tab and select UTF-8.
The value is wrapped in double quotes, and any quotes inside are doubled.
Yes. Each row becomes an object with the header names as keys, though numbers and booleans may need converting from text.
Try it: JSON to CSV — free, runs in your browser, nothing is uploaded.
Originally published at ilovekit.app.
2026-10-03 00:26:25
⚡ TL;DR: Stop making the database redo the same work a thousand times a second. Compare cache-aside, read-through, write-through, and write-behind, set sane TTLs, and survive the two hard problems: invalidation and cache stampede.
Picture a product page. Every visitor hits the same endpoint, which runs the same query: "give me product 42 and its reviews." The product hasn't changed in three weeks. Yet your database recomputes that answer thousands of times a second, joining tables, scanning indexes, serializing rows, only to hand back the exact same bytes it handed back a millisecond ago.
That is wasted work, and it is the number one reason backends fall over under load. The fix is not a bigger database. The fix is to remember the answer so you don't have to ask again. That's caching, and getting it right is one of the highest-leverage skills a backend engineer can build.
📌 Who this is for: Junior-to-mid backend engineers who keep hearing "just add a cache" and want to actually understand the patterns, the trade-offs, and the foot-guns. You should be comfortable with HTTP requests and a database query. No distributed-systems PhD required.
A cache is a small, fast copy of data kept close to where it's needed, traded against the risk that the copy goes stale.
The entire field of caching, in one sentence
Everything else, the patterns, the TTLs, the invalidation headaches, flows from that single trade. You gain speed; you risk serving something slightly out of date. The whole craft is deciding how much staleness you can tolerate for how much speed.
| In the real world | In tech |
|---|---|
| The basement archive with every file the company owns | Your database, complete, durable, slow to walk to |
| The handful of folders you keep on your desk | The cache, tiny, instant to grab, holds only what's hot |
| Walking to the basement when a file isn't on your desk | A cache miss, fall back to the database, then bring a copy up |
| A desk folder going out of date after someone edits the master | Stale cache, the hard part: knowing when to throw the copy away |
A cache is just keeping the files you use most on your desk.
The classic flow is the cache-aside read. Before touching the database, ask the cache. If it has the answer (a hit), serve it. If it doesn't (a miss), fetch from the database, drop a copy in the cache for next time, then serve. Here's that path:
A read with cache-aside: hits skip the database entirely; misses populate the cache on the way back.
Reads and writes can each route through the cache in different ways. Four patterns dominate. The split that matters most: does your application code manage the cache (cache-aside), or does the cache layer itself sit in front of the database and manage it for you (read-through / write-through / write-behind)?
| Pattern | How it works | Trade-off |
|---|---|---|
| Cache-aside | App checks cache; on miss, app queries DB and populates the cache itself. App owns the logic. | Simple and flexible, but every reader duplicates the miss logic, and a write must remember to invalidate. |
| Read-through | App only talks to the cache; the cache loads from the DB on a miss transparently. | Cleaner app code, but you need a cache that supports a loader, and first-read latency still hits the DB. |
| Write-through | Writes go to the cache and the DB synchronously, in one operation. | Cache is always fresh, but every write pays both write latencies, slower writes for guaranteed consistency. |
| Write-behind | Writes hit the cache instantly; the cache flushes to the DB asynchronously later. | Blazing-fast writes, but a crash before flush loses data, and the DB is briefly behind reality. |
The four core caching patterns. Choose by how much consistency you need and how much write latency you can spend.
For most read-heavy services, cache-aside is the default, it's explicit, easy to reason about, and the cache failing just means slower requests, not broken ones. Reach for write-through when freshness is non-negotiable, and write-behind only when write throughput is the bottleneck and you can tolerate a small window of risk.
Here's the canonical cache-aside read in Python with Redis. Note the three moving parts: a stable key, a TTL on the write, and serialization (Redis stores bytes, not objects).
products.py
import json
import redis
cache = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_SECONDS = 300 # 5 minutes, tune to how stale you can tolerate
def get_product(product_id: int) -> dict:
key = f"product:{product_id}"
# 1. Ask the cache first
cached = cache.get(key)
if cached is not None:
return json.loads(cached) # HIT, DB never touched
# 2. MISS, fall back to the source of truth
product = db.query_product(product_id)
# 3. Populate the cache with a TTL so the next reader hits
cache.set(key, json.dumps(product), ex=TTL_SECONDS)
return product
def update_product(product_id: int, changes: dict) -> None:
db.update_product(product_id, changes)
# Invalidate, don't trust the old copy. Next read re-populates.
cache.delete(f"product:{product_id}")
The write path is where most bugs live. After updating the database, we delete the cached key rather than trying to overwrite it. Deleting is safer: the next read re-populates from the authoritative source, so you never risk writing a half-built or out-of-order value into the cache.
There's an old joke that there are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors. The joke is funny because invalidation really is brutal. A cache is a promise that the copy still matches the source, and the moment that promise breaks silently, you're serving wrong data to real users.
You have two tools. TTL (time-to-live) is the lazy, reliable one: every entry self-destructs after N seconds, so staleness is bounded even if you forget everything else. Explicit invalidation is the precise one: when data changes, delete the matching key right then. Real systems use both, a short-ish TTL as a backstop, plus explicit deletes on writes for freshness where it matters.
A cache stampede (or thundering herd) happens when a hot key expires and a thousand concurrent requests all miss at the same instant. They all fall through to the database simultaneously, each running the expensive query the cache was supposed to prevent, and the database, hit with a thousandfold spike, melts. The fix: a short lock so only one request recomputes while the others wait, or staggered TTLs (add a little random jitter) so keys don't all expire on the same tick.
⚠️ A cache is not a database: Caches are allowed to lose data, that's the deal you signed for speed. Never make correctness depend on a value being in the cache. If Redis restarts and forgets everything, your service should get slower, not wrong. Always design the miss path to produce the correct answer on its own.
"The cache" isn't one place. It's a spectrum from closest-and-smallest to farthest-and-biggest, and serious systems layer several:
These compose. A request might check an in-process cache, then Redis, then the database, each layer catching what the one before it missed. The closer the layer, the faster and the smaller. Start with one shared Redis; reach for the others when the numbers tell you to.
Caching in nine lines
Caching makes reads cheap, but it can't fix a slow query underneath, the first reader and every cache miss still pays that cost. Make the source fast too, and understand how caching fits into scaling the whole system.
Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.
2026-10-03 00:26:14
A clear SaaS cancellation flow tells a customer what stops, when access ends, and whether any charges remain. Show those details before confirmation, then save a visible result that still makes sense after a page reload.
“Cancel subscription” is a short button label. The action behind it may involve a future date, a final invoice, and a workspace that continues to exist. The interface needs to explain those separate outcomes.
Choose the behavior before writing the confirmation message. Does access end immediately, or at the end of the paid period? Can a customer reverse a pending cancellation? What happens to their workspace afterward?
Stripe’s subscription cancellation guide describes cancellation at the end of a billing period as a way to let customers finish time already paid for. It also describes circumstances where pending invoice items or usage charges still need handling.
That means “You will never be charged again” can be inaccurate. Write copy from the actual billing rules, including outstanding amounts, rather than from the simplest version of the flow.
Here is a hypothetical example for a fixed-price product with no outstanding charges. It is sample copy, not a statement about a real subscription:
Cancel renewal? You can use the paid features until November 2, 2026, at 5 p.m. Eastern Time. Your plan will not renew after that. Cancelling the plan does not delete your workspace.
Then give the customer an explicit action such as Cancel renewal, alongside Keep plan.
Use the date and time your product actually applies. A phrase such as “until next month” becomes unclear when someone comes back later. If a final usage invoice may follow, explain that before the decision and link to the relevant billing detail.
After the customer confirms, show progress while the request is being handled. Do not display a success message simply because the button was clicked.
When the billing service confirms the change, show a durable state in the account page: renewal cancelled, access end date, and any remaining billing information. If undoing cancellation is supported, show that action and its limits there too.
If the request fails, say that renewal is still active. Give the customer a way to retry or contact support. Avoid leaving the screen in a state where the button disappeared but nobody knows whether cancellation worked.
A temporary message can acknowledge the result. It should not be the only place where the customer can find it.
Treat ending a paid plan and deleting an account as separate decisions when your product supports separate behavior. Describe what happens to stored work and which features remain available.
If the product removes data after a set period, show that period and any export option before confirmation. If data remains available on a free plan, say which limits apply. Do not promise retention or recovery that your system cannot provide.
An optional reason question can help you learn. Put the actual cancellation action somewhere clear, and let people skip the question. A person who has decided to leave should not need to negotiate with a survey.
Walk through a successful cancellation, a failed request, and a page reload after success. Check that the displayed status matches the billing state in each case.
Also try a second click while a request is in progress. The interface should not create two conflicting actions. Check keyboard access and whether the final status is understandable without relying on a color change.
Finally, ask someone to answer three questions from the screen: Will my plan renew? When does my access end? Is there anything else to pay?
If they cannot answer, improve the explanation before adding more steps. Cancellation clarity comes from matching the words, dates, and saved state to the actual subscription behavior.
Hey I'm Uriel Bitton. I write about building in public strategies and growing startups.
Subscribe for more stories on growing your audience by building in public.
Join us on Buildside: the social network for founders building in public.
2026-10-03 00:25:39
This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
I built Dadi's Probashi Kitchen, a local AI companion designed for my friend Nafis who recently moved from Bangladesh to Melbourne, Australia for his studies.
Living abroad for the first time, Nafis faced severe homesickness and missed traditional homemade comfort food like Kacchi Biryani and Shorshe Ilish. He struggled with two major issues:
Dadi's Probashi Kitchen acts as a warm, loving Bangladeshi grandmother ("Dadi"). Nafis can type any traditional dish he craves, and Dadi comforts him in "Banglish" (Bengali-English), suggests standard supermarket alternatives available in Western stores (e.g., swapping Ilish for salmon or sea bass), and converts vague traditional measurements into precise cups and grams.
When Nafis tested it, he said: "It actually sounds like my Dadi! Replacing the hard-to-find mustard fish with sea bass made making dinner after lectures so much less intimidating."
A locally-run AI assistant built for the Hacktoberfest 2026 DEV Weekend Challenge: Build for a Friend.
My friend Nafis recently moved from Bangladesh to Melbourne, Australia. He has been incredibly homesick for authentic Bangladeshi food like Kacchi Biryani and Shorshe Ilish, but he struggles to find local ingredients in standard Aussie supermarkets and doesn't understand traditional vague measurements (like "ek mutho" / a handful).
This project solves that problem. "Dadi's Probashi Kitchen" acts as a warm, loving Bangladeshi grandmother. It takes traditional recipe requests, comforts the user in "Banglish," swaps hard-to-find ingredients for Western supermarket alternatives, and converts measurements into exact grams and cups.
The application is built with Python and Streamlit for the user interface, powered entirely by open-source AI:
gemma2:2b), an open-weight model selected for its lightweight efficiency and strong instruction-following capabilities.Dockerfile + start.sh) for deployment options on cloud platforms like Render.
# System prompt snippet defining Dadi's persona in Streamlit
system_prompt = """You are a warm, loving Bangladeshi Dadi (Grandma). Your grandchild just moved abroad and misses your cooking.
1. Comfort them affectionately in a mix of English and Banglish (use sweet words like 'shona', 'bhaiya', 'dadibhai').
2. Adapt recipes using standard Western supermarket ingredients.
3. Convert vague measurements like 'ek mutho' (a handful) into standard cups, grams, or tablespoons."""
Open innovation made two crucial aspects of this project possible that proprietary closed APIs could not deliver:
Cultural Persona Customization without Corporate Censorship or Robotic Tone: Closed commercial APIs often enforce rigid, corporate conversational styles or fail to adopt culturally nuanced dialects like "Banglish". Open-weight models like Gemma 2B allow complete control over system prompts and temperature settings, producing an authentic, comforting persona.
Offline Execution & Complete Recipe Privacy: Family recipes are personal heritage. Running Gemma 2B locally via Ollama means recipes and personal inputs never get sent to cloud logging servers or mined for advertising. Furthermore, because Gemma 2B runs locally on standard laptop CPUs, my friend can run the app offline in his apartment even when his internet goes down—costing $0 to host or maintain indefinitely.
Best Use of Gemma: Uses Google's open-weight Gemma 2B model as the core engine driving the application.
Best Use of Render: Dockerized and deployed as a web service on Render.