Watch it catch a confident mistake.
A customer sends a message in Arabic, English, or both at once. The model reads it and suggests an order. Nothing it suggests reaches the merchant until deterministic code has checked every field against the catalogue and the rules.
Looks like an order. Sent to the parser.
- Contains a quantity token
- Matches at least one catalogue term
- Not a greeting-only message
- Not a duplicate of a message seen in the last 60s
Delivery requested for tomorrow.
Draft created with 1 open question. Nothing is confirmed until answered.
total 122.00- pass
lines[0].sku
SKU exists in the catalogue. KNF-004 — Knafeh tray, large.
- pass
lines[0].quantity
Quantity within the catalogue maximum. 2 of a possible 10.
- pass
lines[0].unitPrice
Price matches the catalogue. 45 matches.
- pass
lines[1].sku
SKU exists in the catalogue. BKL-011 — Baklava box, 500g.
- pass
lines[1].quantity
Quantity within the catalogue maximum. 1 of a possible 15.
- pass
lines[1].unitPrice
Price matches the catalogue. 32 matches.
- needs a human
customerName
Required before the order can be confirmed. Not in the message. The merchant is asked, never guessed at.
- pass
address
Resolves to a served delivery area. Al-Bireh is in the merchant’s delivery zone.
The parse above is pre-computed — there is no model call on this page. The validation below it is not: that is validateOrder() running here, the same pure function its unit tests cover. It has no access to the confidence score, which is why editing the price to whatever the customer claimed still gets refused.
Note
The parse step is the only part a model touches. Everything after it is ordinary code with ordinary tests: catalogue lookup, quantity bounds, price reconciliation, address completeness. A confident wrong parse is still caught, because the validator does not care how confident the parse was.
I write SQL against real data.
Not “familiar with SQL”. Here is a schema shaped like the one Mo’een runs on, and here are queries I actually write against it.
SyntheticSynthetic data, shaped like the real schema. Pilot merchants’ orders are theirs, not mine to publish.
merchants
One row per tenant. Everything else hangs off this.
- iduuid— primary key
- handletext— Instagram handle
- citytext
- joined_attimestamptz
messages
Every inbound message, whether or not it became an order.
- iduuid
- merchant_iduuid— tenant key — never optional
- bodytext
- scripttext— 'ar' | 'en' | 'mixed'
- received_attimestamptz
orders
A draft becomes an order only once the merchant confirms it.
- iduuid
- merchant_iduuid— tenant key
- message_iduuid— what it was parsed from
- statustext— 'draft' | 'confirmed' | 'rejected'
- parse_confidencenumeric
- needed_editboolean— did the merchant change it
- created_attimestamptz
order_lines
One row per line item.
- order_iduuid
- skutext
- quantityinteger
- unit_pricenumeric
-- Was the parse good enough to leave alone?
-- Bucketed by the model's own confidence, so we can see whether that
-- number means anything. The tenant predicate is not optional.
select
width_bucket(parse_confidence, 0.5, 1.0, 5) as confidence_bucket,
count(*) as orders,
round(100.0 * avg(case when needed_edit
then 0 else 1 end), 1) as pct_untouched,
round(100.0 * avg(case when status = 'confirmed'
then 1 else 0 end), 1) as pct_confirmed
from orders
where merchant_id = current_tenant()
and created_at >= now() - interval '90 days'
group by 1
order by 1;| confidence_bucket | orders | pct_untouched | pct_confirmed |
|---|---|---|---|
| 0.50–0.60 | 41 | 22.0 | 51.2 |
| 0.60–0.70 | 88 | 35.2 | 68.2 |
| 0.70–0.80 | 164 | 51.8 | 79.9 |
| 0.80–0.90 | 297 | 68.4 | 88.6 |
| 0.90–1.00 | 412 | 74.5 | 91.3 |
Confidence correlates with being left alone, but it flattens badly at the top: a quarter of the model’s most confident parses still needed a human edit. That flattening is the entire argument for the validation gate.
Note
The schema mirrors production: orders, order lines, merchants, and the message each order was parsed from. Multi-tenant, so every query has to be ownership-aware — the tenant predicate is not optional and is enforced in the database as well as the API.
I ship production systems, not demos.
Mo’een’s worker processes every inbound message through a queue with leases, retries, dead-lettering and heartbeats. The interesting part is not the happy path. It is what happens when a worker dies holding a lease.
- 00jobsMessage arrives. Row written to the jobs table.
- Next.js 16
- React 19
- TypeScript
- Tailwind v4
- Supabase
- Postgres
- Provider-neutral AI adapter, so no model vendor is load-bearing
- Queue-backed worker with leases, retries and dead-letter handling
- Multi-tenant, with ownership enforced at both the database and the API layer
Note
A lease is a claim with an expiry, so a worker that dies mid-job cannot block the queue forever — the lease lapses and the job is reclaimed. Retries are bounded and back off; anything that exhausts them goes to the dead-letter table with its error, where it can be inspected instead of silently disappearing.
I understand retrieval, not the buzzword.
Documents get chunked, embedded, and searched by cosine similarity. I wrote the cosine by hand rather than pulling a library, because the interesting decisions are in the chunking and the cutoff, not the dot product.
What happens if the supplier misses the delivery date?
- c-114MSA §7.2 — Delivery0.891
Where the Supplier fails to deliver the Services by the Delivery Date, the Customer may claim liquidated damages of 0.5% of the Charges per week of delay, up to a maximum of 5%.
- c-118MSA §7.4 — Delay notification0.847
The Supplier shall notify the Customer in writing within two Business Days of becoming aware that a Delivery Date is at risk, stating the revised date and the cause.
- c-131MSA §9.1 — Termination for cause0.782
Either party may terminate this Agreement immediately where the other commits a material breach which is not remedied within thirty days of written notice.
- c-092MSA §5.3 — Acceptance testing0.751
The Customer shall have ten Business Days from delivery to conduct Acceptance Tests and notify the Supplier of any failure to meet the Acceptance Criteria.
- c-076MSA §4.1 — Charges0.719
The Charges are payable within thirty days of the date of a valid invoice. Late payment attracts interest at 2% above base rate.
Scores respectably. Answers nothing.
- c-155MSA §12.6 — Notices0.706
Any notice under this Agreement shall be in writing and delivered by hand or sent by pre-paid first class post to the address set out in the Particulars.
Scores respectably. Answers nothing.
The two chunks below the line score in the low 0.7s and are worthless for this question — they are contract boilerplate that reads like every other clause. That is what similarity measures: whether two passages talk alike, not whether one answers the other. Set the cutoff too low and you retrieve fluent noise, which is worse than retrieving nothing, because it is what the model will confidently cite.
// No library. It is eight lines, and the interesting
// decisions are elsewhere.
function cosine(a: Float32Array, b: Float32Array): number {
let dot = 0, na = 0, nb = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
na += a[i] * a[i]
nb += b[i] * b[i]
}
return dot / (Math.sqrt(na) * Math.sqrt(nb))
}Note
Similarity is not relevance. A high cosine score means two chunks talk alike, which is why the cutoff matters more than the ranking: past a certain score, you are retrieving noise that reads plausibly, and that is worse than retrieving nothing.
I turn data into decisions.
Retention by weekly cohort. The question is never “what is our retention” — it is which cohort, measured from when, and what you plan to do differently depending on the answer.
SyntheticSynthetic cohorts, modelled on pilot-shaped retention. The pilot’s real numbers are not mine to publish.
- Cohorted by
- Week the merchant connected their Instagram account
- Excludes
- Merchants who never completed onboarding
Switching the definition moves week-4 retention by about nine points without a single number in the underlying data changing. That is the point: “retention” is not a measurement until you say what it counts. Note the cohort sizes too — at n=14 one merchant is seven points, so a week-to-week swing here is one person going quiet, not a trend. Later cohorts holding better than earlier ones is the only part of this chart that is evidence the product changed rather than the customers.
Note
Cohorting by signup week rather than by calendar week is what makes the curve mean anything: it separates “our product got better” from “we acquired different people”. The window definition changes the number more than most product changes do.
Track record.
Work, study and cities, on one axis. The overlap is the information — the master’s and full-time work ran at the same time, in a different country from where I started.
- 2024-01 — now
Product Management Specialist, Consumer · Jawwal, Ramallah
Fintech products, fixed and mobile features, campaign delivery.
- 2025-01 — now
Founder · Mo’een, Ramallah
In pilot with 8 merchants.
- 2021-09 — 2023-06
MSc Business Informatics · Corvinus University, Budapest
Stipendium Hungaricum scholarship.
- 2018-09 — 2021-06
BSc Computer Science · ELTE, Budapest
- 2022-06 — 2022-09
Data Engineering Intern · ASAL Technologies, Ramallah
ETL pipelines, AWS, analytics integration.
Tools
- SQL / PostgreSQL
- Python
- TypeScript
- Databricks
- Power BI
- SAP
- Supabase
- Vercel
Note
Two of these overlap by two years. That was the point: the degree was in business informatics while the work was in data engineering, and each was the reason the other made sense.
Here is what I would build for you.
Scoped, with a deliverable and a timeline rather than a day rate and a vague promise.
Document to structured data
A pipeline that turns your PDFs, invoices or messages into validated rows, with a review step for anything it is unsure about.
Includes the validation layer, not just the extraction. The failure mode you care about is a confident wrong answer, and that is what the gate is for.
Typically3–4 weeksChurn and cohort analysis
Cohorted retention over your actual data, with the queries handed over so you can re-run them without me.
You get the SQL and the definitions, not a dashboard you cannot change. The definitions are the deliverable.
Typically2 weeksArabic and MENA-language AI systems
Extraction or classification that works on mixed Arabic/English text, evaluated on your data rather than on a benchmark.
Mixed-script, dialectal and code-switched text breaks most off-the-shelf pipelines. I have shipped this and I know where it breaks.
Typically4–6 weeks
Describe the problem — I’ll tell you if I’m the wrong person for it.
Note
Each of these is something I have already built once. The timeline is what it took, not what I hope it would take.